chore: update golangci-lint to v2.12.2 with canonical config (#54)
All checks were successful
check / check (push) Successful in 4s
All checks were successful
check / check (push) Successful in 4s
Canonical v2-schema `.golangci.yml`, golangci-lint pins bumped to v2.12.2 in `Dockerfile` and `script/bootstrap`, and the tree brought to `0 issues.` under it. Three behaviour deltas: `Cache.StoreVariant` takes a context (cancelled requests skip the accounting row, recovered by reconciliation); `MetadataStorage.Store` no longer leaks `.tmp-*.json` on Write/Close/Rename failure (dead-defer bug fix); the `signing_key` too-short error text gained a `value too short:` prefix. Eviction-loop context cancellation deferred to #102.
This commit was merged in pull request #54.
This commit is contained in:
@@ -44,7 +44,8 @@ type ContentStorage struct {
|
||||
|
||||
// NewContentStorage creates a new content storage at the given base directory.
|
||||
func NewContentStorage(baseDir string) (*ContentStorage, error) {
|
||||
if err := os.MkdirAll(baseDir, StorageDirPerm); err != nil {
|
||||
err := os.MkdirAll(baseDir, StorageDirPerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create storage directory: %w", err)
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ func NewContentStorage(baseDir string) (*ContentStorage, error) {
|
||||
|
||||
// Store writes content to storage and returns its SHA256 hash.
|
||||
// The content is read fully into memory to compute the hash before writing.
|
||||
func (s *ContentStorage) Store(r io.Reader) (hash ContentHash, size int64, err error) {
|
||||
func (s *ContentStorage) Store(r io.Reader) (ContentHash, int64, error) {
|
||||
// Read all content to compute hash
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
@@ -62,10 +63,11 @@ func (s *ContentStorage) Store(r io.Reader) (hash ContentHash, size int64, err e
|
||||
|
||||
// Compute hash
|
||||
h := sha256.Sum256(data)
|
||||
hash = ContentHash(hex.EncodeToString(h[:]))
|
||||
size = int64(len(data))
|
||||
hash := ContentHash(hex.EncodeToString(h[:]))
|
||||
size := int64(len(data))
|
||||
|
||||
if err := s.writeIfAbsent(hash, data); err != nil {
|
||||
err = s.writeIfAbsent(hash, data)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
@@ -78,64 +80,17 @@ func (s *ContentStorage) Store(r io.Reader) (hash ContentHash, size int64, err e
|
||||
// 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 {
|
||||
func (s *ContentStorage) StoreHashed(
|
||||
hash ContentHash, data []byte,
|
||||
) (int64, error) {
|
||||
err := s.writeIfAbsent(hash, data)
|
||||
if 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
|
||||
if _, statErr := os.Stat(path); statErr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create directory structure
|
||||
dir := filepath.Dir(path)
|
||||
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 fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := tmpFile.Write(data); err != nil {
|
||||
_ = tmpFile.Close()
|
||||
|
||||
return fmt.Errorf("failed to write content: %w", err)
|
||||
}
|
||||
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close temp file: %w", err)
|
||||
}
|
||||
|
||||
// Atomic rename
|
||||
//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 nil
|
||||
}
|
||||
|
||||
// Load returns a reader for the content with the given hash.
|
||||
func (s *ContentStorage) Load(hash ContentHash) (io.ReadCloser, error) {
|
||||
path := s.hashToPath(hash)
|
||||
@@ -195,6 +150,67 @@ func (s *ContentStorage) Exists(hash ContentHash) bool {
|
||||
return err == 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) error {
|
||||
// Build path: <basedir>/<ab>/<cd>/<hash>
|
||||
path := s.hashToPath(hash)
|
||||
|
||||
// Check if already exists
|
||||
_, statErr := os.Stat(path)
|
||||
if statErr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create directory structure
|
||||
dir := filepath.Dir(path)
|
||||
|
||||
err := os.MkdirAll(dir, StorageDirPerm)
|
||||
if 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 fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
tmpPath := tmpFile.Name()
|
||||
|
||||
// Each failure path below unlinks the temp file explicitly. This
|
||||
// replaces a deferred cleanup that read a named result, which the
|
||||
// canonical config does not permit; the set of paths that remove
|
||||
// tmpPath, and the order relative to Close, is unchanged. This
|
||||
// mirrors how MetadataStorage.Store and VariantStorage.Store below
|
||||
// already express the same cleanup.
|
||||
_, err = tmpFile.Write(data)
|
||||
if err != nil {
|
||||
_ = tmpFile.Close()
|
||||
_ = os.Remove(tmpPath)
|
||||
|
||||
return fmt.Errorf("failed to write content: %w", err)
|
||||
}
|
||||
|
||||
err = tmpFile.Close()
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
|
||||
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 fmt.Errorf("failed to rename temp file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hashToPath converts a hash to a file path: <basedir>/<ab>/<cd>/<hash>
|
||||
func (s *ContentStorage) hashToPath(hash ContentHash) string {
|
||||
h := string(hash)
|
||||
@@ -213,7 +229,8 @@ type MetadataStorage struct {
|
||||
|
||||
// NewMetadataStorage creates a new metadata storage at the given base directory.
|
||||
func NewMetadataStorage(baseDir string) (*MetadataStorage, error) {
|
||||
if err := os.MkdirAll(baseDir, StorageDirPerm); err != nil {
|
||||
err := os.MkdirAll(baseDir, StorageDirPerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create metadata directory: %w", err)
|
||||
}
|
||||
|
||||
@@ -221,6 +238,8 @@ func NewMetadataStorage(baseDir string) (*MetadataStorage, error) {
|
||||
}
|
||||
|
||||
// SourceMetadata represents cached metadata about a source URL.
|
||||
//
|
||||
//nolint:tagliatelle // stored metadata format uses snake_case
|
||||
type SourceMetadata struct {
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
@@ -239,12 +258,16 @@ type SourceMetadata struct {
|
||||
}
|
||||
|
||||
// Store writes metadata to storage.
|
||||
func (s *MetadataStorage) Store(host string, pathHash PathHash, meta *SourceMetadata) error {
|
||||
func (s *MetadataStorage) Store(
|
||||
host string, pathHash PathHash, meta *SourceMetadata,
|
||||
) error {
|
||||
path := s.metaPath(host, pathHash)
|
||||
|
||||
// Create directory structure
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, StorageDirPerm); err != nil {
|
||||
|
||||
err := os.MkdirAll(dir, StorageDirPerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
@@ -259,27 +282,29 @@ func (s *MetadataStorage) Store(host string, pathHash PathHash, meta *SourceMeta
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
tmpPath := tmpFile.Name()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := tmpFile.Write(data); err != nil {
|
||||
_, err = tmpFile.Write(data)
|
||||
if err != nil {
|
||||
_ = tmpFile.Close()
|
||||
_ = os.Remove(tmpPath)
|
||||
|
||||
return fmt.Errorf("failed to write metadata: %w", err)
|
||||
}
|
||||
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
err = tmpFile.Close()
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
|
||||
return fmt.Errorf("failed to close temp file: %w", err)
|
||||
}
|
||||
|
||||
// Atomic rename
|
||||
//nolint:gosec // G703: paths from internal SHA256 hashes
|
||||
if err := os.Rename(filepath.Clean(tmpPath), filepath.Clean(path)); err != nil {
|
||||
err = os.Rename(filepath.Clean(tmpPath), filepath.Clean(path))
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
|
||||
return fmt.Errorf("failed to rename temp file: %w", err)
|
||||
}
|
||||
|
||||
@@ -287,7 +312,9 @@ func (s *MetadataStorage) Store(host string, pathHash PathHash, meta *SourceMeta
|
||||
}
|
||||
|
||||
// Load reads metadata from storage.
|
||||
func (s *MetadataStorage) Load(host string, pathHash PathHash) (*SourceMetadata, error) {
|
||||
func (s *MetadataStorage) Load(
|
||||
host string, pathHash PathHash,
|
||||
) (*SourceMetadata, error) {
|
||||
path := s.metaPath(host, pathHash)
|
||||
|
||||
data, err := os.ReadFile(path) //nolint:gosec // path derived from host+hash
|
||||
@@ -300,7 +327,9 @@ func (s *MetadataStorage) Load(host string, pathHash PathHash) (*SourceMetadata,
|
||||
}
|
||||
|
||||
var meta SourceMetadata
|
||||
if err := json.Unmarshal(data, &meta); err != nil {
|
||||
|
||||
err = json.Unmarshal(data, &meta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal metadata: %w", err)
|
||||
}
|
||||
|
||||
@@ -366,6 +395,8 @@ type VariantStorage struct {
|
||||
}
|
||||
|
||||
// VariantMeta contains metadata about a cached variant.
|
||||
//
|
||||
//nolint:tagliatelle // stored metadata format uses snake_case
|
||||
type VariantMeta struct {
|
||||
ContentType string `json:"content_type"`
|
||||
Size int64 `json:"size"`
|
||||
@@ -374,7 +405,8 @@ type VariantMeta struct {
|
||||
|
||||
// NewVariantStorage creates a new variant storage at the given base directory.
|
||||
func NewVariantStorage(baseDir string) (*VariantStorage, error) {
|
||||
if err := os.MkdirAll(baseDir, StorageDirPerm); err != nil {
|
||||
err := os.MkdirAll(baseDir, StorageDirPerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create variant storage directory: %w", err)
|
||||
}
|
||||
|
||||
@@ -382,19 +414,23 @@ func NewVariantStorage(baseDir string) (*VariantStorage, error) {
|
||||
}
|
||||
|
||||
// Store writes content and metadata to storage at the given key.
|
||||
func (s *VariantStorage) Store(key VariantKey, r io.Reader, contentType string) (size int64, err error) {
|
||||
func (s *VariantStorage) Store(
|
||||
key VariantKey, r io.Reader, contentType string,
|
||||
) (int64, error) {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to read content: %w", err)
|
||||
}
|
||||
|
||||
size = int64(len(data))
|
||||
size := int64(len(data))
|
||||
path := s.keyToPath(key)
|
||||
metaPath := path + ".meta"
|
||||
|
||||
// Create directory structure
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, StorageDirPerm); err != nil {
|
||||
|
||||
err = os.MkdirAll(dir, StorageDirPerm)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
@@ -403,27 +439,29 @@ func (s *VariantStorage) Store(key VariantKey, r io.Reader, contentType string)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
tmpPath := tmpFile.Name()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := tmpFile.Write(data); err != nil {
|
||||
_, err = tmpFile.Write(data)
|
||||
if err != nil {
|
||||
_ = tmpFile.Close()
|
||||
_ = os.Remove(tmpPath)
|
||||
|
||||
return 0, fmt.Errorf("failed to write content: %w", err)
|
||||
}
|
||||
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
err = tmpFile.Close()
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
|
||||
return 0, fmt.Errorf("failed to close temp file: %w", err)
|
||||
}
|
||||
|
||||
// Atomic rename content
|
||||
//nolint:gosec // G703: paths from internal SHA256 hashes
|
||||
if err := os.Rename(filepath.Clean(tmpPath), filepath.Clean(path)); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -439,10 +477,8 @@ func (s *VariantStorage) Store(key VariantKey, r io.Reader, contentType string)
|
||||
return 0, fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(metaPath, metaData, StorageFilePerm); err != nil {
|
||||
// Non-fatal, content is stored
|
||||
_ = err
|
||||
}
|
||||
// Metadata write failure is non-fatal; content is already stored.
|
||||
_ = os.WriteFile(metaPath, metaData, StorageFilePerm)
|
||||
|
||||
return size, nil
|
||||
}
|
||||
@@ -463,8 +499,11 @@ func (s *VariantStorage) Load(key VariantKey) (io.ReadCloser, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// LoadWithMeta returns a reader, size, and content type for the content at the given key.
|
||||
func (s *VariantStorage) LoadWithMeta(key VariantKey) (io.ReadCloser, int64, string, error) {
|
||||
// LoadWithMeta returns a reader, size, and content type for the content at
|
||||
// the given key.
|
||||
func (s *VariantStorage) LoadWithMeta(
|
||||
key VariantKey,
|
||||
) (io.ReadCloser, int64, string, error) {
|
||||
path := s.keyToPath(key)
|
||||
metaPath := path + ".meta"
|
||||
|
||||
@@ -521,14 +560,14 @@ func (s *VariantStorage) Delete(key VariantKey) error {
|
||||
// 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 {
|
||||
err := s.Delete(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metaPath := s.keyToPath(key) + ".meta"
|
||||
|
||||
//nolint:gosec // G703: path derived from cache key
|
||||
err := os.Remove(metaPath)
|
||||
err = os.Remove(metaPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete variant metadata: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user