check / check (push) Canceled after 0s
The image request now holds only its source and its transform, exactly what the variant key covers. The /v1/image/ handler checks the allowlist and verifies the signature itself with Signer.Verify(req, sig, expires). Both image handlers keep the URL's expiry as a local value for Cache-Control. The image service holds no signer, allowlist or AllowHTTP, and ValidateRequest is gone. The fetcher takes the source and builds its URL with Source.URL from its own AllowHTTP, so no URL string is built and parsed again. chooseAutoFormat returns the format it chooses instead of writing into the request. The signed URL format and every route's answers are unchanged. Model: opus-5-5
92 lines
2.2 KiB
Go
92 lines
2.2 KiB
Go
package httpfetcher
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
)
|
|
|
|
// MockFetcher implements Fetcher using an embedded filesystem.
|
|
// Files are organized as: hostname/path/to/file.ext
|
|
// The source with host example.com and path /images/photo.jpg maps to
|
|
// example.com/images/photo.jpg; the source query is ignored.
|
|
type MockFetcher struct {
|
|
fs fs.FS
|
|
}
|
|
|
|
// NewMock creates a new mock fetcher backed by the given filesystem.
|
|
func NewMock(fsys fs.FS) *MockFetcher {
|
|
return &MockFetcher{fs: fsys}
|
|
}
|
|
|
|
// Fetch retrieves the source's content from the mock filesystem.
|
|
func (m *MockFetcher) Fetch(
|
|
ctx context.Context, source imagerequest.Source,
|
|
) (*FetchResult, error) {
|
|
// Check context cancellation
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
path := source.Host + source.Path
|
|
|
|
// Open the file
|
|
f, err := m.fs.Open(path)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, fmt.Errorf("%w: status 404", ErrUpstreamError)
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to open mock file: %w", err)
|
|
}
|
|
|
|
// Get file info for content length
|
|
stat, err := f.Stat()
|
|
if err != nil {
|
|
_ = f.Close()
|
|
|
|
return nil, fmt.Errorf("failed to stat mock file: %w", err)
|
|
}
|
|
|
|
// Detect content type from extension
|
|
contentType := detectContentTypeFromPath(path)
|
|
|
|
return &FetchResult{
|
|
Content: f,
|
|
ContentLength: stat.Size(),
|
|
ContentType: contentType,
|
|
Headers: make(http.Header),
|
|
}, nil
|
|
}
|
|
|
|
// detectContentTypeFromPath returns the MIME type based on file extension.
|
|
func detectContentTypeFromPath(path string) string {
|
|
path = strings.ToLower(path)
|
|
|
|
switch {
|
|
case strings.HasSuffix(path, ".jpg"), strings.HasSuffix(path, ".jpeg"):
|
|
return contentTypeJPEG
|
|
case strings.HasSuffix(path, ".png"):
|
|
return contentTypePNG
|
|
case strings.HasSuffix(path, ".gif"):
|
|
return contentTypeGIF
|
|
case strings.HasSuffix(path, ".webp"):
|
|
return contentTypeWebP
|
|
case strings.HasSuffix(path, ".avif"):
|
|
return contentTypeAVIF
|
|
case strings.HasSuffix(path, ".jxl"):
|
|
return contentTypeJXL
|
|
case strings.HasSuffix(path, ".svg"):
|
|
return contentTypeSVG
|
|
default:
|
|
return contentTypeOctetStream
|
|
}
|
|
}
|