Files
git-velocity/internal/github/cache/cache.go
T
2025-12-10 21:09:25 +00:00

218 lines
4.3 KiB
Go

package cache
import (
"crypto/sha256"
"encoding/gob"
"encoding/hex"
"os"
"path/filepath"
"sync"
"time"
)
// Cache defines the interface for caching
type Cache interface {
Get(key string) (interface{}, bool)
Set(key string, value interface{})
Delete(key string)
Clear() error
}
// FileCache implements file-based caching
type FileCache struct {
directory string
ttl time.Duration
mu sync.RWMutex
}
// cacheEntry wraps a cached value with expiration
type cacheEntry struct {
Value interface{}
ExpiresAt time.Time
}
// NewFileCache creates a new file-based cache
func NewFileCache(directory string, ttl time.Duration) (*FileCache, error) {
// Create directory if it doesn't exist
if err := os.MkdirAll(directory, 0750); err != nil {
return nil, err
}
return &FileCache{
directory: directory,
ttl: ttl,
}, nil
}
// Get retrieves a value from the cache
func (c *FileCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
path := c.keyToPath(key)
file, err := os.Open(path) // #nosec G304 -- path is internally generated hash
if err != nil {
return nil, false
}
defer file.Close()
var entry cacheEntry
decoder := gob.NewDecoder(file)
if err := decoder.Decode(&entry); err != nil {
return nil, false
}
// Check expiration
if time.Now().After(entry.ExpiresAt) {
_ = os.Remove(path)
return nil, false
}
return entry.Value, true
}
// Set stores a value in the cache
func (c *FileCache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
entry := cacheEntry{
Value: value,
ExpiresAt: time.Now().Add(c.ttl),
}
path := c.keyToPath(key)
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil {
return
}
file, err := os.Create(path) // #nosec G304 -- path is internally generated hash
if err != nil {
return
}
defer file.Close()
encoder := gob.NewEncoder(file)
_ = encoder.Encode(entry)
}
// Delete removes a value from the cache
func (c *FileCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
path := c.keyToPath(key)
_ = os.Remove(path)
}
// Clear removes all cached values
func (c *FileCache) Clear() error {
c.mu.Lock()
defer c.mu.Unlock()
return os.RemoveAll(c.directory)
}
// keyToPath converts a cache key to a file path
func (c *FileCache) keyToPath(key string) string {
hash := sha256.Sum256([]byte(key))
filename := hex.EncodeToString(hash[:8]) + ".gob"
return filepath.Join(c.directory, filename)
}
// NoopCache is a cache that doesn't cache anything
type NoopCache struct{}
// NewNoopCache creates a new no-op cache
func NewNoopCache() *NoopCache {
return &NoopCache{}
}
// Get always returns false
func (c *NoopCache) Get(key string) (interface{}, bool) {
return nil, false
}
// Set does nothing
func (c *NoopCache) Set(key string, value interface{}) {}
// Delete does nothing
func (c *NoopCache) Delete(key string) {}
// Clear does nothing
func (c *NoopCache) Clear() error {
return nil
}
// MemoryCache implements in-memory caching (useful for testing)
type MemoryCache struct {
data map[string]cacheEntry
ttl time.Duration
mu sync.RWMutex
}
// NewMemoryCache creates a new in-memory cache
func NewMemoryCache(ttl time.Duration) *MemoryCache {
return &MemoryCache{
data: make(map[string]cacheEntry),
ttl: ttl,
}
}
// Get retrieves a value from the cache
func (c *MemoryCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.data[key]
if !ok {
return nil, false
}
// Check expiration
if time.Now().After(entry.ExpiresAt) {
delete(c.data, key)
return nil, false
}
return entry.Value, true
}
// Set stores a value in the cache
func (c *MemoryCache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = cacheEntry{
Value: value,
ExpiresAt: time.Now().Add(c.ttl),
}
}
// Delete removes a value from the cache
func (c *MemoryCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.data, key)
}
// Clear removes all cached values
func (c *MemoryCache) Clear() error {
c.mu.Lock()
defer c.mu.Unlock()
c.data = make(map[string]cacheEntry)
return nil
}
// Register types for gob encoding
func init() {
// Register common types that might be cached
gob.Register([]interface{}{})
gob.Register(map[string]interface{}{})
}