Keep cached sources and variants in one FileStore type (closes #318)
check / check (push) Canceled after 0s

imgcache.FileStore is a directory of files named by a hex key, at
<ab>/<cd>/<key>. It writes a file under a temp name, syncs it and renames
it into place. It opens, reports the size of and deletes the file for a
key. The cache keeps its sources in one FileStore and its variants in
another, so ContentStorage and VariantStorage are gone. FileStore has no
Exists: the reconciliation pass checks a variant's file with Size, and the
tests look for the file on disk. Cache.Variant reads the content type from
the file's first bytes itself. storage.go is now filestore.go, and the
unused testdata/red.avif is gone.

Model: opus-5-5
This commit was merged in pull request #335.
This commit is contained in:
2026-10-26 20:24:33 +01:00
parent 17376d885b
commit ac850823f4
12 changed files with 496 additions and 535 deletions
+55 -11
View File
@@ -19,6 +19,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"sneak.berlin/go/pixa/internal/imagerequest"
"sneak.berlin/go/pixa/internal/magic"
)
// maxPendingAccessTimes is how many access times of variants served from
@@ -57,8 +58,8 @@ type Config struct {
// Cache implements the caching layer for the image proxy.
type Cache struct {
db *sql.DB
srcContent *ContentStorage // source images by source key
variants *VariantStorage // processed variants by cache key
srcContent *FileStore // source images by source key
variants *FileStore // processed variants by cache key
config Config
log *slog.Logger
@@ -135,14 +136,14 @@ func NewCache(
return c, nil
}
srcContent, err := NewContentStorage(
srcContent, err := NewFileStore(
filepath.Join(config.StateDir, "cache", "sources"),
)
if err != nil {
return nil, fmt.Errorf("failed to create source content storage: %w", err)
}
variants, err := NewVariantStorage(
variants, err := NewFileStore(
filepath.Join(config.StateDir, "cache", "variants"),
)
if err != nil {
@@ -169,14 +170,21 @@ func (c *Cache) Variant(
return nil, 0, "", ErrNotFound
}
reader, size, contentType, err := c.variants.LoadWithContentType(cacheKey)
file, size, err := c.variants.Open(string(cacheKey))
if err != nil {
return nil, 0, "", err
}
contentType, err := outputContentType(file)
if err != nil {
_ = file.Close()
return nil, 0, "", err
}
c.touchVariant(cacheKey)
return reader, size, contentType, nil
return file, size, contentType, nil
}
// PutVariant stores a processed variant by its cache key and records
@@ -190,11 +198,18 @@ func (c *Cache) PutVariant(
return nil
}
size, err := c.variants.Store(cacheKey, content)
data, err := io.ReadAll(content)
if err != nil {
return fmt.Errorf("failed to read content: %w", err)
}
err = c.variants.Write(string(cacheKey), data)
if err != nil {
return err
}
size := int64(len(data))
_, err = c.db.ExecContext(ctx, `
INSERT INTO variant_content (cache_key, size_bytes)
VALUES (?, ?)
@@ -238,14 +253,14 @@ func (c *Cache) Source(
return nil, 0, ErrNotFound
}
reader, size, err := c.srcContent.LoadWithSize(key)
file, size, err := c.srcContent.Open(string(key))
if err != nil {
return nil, 0, err
}
c.touchSourceContent(key)
return reader, size, nil
return file, size, nil
}
// PutSource stores fetched source content: it writes the file named by
@@ -270,11 +285,13 @@ func (c *Cache) PutSource(
key := src.Key()
size, err := c.srcContent.Store(key, data)
err = c.srcContent.Write(string(key), data)
if err != nil {
return "", fmt.Errorf("failed to store source content: %w", err)
}
size := int64(len(data))
_, err = c.db.ExecContext(ctx, `
INSERT INTO source_content (source_key, size_bytes)
VALUES (?, ?)
@@ -283,7 +300,7 @@ func (c *Cache) PutSource(
last_accessed_at = CURRENT_TIMESTAMP
`, string(key), size)
if err != nil {
deleteErr := c.srcContent.Delete(key)
deleteErr := c.srcContent.Delete(string(key))
if deleteErr != nil {
c.log.Warn("failed to remove source file whose row was not written",
"source_key", key, "error", deleteErr)
@@ -422,3 +439,30 @@ func (c *Cache) cleanExpiredBatch(ctx context.Context) (int64, error) {
return deleted, nil
}
// errNotAnOutputFormat is returned for a variant file whose first bytes are
// those of a format pixa reads but never writes, such as SVG.
var errNotAnOutputFormat = errors.New("variant file is not in an output format")
// outputContentType reads the first bytes of content, without moving its
// offset, and returns the content type of the output format they show.
func outputContentType(content io.ReaderAt) (string, error) {
firstBytes := make([]byte, magic.MinMagicBytes)
_, err := content.ReadAt(firstBytes, 0)
if err != nil {
return "", fmt.Errorf("failed to read the first bytes: %w", err)
}
detected, err := magic.DetectFormat(firstBytes)
if err != nil {
return "", err
}
_, isOutputFormat := magic.MIMEToImageFormat(string(detected))
if !isOutputFormat {
return "", fmt.Errorf("%w: %s", errNotAnOutputFormat, detected)
}
return string(detected), nil
}