FetchResult now includes: - StatusCode: HTTP status from upstream - FetchDurationMs: time to fetch from upstream - RemoteAddr: upstream server address SourceMetadata now stores: - ContentLength: size from upstream - FetchDurationMs: fetch timing - RemoteAddr: for debugging Image conversion log now includes: - host: source hostname (was missing) - path: source path (renamed from file) - convert_ms: image processing time - quality: requested quality setting - fit: requested fit mode
230 lines
6.8 KiB
Go
230 lines
6.8 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
|
|
}
|
|
|
|
// SourceURL returns the full upstream URL to fetch
|
|
func (r *ImageRequest) SourceURL() string {
|
|
url := "https://" + 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
|
|
}
|
|
|
|
// Processor handles image transformation (resize, format conversion)
|
|
type Processor interface {
|
|
// Process transforms an image according to the request
|
|
Process(ctx context.Context, input io.Reader, req *ImageRequest) (*ProcessResult, error)
|
|
// SupportedInputFormats returns MIME types this processor can read
|
|
SupportedInputFormats() []string
|
|
// SupportedOutputFormats returns formats this processor can write
|
|
SupportedOutputFormats() []ImageFormat
|
|
}
|
|
|
|
// ProcessResult contains the result of image processing
|
|
type ProcessResult struct {
|
|
// Content is the processed image data
|
|
Content io.ReadCloser
|
|
// ContentLength is the size in bytes
|
|
ContentLength int64
|
|
// ContentType is the MIME type of the output
|
|
ContentType string
|
|
// Width is the output image width
|
|
Width int
|
|
// Height is the output image height
|
|
Height int
|
|
// InputWidth is the original image width before processing
|
|
InputWidth int
|
|
// InputHeight is the original image height before processing
|
|
InputHeight int
|
|
// InputFormat is the detected input format (e.g., "jpeg", "png")
|
|
InputFormat 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)
|
|
}
|