283 lines
6.4 KiB
Go
283 lines
6.4 KiB
Go
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
|
|
}
|