Commit Graph

131 Commits

Author SHA1 Message Date
0000188265 chore: add hooks target and 30s test timeout to Makefile
Add missing fmt-check to .PHONY, add hooks target for pre-commit
hook installation, and add 30-second timeout to test target per
repo policy.
2026-02-25 18:18:00 +07:00
f702e64139 Merge pull request 'chore: remove local dev config files' (#12) from chore/remove-stale-files into main
Reviewed-on: #12
2026-02-20 12:05:28 +01:00
user
c4368b1541 chore: remove local dev config files
Remove config.yaml and config.dev.yml (local development configs with
hardcoded keys that shouldn't be committed). config.example.yml remains
as the canonical example config. Added removed files to .gitignore.
2026-02-20 02:59:30 -08:00
2fb36c5ccb Merge pull request 'fix: propagate AllowHTTP to SourceURL() scheme selection (closes #1)' (#6) from fix/issue-1 into main
Reviewed-on: #6
2026-02-09 01:41:31 +01:00
clawbot
40c4b53b01 fix: propagate AllowHTTP to SourceURL() scheme selection
SourceURL() previously hardcoded https:// regardless of the AllowHTTP
config setting. This made testing with HTTP-only test servers impossible.

Add AllowHTTP field to ImageRequest and use it to determine the URL
scheme. The Service propagates the config setting to each request.

Fixes #1
2026-02-08 16:34:42 -08:00
b800ef86d8 Merge pull request 'fix: check negative cache in Service.Get() before fetching upstream (closes #3)' (#8) from fix/issue-3 into main
Reviewed-on: #8
2026-02-09 01:32:26 +01:00
3d857da237 Merge branch 'main' into fix/issue-3 2026-02-09 01:32:17 +01:00
46a92c3514 Merge pull request 'fix: correct Stats() column scanning and HitRate computation (closes #4)' (#9) from fix/issue-4 into main
Reviewed-on: #9
2026-02-09 01:31:18 +01:00
39354e41c3 Merge branch 'main' into fix/issue-4 2026-02-09 01:31:03 +01:00
d1eff1315b Merge pull request 'fix: encode source query in GenerateSignedURL to avoid malformed URLs (closes #2)' (#7) from fix/issue-2 into main
Reviewed-on: #7
2026-02-09 01:30:51 +01:00
814c735295 Merge branch 'main' into fix/issue-2 2026-02-09 01:29:07 +01:00
7ce6f81d8d Merge pull request 'fix: guard against division by zero when fetchBytes is 0 (closes #5)' (#10) from fix/issue-5 into main
Reviewed-on: #10
Reviewed-by: Jeffrey Paul <sneak@noreply.example.org>
2026-02-09 01:05:24 +01:00
clawbot
e651e672aa fix: check negative cache in Service.Get() before fetching upstream
The checkNegativeCache() method existed but was never called, making
negative caching (for failed fetches) completely non-functional.
Failed URLs were being re-fetched on every request.

Add negative cache check at the start of Service.Get() to short-circuit
requests for recently-failed URLs.

Fixes #3
2026-02-08 16:02:33 -08:00
clawbot
79ceed2ee4 fix: guard against division by zero when fetchBytes is 0
processAndStore() computed sizePercent as outputSize/fetchBytes*100
without checking for zero, producing Inf/NaN in logs and metrics.

Also treat empty cached source data the same as missing (re-fetch
from upstream) since zero-byte images can't be processed.

Fixes #5
2026-02-08 15:59:51 -08:00
clawbot
e3b346e881 fix: correct Stats() to scan only hit/miss counts, compute HitRate properly
Stats() was scanning 5 SQL columns (hit_count, miss_count,
upstream_fetch_count, upstream_fetch_bytes, transform_count) into
mismatched struct fields, causing HitRate to contain the integer
transform_count instead of a 0.0-1.0 ratio.

Simplify the query to only fetch hit_count and miss_count, then
compute TotalItems, TotalSizeBytes, and HitRate correctly.

Fixes #4
2026-02-08 15:59:27 -08:00
clawbot
0ff3071337 fix: encode source query in GenerateSignedURL to avoid malformed URLs
When a source URL has query parameters, GenerateSignedURL() was
embedding a bare '?' in the path, causing everything after it to be
parsed as the HTTP query string instead of as path segments. This
made the size/format segment unreachable by the URL parser.

Percent-encode the query string in the path segment so it remains
part of the path and can be correctly extracted by ParseImagePath.

Fixes #2
2026-02-08 15:58:32 -08:00
be293906bc Add type-safe hash types for cache storage
Define ContentHash, VariantKey, and PathHash types to replace
raw strings, providing compile-time type safety for storage
operations. Update storage layer to use typed parameters,
refactor cache to use variant storage keyed by VariantKey,
and implement source content reuse on cache misses.
2026-01-08 16:55:20 -08:00
555f150179 Add additional hosts to dev whitelist 2026-01-08 16:14:05 -08:00
87a8393537 Change encrypted URL format to /v1/e/{token}/img.{ext}
Add trailing filename to encrypted URLs for better browser compatibility.
The filename is ignored by the server but helps browsers identify content type.
2026-01-08 16:14:01 -08:00
6ab0d4a5b9 Simplify log source to file.go:line format
Replace verbose source object with simple "file.go:line" string
for cleaner log output.
2026-01-08 16:13:56 -08:00
982accd549 Suppress verbose vips logging output
Initialize libvips with LogLevelError to prevent info-level messages
from polluting the JSON log stream.
2026-01-08 16:13:52 -08:00
3849128c45 Remove runtime nil checks for always-initialized components
Since signing_key is now required at config load time, sessMgr, encGen,
and signer are always initialized. Remove unnecessary nil checks that
were runtime failure paths that can no longer be reached.

- handlers.go: Remove conditional init, always create sessMgr/encGen
- auth.go: Remove nil checks for sessMgr
- imageenc.go: Remove nil check for encGen
- service.go: Require signing_key in NewService, remove signer nil checks
- Update tests to provide signing_key
2026-01-08 15:58:44 -08:00
02dedd433b Require signing_key at startup, add default config
- Add config validation: signing_key required, minimum 32 characters
- Server now fails to start without valid signing_key (no more runtime errors)
- Add config.example.yml with default whitelist hosts
- Copy config to /etc/pixa/config.yml in Docker image
- Update entrypoint to use --config /etc/pixa/config.yml
- Add config.dev.yml for local Docker development
- Mount dev config in make devserver
2026-01-08 15:48:37 -08:00
d2e2e319be Create /var/lib/pixa directory in Docker image for database/cache
- Change default StateDir from ./data to /var/lib/pixa (proper Unix convention)
- Create directory owned by pixad user in Dockerfile
- Set WORKDIR to /var/lib/pixa
2026-01-08 15:37:25 -08:00
fc49e69d8b Add make devserver target for local Docker development 2026-01-08 15:22:25 -08:00
78f844fca5 Switch to govips for native CGO image processing
- Replace gen2brain/avif, gen2brain/webp, disintegration/imaging with govips
- govips uses libvips via CGO for fast native image processing
- Add libheif-dev to Dockerfile for AVIF support
- Add docker-test Makefile target for running tests in Docker
- Update processor.go to use vips API for decode, resize, encode
- Add TestMain to initialize/shutdown vips in tests
- Remove WASM-based libraries (gen2brain) in favor of native codecs

Performance improvement: AVIF encoding now uses native libheif instead of
WASM, significantly reducing encoding time for large images.
2026-01-08 15:16:34 -08:00
4b2d85010e Add two-stage Dockerfile with CGO support
- Build stage: golang:1.24-alpine with vips-dev for CGO image libs
- Runtime stage: alpine:3.21 with vips runtime only
- Pass VERSION build arg for ldflags embedding
- Add 'make docker' target to build image with git version
2026-01-08 15:05:49 -08:00
467237c849 Add AVIF option to URL generator 2026-01-08 13:12:51 -08:00
ca4446e10a Implement AVIF encoding support
- Add AVIF case to encode() using gen2brain/avif
- Add AVIF to SupportedOutputFormats()
- Use default encoder speed of 6 for balance of speed/quality
2026-01-08 13:12:34 -08:00
32f9166ece Add failing test for AVIF encoding
Test verifies that images can be encoded to AVIF format.
Currently fails because AVIF encoding is not implemented.
Removes the rejection test for AVIF output format.
2026-01-08 13:11:12 -08:00
1bdf0a9424 Implement AVIF decoding support
- Add github.com/gen2brain/avif dependency (CGO-free, WASM-based)
- Update decode() to try AVIF after WebP
- Add AVIF to SupportedInputFormats()
2026-01-08 13:10:34 -08:00
615586fcea Add failing test for AVIF decoding
Test verifies that AVIF images can be decoded and processed.
Currently fails because AVIF decoding is not implemented.
2026-01-08 13:09:40 -08:00
9bfae69ccf Fix logging: add response_bytes to middleware, cache_key to handler
- Middleware now tracks and logs bytes written via response_bytes
- Handler logs cache_key for cache hit debugging
- Changed "served encrypted image" to "image served" (only URL is encrypted)
2026-01-08 13:05:10 -08:00
e5135b3697 Add 1-minute expiration option and quality presets to URL generator
Quality is now a dropdown with named presets:
- Potato (25), Low (50), Medium (70), High (85), Ultra (100)

Added 1-minute TTL option for testing short-lived URLs.
2026-01-08 12:52:14 -08:00
77c6744383 Add upstream connection info and download metrics to logging
- Capture TLS version, cipher suite, HTTP version, and remote addr
- Add download bitrate using go-humanize SI formatting
- Use consistent WxH format for dimensions (not struct notation)
- Rename input/output to src/dst for consistency
- Add separate "upstream fetched" log with connection details
2026-01-08 12:47:31 -08:00
7d0ac0a139 Remove Buildarch from ldflags, use runtime.GOARCH instead
The architecture is available at runtime via stdlib, no need to bake
it in at build time.
2026-01-08 12:38:24 -08:00
15d9439e3d Add fetch/conversion metrics and improve logging
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
2026-01-08 12:34:26 -08:00
4426387d1c Fix hot cache to include ContentType and SizeBytes
Hot cache entries now store all data needed to serve a cache hit
without any database access:
- OutputHash (for file lookup)
- ContentType (for Content-Type header)
- SizeBytes (for Content-Length header)

Previously hot cache only stored OutputHash, causing empty
Content-Type headers on cached WebP responses.
2026-01-08 12:28:17 -08:00
51a1ae4a13 Add failing test for hot cache ContentType
Hot cache lookups must return ContentType to serve correct
Content-Type headers. Currently returns empty string.
2026-01-08 12:25:01 -08:00
10b5cc7063 Fix proportional scaling when single dimension is 0
When only width or height is specified (the other being 0), scale the
image proportionally to maintain aspect ratio. Previously, 0 was passed
directly to the resize function which produced a 0x0 image.
2026-01-08 12:20:58 -08:00
817d760b4d Add failing tests for proportional scaling
When only one dimension is provided (e.g., width=400, height=0),
the image should scale proportionally. Currently returns 0x0.
2026-01-08 12:20:19 -08:00
70d55977c0 Add WebP encoding support
Uses github.com/gen2brain/webp - a CGO-free library that uses WASM via
wazero runtime for encoding. WebP decoding was already supported.

- Add gen2brain/webp dependency for encoding
- Implement WebP encoding in processor.go
- Add FormatWebP to SupportedOutputFormats
- Re-enable WebP option in generator form dropdown
- Mark WebP encoding as complete in TODO.md
2026-01-08 11:55:45 -08:00
0c9eb35bd2 Add failing test for WebP encoding support
TDD: This test expects WebP encoding to succeed. It currently fails
because WebP encoding is not implemented (returns ErrUnsupportedOutputFormat).
The test will pass once we add the gen2brain/webp library.
2026-01-08 11:54:03 -08:00
aab43db44a Add WebP and AVIF encoding support to P0 TODO 2026-01-08 11:12:59 -08:00
064ab10607 Add no-silent-fallback rule to CLAUDE.md
Never silently fall back to a different setting when a user's parameter
explicitly specifies a value. Return an error for invalid explicit values;
only apply defaults for omitted parameters.
2026-01-08 11:08:33 -08:00
37af10cc2b Update generator form for supported formats and Never expiry
- Remove WebP and AVIF options (encoding not supported)
- Add GIF option (encoding is supported)
- Add 'Never' TTL option as default
2026-01-08 11:08:28 -08:00
b55b75cbe7 Fix silent fallbacks for unsupported formats and fit modes
- Return ErrUnsupportedOutputFormat for WebP/AVIF encoding
- Return ErrInvalidFitMode for unknown fit mode values
- Add ValidateFitMode() for input validation
- Validate fit mode at handler level before processing

Silent fallbacks violate the principle of least surprise and mask bugs.
When a user explicitly specifies a value, we should either honor it or
return an error - never silently substitute a different value.
2026-01-08 11:08:22 -08:00
df6d347e68 Add tests for unsupported output format errors
Tests verify that WebP and AVIF encoding requests return
ErrUnsupportedOutputFormat instead of silently falling back
to a different format.
2026-01-08 11:08:16 -08:00
014c144d73 Add 'Never' expiry option for encrypted URLs
- Make ExpiresAt optional in CBOR (omitempty) for smaller tokens
- Treat ExpiresAt=0 as 'never expires' in parser
- URL-encode token with url.PathEscape() for safety
- Add 'Never' as default TTL option in generator form
2026-01-08 11:08:11 -08:00
d9f4e2038e Add manual test script for auth and encrypted URLs
scripts/manual-test.sh tests:
1. Healthcheck endpoint
2. Login page displays
3. Wrong password shows error
4. Correct password shows generator
5. Generate encrypted URL
6. Fetch image via encrypted URL
7. Fetch image via direct proxy (whitelisted)
8. Logout redirects to login
9. Expired URL returns 410
2026-01-08 10:53:02 -08:00