All checks were successful
check / check (push) Successful in 2m3s
Replace .golangci.yml with the canonical v2-schema config (default: all minus six disabled linters, lll 88, tests included) and bump every golangci-lint pin to v2.12.2: - Dockerfile: golangci/golangci-lint:v2.12.2-alpine (hash-pinned) - script/bootstrap: GOLANGCI_LINT_VERSION 2.12.2 with new linux-amd64/arm64 release-archive sha256 pins Fix all 747 findings the stricter config surfaces, with no behavior changes: t.Parallel() throughout the test suite, static sentinel errors and errors.Is comparisons, checked error returns, context propagation (contextcheck/noctx), 88-column wrapping, extracted constants and helpers for goconst/dupl/funlen/cyclop, exhaustive switch cases replicating existing defaults, and white-box test files renamed to *_internal_test.go for testpackage. Three nolint:tagliatelle directives preserve the existing snake_case JSON wire and on-disk metadata formats.
184 lines
5.2 KiB
Go
184 lines
5.2 KiB
Go
// Package imgcache provides interfaces and types for the image caching proxy.
|
|
package imgcache
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/url"
|
|
"time"
|
|
)
|
|
|
|
// ErrInvalidFitMode is returned when an invalid fit mode is provided.
|
|
var ErrInvalidFitMode = errors.New("invalid fit mode")
|
|
|
|
// ImageFormat represents supported output image formats.
|
|
type ImageFormat string
|
|
|
|
// Supported image output formats.
|
|
const (
|
|
FormatOriginal ImageFormat = "orig"
|
|
FormatJPEG ImageFormat = "jpeg"
|
|
FormatPNG ImageFormat = "png"
|
|
FormatWebP ImageFormat = "webp"
|
|
FormatAVIF ImageFormat = "avif"
|
|
FormatGIF ImageFormat = "gif"
|
|
)
|
|
|
|
// Size represents requested image dimensions
|
|
type Size struct {
|
|
Width int
|
|
Height int
|
|
}
|
|
|
|
// OriginalSize returns true if this represents "keep original size"
|
|
func (s Size) OriginalSize() bool {
|
|
return s.Width == 0 && s.Height == 0
|
|
}
|
|
|
|
// FitMode represents how to fit image into requested dimensions.
|
|
type FitMode string
|
|
|
|
// Supported image fit modes.
|
|
const (
|
|
FitCover FitMode = "cover"
|
|
FitContain FitMode = "contain"
|
|
FitFill FitMode = "fill"
|
|
FitInside FitMode = "inside"
|
|
FitOutside FitMode = "outside"
|
|
)
|
|
|
|
// ValidateFitMode checks if the given fit mode is valid.
|
|
// Returns ErrInvalidFitMode for unrecognized fit modes.
|
|
func ValidateFitMode(fit FitMode) error {
|
|
switch fit {
|
|
case FitCover, FitContain, FitFill, FitInside, FitOutside, "":
|
|
return nil
|
|
default:
|
|
return ErrInvalidFitMode
|
|
}
|
|
}
|
|
|
|
// ImageRequest represents a request for a processed image
|
|
type ImageRequest struct {
|
|
// SourceHost is the origin host (e.g., "cdn.example.com")
|
|
SourceHost string
|
|
// SourcePath is the path on the origin (e.g., "/photos/cat.jpg")
|
|
SourcePath string
|
|
// SourceQuery is the optional query string for the origin URL
|
|
SourceQuery string
|
|
// Size is the requested output dimensions
|
|
Size Size
|
|
// Format is the requested output format
|
|
Format ImageFormat
|
|
// Quality is the output quality (1-100) for lossy formats
|
|
Quality int
|
|
// FitMode is how to fit the image into requested dimensions
|
|
FitMode FitMode
|
|
// Signature is the HMAC signature for non-allowlisted hosts
|
|
Signature string
|
|
// Expires is the signature expiration timestamp
|
|
Expires time.Time
|
|
// AllowHTTP indicates whether HTTP (non-TLS) is allowed for this request
|
|
AllowHTTP bool
|
|
}
|
|
|
|
// SourceURL returns the full upstream URL to fetch.
|
|
// Uses http:// scheme when AllowHTTP is true, otherwise https://.
|
|
func (r *ImageRequest) SourceURL() string {
|
|
scheme := "https"
|
|
if r.AllowHTTP {
|
|
scheme = "http"
|
|
}
|
|
|
|
url := scheme + "://" + r.SourceHost + r.SourcePath
|
|
if r.SourceQuery != "" {
|
|
url += "?" + r.SourceQuery
|
|
}
|
|
|
|
return url
|
|
}
|
|
|
|
// ImageResponse represents a processed image ready to serve
|
|
type ImageResponse struct {
|
|
// Content is the image data reader
|
|
Content io.ReadCloser
|
|
// ContentLength is the size in bytes (-1 if unknown)
|
|
ContentLength int64
|
|
// ContentType is the MIME type of the response
|
|
ContentType string
|
|
// ETag is the entity tag for caching
|
|
ETag string
|
|
// LastModified is when the content was last modified
|
|
LastModified time.Time
|
|
// CacheStatus indicates HIT, MISS, or STALE
|
|
CacheStatus CacheStatus
|
|
// FetchedBytes is the number of bytes fetched from upstream (0 if cache hit)
|
|
FetchedBytes int64
|
|
}
|
|
|
|
// CacheStatus indicates whether the response was served from cache.
|
|
type CacheStatus string
|
|
|
|
// Cache status values for response headers.
|
|
const (
|
|
CacheHit CacheStatus = "HIT"
|
|
CacheMiss CacheStatus = "MISS"
|
|
CacheStale CacheStatus = "STALE"
|
|
)
|
|
|
|
// ImageCache is the main interface for the image caching proxy
|
|
type ImageCache interface {
|
|
// Get retrieves a processed image, fetching and processing if necessary
|
|
Get(ctx context.Context, req *ImageRequest) (*ImageResponse, error)
|
|
|
|
// Warm pre-fetches and caches an image without returning it
|
|
Warm(ctx context.Context, req *ImageRequest) error
|
|
|
|
// Purge removes a cached image
|
|
Purge(ctx context.Context, req *ImageRequest) error
|
|
|
|
// Stats returns cache statistics
|
|
Stats(ctx context.Context) (*CacheStats, error)
|
|
}
|
|
|
|
// CacheStats contains cache statistics
|
|
type CacheStats struct {
|
|
// TotalItems is the number of cached items
|
|
TotalItems int64
|
|
// TotalSizeBytes is the total size of cached content
|
|
TotalSizeBytes int64
|
|
// HitCount is the number of cache hits
|
|
HitCount int64
|
|
// MissCount is the number of cache misses
|
|
MissCount int64
|
|
// HitRate is HitCount / (HitCount + MissCount)
|
|
HitRate float64
|
|
}
|
|
|
|
// SignatureValidator validates request signatures
|
|
type SignatureValidator interface {
|
|
// Validate checks if the signature is valid for the request
|
|
Validate(req *ImageRequest) error
|
|
// Generate creates a signature for a request
|
|
Generate(req *ImageRequest) string
|
|
}
|
|
|
|
// Allowlist checks if a URL is allowlisted (no signature required)
|
|
type Allowlist interface {
|
|
// IsAllowlisted returns true if the URL doesn't require a signature
|
|
IsAllowlisted(u *url.URL) bool
|
|
}
|
|
|
|
// Storage handles persistent storage of cached content
|
|
type Storage interface {
|
|
// Store saves content and returns its hash
|
|
Store(ctx context.Context, content io.Reader) (hash string, err error)
|
|
// Load retrieves content by hash
|
|
Load(ctx context.Context, hash string) (io.ReadCloser, error)
|
|
// Delete removes content by hash
|
|
Delete(ctx context.Context, hash string) error
|
|
// Exists checks if content exists
|
|
Exists(ctx context.Context, hash string) (bool, error)
|
|
}
|