All checks were successful
check / check (push) Successful in 58s
Move ImageProcessor, Params, New(), DefaultMaxInputBytes, ErrInputDataTooLarge, and related types from internal/imgcache/ into a new standalone package internal/imageprocessor/. The imageprocessor package defines its own Format, FitMode, Size, Request, and Result types, making it fully independent with no imports from imgcache. The imgcache service converts between its own types and imageprocessor types at the boundary. Changes: - New package: internal/imageprocessor/ with imageprocessor.go and tests - Removed: processor.go and processor_test.go from internal/imgcache/ - Removed: Processor interface and ProcessResult from imgcache.go (now unused) - Updated: service.go uses *imageprocessor.ImageProcessor directly - Copied: testdata/red.avif for AVIF decode test Addresses review feedback on PR #37: image processing is a distinct concern from the HTTP service layer and belongs in its own package.
213 lines
6.2 KiB
Go
213 lines
6.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-whitelisted 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
|
|
}
|
|
|
|
// Whitelist checks if a URL is whitelisted (no signature required)
|
|
type Whitelist interface {
|
|
// IsWhitelisted returns true if the URL doesn't require a signature
|
|
IsWhitelisted(u *url.URL) bool
|
|
}
|
|
|
|
// Fetcher fetches images from upstream origins
|
|
type Fetcher interface {
|
|
// Fetch retrieves an image from the origin
|
|
Fetch(ctx context.Context, url string) (*FetchResult, error)
|
|
}
|
|
|
|
// FetchResult contains the result of fetching from upstream
|
|
type FetchResult struct {
|
|
// Content is the raw image data
|
|
Content io.ReadCloser
|
|
// ContentLength is the size in bytes (-1 if unknown)
|
|
ContentLength int64
|
|
// ContentType is the MIME type from upstream
|
|
ContentType string
|
|
// Headers contains all response headers from upstream
|
|
Headers map[string][]string
|
|
// StatusCode is the HTTP status code from upstream
|
|
StatusCode int
|
|
// FetchDurationMs is how long the fetch took in milliseconds
|
|
FetchDurationMs int64
|
|
// RemoteAddr is the IP:port of the upstream server
|
|
RemoteAddr string
|
|
// HTTPVersion is the protocol version (e.g., "1.1", "2.0")
|
|
HTTPVersion string
|
|
// TLSVersion is the TLS protocol version (e.g., "TLS 1.3")
|
|
TLSVersion string
|
|
// TLSCipherSuite is the negotiated cipher suite name
|
|
TLSCipherSuite string
|
|
}
|
|
|
|
// 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)
|
|
}
|