159 lines
3.2 KiB
Go
159 lines
3.2 KiB
Go
|
|
package cache
|
||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/sha256"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"sync"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
indexFileName = "index.json"
|
||
|
|
storageDirName = "sha256"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Cache provides a content-addressable storage for web content.
|
||
|
|
type Cache struct {
|
||
|
|
dir string
|
||
|
|
index map[string]string
|
||
|
|
mutex sync.RWMutex
|
||
|
|
}
|
||
|
|
|
||
|
|
// New creates a new Cache instance.
|
||
|
|
func New(dir string) (*Cache, error) {
|
||
|
|
storageDir := filepath.Join(dir, storageDirName)
|
||
|
|
if err := os.MkdirAll(storageDir, 0755); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to create cache storage directory: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
cache := &Cache{
|
||
|
|
dir: dir,
|
||
|
|
index: make(map[string]string),
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := cache.loadIndex(); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to load cache index: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return cache, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get retrieves content from the cache for a given URL.
|
||
|
|
func (c *Cache) Get(url string) ([]byte, bool, error) {
|
||
|
|
c.mutex.RLock()
|
||
|
|
hash, ok := c.index[url]
|
||
|
|
c.mutex.RUnlock()
|
||
|
|
|
||
|
|
if !ok {
|
||
|
|
return nil, false, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
path := c.getStoragePath(hash)
|
||
|
|
data, err := os.ReadFile(path)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return nil, false, nil
|
||
|
|
}
|
||
|
|
return nil, false, fmt.Errorf("failed to read from cache: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return data, true, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Put adds content to the cache for a given URL.
|
||
|
|
func (c *Cache) Put(url string, data []byte) error {
|
||
|
|
hashBytes := sha256.Sum256(data)
|
||
|
|
hash := fmt.Sprintf("%x", hashBytes)
|
||
|
|
|
||
|
|
path := c.getStoragePath(hash)
|
||
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||
|
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := os.WriteFile(path, data, 0644); err != nil {
|
||
|
|
return fmt.Errorf("failed to write to cache: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
c.mutex.Lock()
|
||
|
|
c.index[url] = hash
|
||
|
|
c.mutex.Unlock()
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Close saves the index file.
|
||
|
|
func (c *Cache) Close() error {
|
||
|
|
return c.saveIndex()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear removes the cache directory.
|
||
|
|
func (c *Cache) Clear() error {
|
||
|
|
return os.RemoveAll(c.dir)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Dir returns the cache directory.
|
||
|
|
func (c *Cache) Dir() string {
|
||
|
|
return c.dir
|
||
|
|
}
|
||
|
|
|
||
|
|
// Size returns the total size of the cache.
|
||
|
|
func (c *Cache) Size() (int64, error) {
|
||
|
|
var size int64
|
||
|
|
err := filepath.Walk(c.dir, func(_ string, info os.FileInfo, err error) error {
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if !info.IsDir() {
|
||
|
|
size += info.Size()
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
return size, err
|
||
|
|
}
|
||
|
|
|
||
|
|
// NumEntries returns the number of entries in the cache.
|
||
|
|
func (c *Cache) NumEntries() int {
|
||
|
|
c.mutex.RLock()
|
||
|
|
defer c.mutex.RUnlock()
|
||
|
|
return len(c.index)
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
func (c *Cache) getStoragePath(hash string) string {
|
||
|
|
return filepath.Join(c.dir, storageDirName, hash[:2], hash)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *Cache) loadIndex() error {
|
||
|
|
indexPath := filepath.Join(c.dir, indexFileName)
|
||
|
|
file, err := os.Open(indexPath)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer file.Close()
|
||
|
|
|
||
|
|
decoder := json.NewDecoder(file)
|
||
|
|
c.mutex.Lock()
|
||
|
|
defer c.mutex.Unlock()
|
||
|
|
return decoder.Decode(&c.index)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *Cache) saveIndex() error {
|
||
|
|
indexPath := filepath.Join(c.dir, indexFileName)
|
||
|
|
file, err := os.Create(indexPath)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer file.Close()
|
||
|
|
|
||
|
|
encoder := json.NewEncoder(file)
|
||
|
|
encoder.SetIndent("", " ")
|
||
|
|
c.mutex.Lock()
|
||
|
|
defer c.mutex.Unlock()
|
||
|
|
return encoder.Encode(c.index)
|
||
|
|
}
|