package cache import ( "crypto/sha256" "encoding/json" "fmt" "os" "path/filepath" "sync" "time" ) // PackageIndex represents a cached index of Debian packages type PackageIndex struct { mu sync.RWMutex packages map[string]*PackageEntry binaries map[string]string // binary package -> source package mapping lastUpdate time.Time version string } // PackageEntry represents a single package entry in the index type PackageEntry struct { Package string Version string Directory string DSCName string DSCSHA256 string Binaries []string BuildDepends []string } // IndexCache manages persistent package indices type IndexCache struct { cacheDir string indices map[string]*PackageIndex // key: mirror:suite:component mu sync.RWMutex } // NewIndexCache creates a new index cache manager func NewIndexCache(cacheDir string) *IndexCache { if cacheDir == "" { cacheDir = defaultCacheDir() } return &IndexCache{ cacheDir: cacheDir, indices: make(map[string]*PackageIndex), } } // GetIndex retrieves or creates a package index for the given mirror/suite/component func (c *IndexCache) GetIndex(mirror, suite, component string) (*PackageIndex, error) { key := fmt.Sprintf("%s:%s:%s", mirror, suite, component) // Fast path: check if already loaded c.mu.RLock() if idx, exists := c.indices[key]; exists { c.mu.RUnlock() return idx, nil } c.mu.RUnlock() // Slow path: load from disk or create new c.mu.Lock() defer c.mu.Unlock() // Double-check after acquiring write lock if idx, exists := c.indices[key]; exists { return idx, nil } // Try to load from disk cacheFile := c.cacheFilePath(mirror, suite, component) if idx, err := c.loadIndexFromFile(cacheFile); err == nil { c.indices[key] = idx return idx, nil } // Create new empty index idx := &PackageIndex{ packages: make(map[string]*PackageEntry), binaries: make(map[string]string), lastUpdate: time.Time{}, version: "1.0", } c.indices[key] = idx return idx, nil } // LookupPackage finds a package by name in the index (O(1) lookup) func (idx *PackageIndex) LookupPackage(name string) (*PackageEntry, bool) { idx.mu.RLock() defer idx.mu.RUnlock() entry, exists := idx.packages[name] return entry, exists } // LookupBinary finds a binary package and returns its source package func (idx *PackageIndex) LookupBinary(binaryName string) (string, bool) { idx.mu.RLock() defer idx.mu.RUnlock() source, exists := idx.binaries[binaryName] return source, exists } // AddPackage adds a package to the index func (idx *PackageIndex) AddPackage(entry *PackageEntry) { idx.mu.Lock() defer idx.mu.Unlock() idx.packages[entry.Package] = entry // Index binaries for fast lookup for _, binary := range entry.Binaries { if binary != "" && binary != entry.Package { idx.binaries[binary] = entry.Package } } } // Save persists the index to disk func (idx *PackageIndex) Save(cacheFile string) error { idx.mu.RLock() defer idx.mu.RUnlock() // Ensure cache directory exists if err := os.MkdirAll(filepath.Dir(cacheFile), 0755); err != nil { return err } // Create temporary file tempFile := cacheFile + ".tmp" f, err := os.Create(tempFile) if err != nil { return err } defer f.Close() // Write JSON encoder := json.NewEncoder(f) encoder.SetIndent("", " ") data := struct { Packages map[string]*PackageEntry Binaries map[string]string LastUpdate time.Time Version string }{ Packages: idx.packages, Binaries: idx.binaries, LastUpdate: time.Now(), Version: idx.version, } if err := encoder.Encode(data); err != nil { os.Remove(tempFile) return err } // Atomic rename if err := os.Rename(tempFile, cacheFile); err != nil { os.Remove(tempFile) return err } return nil } // IsExpired checks if the cache is older than maxAge func (idx *PackageIndex) IsExpired(maxAge time.Duration) bool { idx.mu.RLock() defer idx.mu.RUnlock() return time.Since(idx.lastUpdate) > maxAge } // GetStats returns index statistics func (idx *PackageIndex) GetStats() (packageCount int, binaryCount int) { idx.mu.RLock() defer idx.mu.RUnlock() return len(idx.packages), len(idx.binaries) } // loadIndexFromFile loads an index from disk func (c *IndexCache) loadIndexFromFile(cacheFile string) (*PackageIndex, error) { f, err := os.Open(cacheFile) if err != nil { return nil, err } defer f.Close() var data struct { Packages map[string]*PackageEntry Binaries map[string]string LastUpdate time.Time Version string } if err := json.NewDecoder(f).Decode(&data); err != nil { return nil, err } idx := &PackageIndex{ packages: data.Packages, binaries: data.Binaries, lastUpdate: data.LastUpdate, version: data.Version, } // Validate integrity if idx.packages == nil || idx.binaries == nil { return nil, fmt.Errorf("invalid index file: missing maps") } return idx, nil } // CacheFilePath returns the cache file path for given parameters (public method) func (c *IndexCache) CacheFilePath(mirror, suite, component string) string { return c.cacheFilePath(mirror, suite, component) } // cacheFilePath returns the cache file path for given parameters func (c *IndexCache) cacheFilePath(mirror, suite, component string) string { // Create safe filename from mirror URL mirrorHash := sha256.Sum256([]byte(mirror)) mirrorShort := fmt.Sprintf("%x", mirrorHash)[:8] filename := fmt.Sprintf("package_index_%s_%s_%s.json", mirrorShort, suite, component) return filepath.Join(c.cacheDir, filename) } // defaultCacheDir returns the default cache directory func defaultCacheDir() string { if dir := os.Getenv("ZSVO_CACHE"); dir != "" { return dir } if home, err := os.UserHomeDir(); err == nil { return filepath.Join(home, ".cache", "zsvo") } return "/var/cache/zsvo" } // Clear removes all cached indices func (c *IndexCache) Clear() error { c.mu.Lock() defer c.mu.Unlock() c.indices = make(map[string]*PackageIndex) return os.RemoveAll(c.cacheDir) } // CleanupExpired removes expired cache files func (c *IndexCache) CleanupExpired(maxAge time.Duration) error { entries, err := os.ReadDir(c.cacheDir) if err != nil { if os.IsNotExist(err) { return nil } return err } cutoff := time.Now().Add(-maxAge) for _, entry := range entries { if entry.IsDir() { continue } info, err := entry.Info() if err != nil { continue } if info.ModTime().Before(cutoff) { os.Remove(filepath.Join(c.cacheDir, entry.Name())) } } return nil }