Fix cover updates, and accept a cover URL #26
Loading…
Reference in a new issue
No description provided.
Delete branch "cover-updates"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Replacing a cover appeared to do nothing, and chasing that turned up two
adjacent bugs plus a missing affordance.
Stale covers.
/cover/{uuid}is a stable URL served withmax-age=86400, so once new bytes landed on disk the browser keptshowing the copy it still considered fresh, for up to a day. Now served
no-cache;ServeFilealready setsLast-Modified, so revalidation isa 304 in the common case.
internal/kobo/cover.gostill has the samepublic, max-age=86400on an equally stable URL — left alone here sinceit is device-facing.
Silently swallowed failures. The cover-replacement path discarded
every error and redirected 303 exactly like a success, which is why a
cover that never changed gave nothing to go on. Failures now render on
the edit page with the reason.
PNG stored as .jpg. The form accepted
image/pngwhileWriteCoveralways named the file
.jpgand both cover handlers announceimage/jpeg.WriteCovernow normalises — JPEG byte-for-byte, PNGre-encoded, anything else
ErrUnsupportedImage— rather than making theextension dynamic, which
RenameCover,Archive,Restoreand theKobo handler would all have to agree on. An epub with an unstorable
cover now loses its thumbnail instead of failing the import, and the
admin API answers 415 when a body is not the image its
Content-Typeclaimed.
Cover URLs. A
cover_urlfield on the edit form, fetchedserver-side, so a cover found online need not be downloaded and
re-uploaded by hand. An attached file wins over a URL left in the field.
The server making that request is the part worth care: the URL is
attacker-chosen input even though only the owner can submit it, and this
host shares networks with the identity provider, the vault and its own
admin API on localhost.
internal/coverfetchis the single place thatdecides what is reachable — http/https only, and a dial-time check of
the resolved address, so a hostname pointing into private space and
any redirect hop are both caught where inspecting the URL string would
miss them. Blocked: loopback, RFC1918, link-local, IPv6 ULA, and
100.64.0.0/10explicitly, sincenet.IP.IsPrivatedoes not cover itand it is the tailnet range.
Content-Typeis not trusted; themagic-byte check stays the authority. 15s timeout, 20 MiB cap enforced
on the body rather than on a
Content-Lengththat can lie.Testing
go vetclean, full suite green,CGO_ENABLED=0build OK.Added: PNG re-encode, unsupported rejection, lying
Content-Type→ 415,a successful multipart upload, a rejected cover asserting 400 with the
message visible and
CoverPathleft unset, 9coverfetchtests(including redirect-to-private and a blocked/allowed address table), and
6 web-level URL tests — stored, empty URL is not a change, bad scheme,
private address refused and never fetched, upstream status surfaced,
and file-beats-URL asserting the URL server was hit zero times.
Existing cover fixtures were string literals like
[]byte("jpg-bytes"),which the format check now rejects, so they generate real images.
Note
One existing library file,
Eric Matthes/Python Crash Course.jpg, isPNG bytes in a
.jpg(the only one of 129). It renders because browserssniff; re-uploading or re-importing normalises it. Not migrated here.
🤖 Generated with Claude Code
Replacing a cover appeared to do nothing. /cover/{uuid} is a stable URL served with max-age=86400, so after the new bytes landed on disk the browser kept showing the copy it still considered fresh, for up to a day. Serve it no-cache instead: ServeFile already sets Last-Modified, so the revalidation is a 304 in the common case. The edit form also accepts image/png while WriteCover always named the file .jpg and both cover handlers always announce image/jpeg. Normalise on write — JPEG passes through byte-for-byte, PNG is re-encoded, anything else is ErrUnsupportedImage — rather than make the extension dynamic, which RenameCover, Archive, Restore and the Kobo cover handler would all have to agree on. That sentinel needs handling where covers come from elsewhere: an epub declaring a cover we cannot store now loses its thumbnail instead of failing the import, and the admin API answers 415 when a body is not the image its Content-Type claimed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>