Merge branch 'main' into golangci-v2.12.2

Absorbs the cache size management and LRU eviction work (#55). All four
textual conflicts resolved in favor of main's implementation, with this
branch's mechanical lint conformance re-applied on top:

- internal/config/config.go: took main's cache_max_bytes wiring
  (CacheMaxBytes, cacheMaxBytesExplicit) verbatim and expressed the key
  through this branch's constant convention as keyCacheMaxBytes.
- internal/imgcache/cache.go: took main's disabled-cache guards, LRU
  touch on lookup, and variant_content size accounting verbatim;
  re-applied this branch's signature wrapping for lll.
- internal/imgcache/storage.go: took main's new writeIfAbsent helper and
  its temp-file cleanup defer verbatim. The auto-merge had silently
  dropped that defer in favor of this branch's inline cleanup form;
  restored so the cleanup semantics that arrived from main are intact.
- TODO.md: kept both sides' Completed Steps entries.

.golangci.yml resolves to this branch's canonical version
(sha256 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb).

make build and make test are green; #55's code is not yet conformant
with the canonical lint config, which the following commits address.
This commit is contained in:
2026-08-09 13:16:14 +00:00
15 changed files with 2610 additions and 57 deletions

View File

@@ -66,55 +66,76 @@ func (s *ContentStorage) Store(r io.Reader) (ContentHash, int64, error) {
hash := ContentHash(hex.EncodeToString(h[:]))
size := int64(len(data))
if err := s.writeIfAbsent(hash, data); err != nil {
return "", 0, err
}
return hash, size, nil
}
// StoreHashed writes pre-hashed content to storage at the path derived
// from hash, without recomputing it. Callers that already know the
// hash before writing (e.g. because they must hold a hash-keyed lock
// across the whole store operation) use this instead of Store. Like
// Store, it is idempotent: content already on disk at that path is
// left untouched.
func (s *ContentStorage) StoreHashed(hash ContentHash, data []byte) (size int64, err error) {
if err := s.writeIfAbsent(hash, data); err != nil {
return 0, err
}
return int64(len(data)), nil
}
// writeIfAbsent writes data to the path derived from hash, unless
// content already exists there, via a temp-file-plus-rename so
// concurrent readers never observe a partial file.
func (s *ContentStorage) writeIfAbsent(hash ContentHash, data []byte) (err error) {
// Build path: <basedir>/<ab>/<cd>/<hash>
path := s.hashToPath(hash)
// Check if already exists
_, err = os.Stat(path)
if err == nil {
return hash, size, nil
if _, statErr := os.Stat(path); statErr == nil {
return nil
}
// Create directory structure
dir := filepath.Dir(path)
err = os.MkdirAll(dir, StorageDirPerm)
if err != nil {
return "", 0, fmt.Errorf("failed to create directory: %w", err)
if err := os.MkdirAll(dir, StorageDirPerm); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// Write to temp file first, then rename for atomicity
tmpFile, err := os.CreateTemp(dir, ".tmp-*")
if err != nil {
return "", 0, fmt.Errorf("failed to create temp file: %w", err)
return fmt.Errorf("failed to create temp file: %w", err)
}
tmpPath := tmpFile.Name()
_, err = tmpFile.Write(data)
if err != nil {
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
defer func() {
if err != nil {
_ = os.Remove(tmpPath)
}
}()
return "", 0, fmt.Errorf("failed to write content: %w", err)
if _, err := tmpFile.Write(data); err != nil {
_ = tmpFile.Close()
return fmt.Errorf("failed to write content: %w", err)
}
err = tmpFile.Close()
if err != nil {
_ = os.Remove(tmpPath)
return "", 0, fmt.Errorf("failed to close temp file: %w", err)
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("failed to close temp file: %w", err)
}
// Atomic rename
err = os.Rename(filepath.Clean(tmpPath), filepath.Clean(path))
if err != nil {
_ = os.Remove(tmpPath)
return "", 0, fmt.Errorf("failed to rename temp file: %w", err)
//nolint:gosec // G703: paths from internal SHA256 hashes
if err := os.Rename(filepath.Clean(tmpPath), filepath.Clean(path)); err != nil {
return fmt.Errorf("failed to rename temp file: %w", err)
}
return hash, size, nil
return nil
}
// Load returns a reader for the content with the given hash.
@@ -522,6 +543,24 @@ func (s *VariantStorage) Delete(key VariantKey) error {
return nil
}
// DeleteWithMeta removes the content at the given key together with
// its .meta sidecar file. A missing file is not an error.
func (s *VariantStorage) DeleteWithMeta(key VariantKey) error {
if err := s.Delete(key); err != nil {
return err
}
metaPath := s.keyToPath(key) + ".meta"
//nolint:gosec // G703: path derived from cache key
err := os.Remove(metaPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete variant metadata: %w", err)
}
return nil
}
// keyToPath converts a key to a file path: <basedir>/<ab>/<cd>/<key>
func (s *VariantStorage) keyToPath(key VariantKey) string {
k := string(key)