next -> main #149

Open
clawbot wants to merge 11 commits from next into main
11 Commits
Author SHA1 Message Date
clawbot fd7d7ed205 Container makes /var/lib/pixa usable before starting pixad (closes #159)
check / check (push) Waiting to run
The entrypoint now creates /var/lib/pixa if it is missing. When the
directory or one of its top-level entries belongs to another user or
group, it gives the whole tree to pixad (uid and gid 65532); it then
sets the directory's mode to 750 and runs the server as pixad as
before. Data left by a run under another uid is taken over this way.
Only the top level is checked, so a normal start does not walk the
cache; the tree is changed deepest first, so an interrupted start is
finished by the next one.

"Running under upaas" in README.md no longer tells the operator to
create or chown the host directory.

Model: opus-5-5
2026-09-29 12:44:37 +02:00
clawbot bce8860c2e Keep variant content types in memory for cache hits (closes #70)
check / check (push) Successful in 14s
Cache.metaCache was declared and never used, so every cache hit read
and parsed the variant's .meta file. It is now an LRU
(github.com/hashicorp/golang-lru/v2) of up to 10,000 variants' content
types, filled by StoreVariant and by a read of a .meta file, so a hit
for a variant it holds skips the .meta read. Only a type from a .meta
file or a store ever enters memory, never the application/octet-stream
fallback, and a stored type is never replaced by an older one from
disk. The variant file itself is still opened on every hit, so nothing
is served from memory alone.

Model: opus-5-5
2026-09-29 12:00:10 +02:00
clawbot 99735f479b Answer image requests with 503 in maintenance mode (closes #71)
check / check (push) Successful in 13s
maintenance_mode was only reported by the health check; every request
was still served. One middleware in routes.go, applied to /v1/image/
and /v1/e/ only, now answers them with 503, a Retry-After of
MaintenanceRetryAfterSeconds and the JSON error body while it is on.
It calls Server.MaintenanceMode(), which had no caller.

The health check stays 200 and reports maintenance_mode: the image's
Docker HEALTHCHECK requests it, a 503 there would make the container
unhealthy, and upaas marks a deploy failed when its container is
unhealthy. The login and URL generator pages and /metrics keep working.
Documented in README.md and config.example.yml.

Model: opus-5-5
2026-09-29 11:01:30 +02:00
clawbot baf457d21e Bound concurrent image processing and upstream fetches (closes #64)
check / check (push) Successful in 17s
Nothing bounded total in-flight work, so a burst of cache misses across
hosts could exhaust memory. Two settings now do: max_concurrent_processing
(default the CPUs Go uses) and upstream_connections (default 64, beside
the per-host limit). A request that finds either full waits up to 10
seconds, then gets 503; a slot is released on every path. No request
holds source bytes while it waits: a cached source is read only after
the processing slot is taken, and a fetched one only while it holds its
upstream connection. libvips runs one worker thread per image with its
operation cache off. Both waits count toward downstream_timeout, as the
README says.

Model: opus-5-5
2026-09-29 10:32:16 +02:00
clawbot 0c99be939a Install Dockerfile build dependencies through script/bootstrap (closes #95)
check / check (push) Successful in 3m29s
The Dockerfile lint and build stages and Dockerfile.lint each carried
their own apk add list, a copy of what script/bootstrap installs. They
now copy script/, go.mod and go.sum and run script/bootstrap, so that
layer is reused until one of those changes. script/bootstrap gains a C
compiler check: the golang image has none, and cgo needs one.

The build adds -trimpath and -s -w; CGO_ENABLED=1 stays, as govips
links libvips. ARG VERSION moves to just above the build, so a new
version reruns neither script/bootstrap nor the tests.

Model: opus-5-5
2026-09-29 09:11:20 +02:00
clawbot 56217cbf4a Add the four settings the README documented but pixa lacked (closes #61)
check / check (push) Successful in 15s
README.md documented access_control_allow_origin,
upstream_fetch_timeout, upstream_max_response_size and
downstream_timeout, but pixa did not know them, so a config following
the README aborted startup. Each is now a setting with its PIXA_
variable, defaulting to the value that was fixed in the code: *, 30s,
50 MiB and 60s. Durations are Go duration strings and must be positive;
the size is whole bytes, at most 1 GiB. The origin is * or one http or
https origin written exactly as a browser sends it; anything else
aborts startup. downstream_timeout sets both the server's write timeout
and the per-request timeout. The owner approved the edits to existing
tests.

Model: opus-5-5
2026-09-29 08:49:18 +02:00
clawbot 46a55ec15d Move migrations to internal/db/migrations (closes #96)
check / check (push) Successful in 14s
REPO_POLICIES.md puts migrations in internal/db/migrations/ as
000_migration.sql and 001_schema.sql. The two files move there with
their contents unchanged. go:embed cannot reach outside its own
package, so internal/db/migrations has a small package that embeds
them, and internal/database reads them through its FS(). The database
package stays where CONVENTIONS.md puts it; moving it would change
existing test files in other packages.

The version still comes from the filename prefix, so a database that
has recorded versions 0 and 1 runs neither again. A new test applies
the migrations twice to one database file and checks that the second
run applies nothing.

Model: opus-5-5
2026-09-29 07:42:05 +02:00
clawbot 1b920fe000 Correct trusted_proxies advice and state signature padding (closes #150)
check / check (push) Successful in 14s
The README told operators to set trusted_proxies to the proxy's own
address. A proxy on the Docker host that connects over 127.0.0.1 reaches
pixa from the Docker network's gateway, so that advice made pixa count
every user as one client for the login limit. The login-limit paragraph,
the trusted_proxies entry and config.example.yml now say to use the
address pixa sees for requests through the proxy, that a proxy connecting
through another host address is seen with that address, and how to read
it from the request log.

The signature section now says sig is base64url with the = padding
kept, since pixa compares it exactly, and shows the example's sig for a
stated key, computed with pixa's signer.

Model: opus-5-5
2026-09-29 06:05:44 +02:00
clawbot e98b998cb6 Make the cache stats count what is cached, fetched and transcoded (closes #56)
check / check (push) Successful in 18s
Cache.Stats read request_cache and output_content, which nothing
writes, so TotalItems and TotalSizeBytes were always 0. They now count
source_content plus variant_content and use UsageBytes; a disabled disk
cache reports 0 for both. The upstream fetch count and bytes and the
transform count never moved: Get now passes the bytes it fetched
(including those read before a failed body read) and counts each
successful transcode. Hits, misses and these counters are written with
context.WithoutCancel, so a client disconnect or the request timeout no
longer loses them. The unused tables stay; metaCache is #70.

Model: opus-5-5
2026-09-29 05:25:10 +02:00
clawbot ed3f8770e6 Give pixad a fixed uid and gid 65532 (closes #151)
check / check (push) Successful in 20s
adduser took the first free uid, 1000, and the entrypoint gives a
bind-mounted /var/lib/pixa to pixad, so on the host a person's login
account ended up owning pixa's database and cache. The image now creates
the pixad group with gid 65532 and the pixad user with uid 65532, which
host login and system accounts do not use. The first-run step of
"Running under upaas" in README.md names the uid and gid.

Model: opus-5-5
2026-09-29 04:44:49 +02:00
clawbot 2afe61e301 Keep max-age within an expiring image URL's lifetime (closes #63)
check / check (push) Successful in 12s
Both image routes sent Cache-Control: public, max-age=31536000,
immutable unconditionally, so a browser or proxy could keep serving an
image for a year after its signed or encrypted URL had expired. max-age
is now the whole seconds left until the URL expires, never negative and
at most one year; a URL with no expiry keeps one year. The 304 answer
uses the same value. An encrypted URL's expiry now reaches
ImageRequest.Expires through ToImageRequest. immutable stays: freshness
now ends no later than the URL's expiry. README.md documents the header.

Model: opus-5-5
2026-09-29 03:51:58 +02:00