Release · v3.14.21
Label stored objects honestly and keep the repair…
chore(storage): label stored objects honestly and keep the repair tool safe
Details
Every object in the `site-assets` bucket answers `cache-control: no-cache`, and
15 of the 16 documents answer `content-type: application/octet-stream`. The
uploader already set a MIME type and an hour's lifetime, so those objects were
not put there by it.
Be clear about what this is worth, because two earlier drafts of this commit
were not. It is hygiene. It is not the egress fix:
- `no-cache` does NOT mean re-downloaded. Storage sends an ETag and honours
conditional requests: a repeat view of a document returns 304 and costs zero
bytes. A longer lifetime saves a revalidation round trip, not a download.
- `octet-stream` does NOT stop the in-tab viewer rendering a PDF.
`inferContentTypeFromUrl` repairs the type from the URL extension before
building the Blob; octet-stream PDFs render today. A wrong type matters
outside the viewer — a direct link, a crawler, a download.
The egress fix was moving the images out of Storage, which the two preceding
commits do.
So the cache lifetime stays at an hour rather than rising to a year. Objects
are keyed by file name and the row URLs carry no version, so a new CV is
uploaded over `documents/CV.pdf` and returning visitors keep whatever an
earlier response told them to keep — a year-long lifetime would hide an updated
CV from them for a year, with no way to force a refresh short of renaming the
file and rewriting the row. Since the lifetime was never the expensive thing,
there is nothing to buy by taking that risk.
What does change:
- The MIME table covers the extensions `mimetypes` misses (webp, xlsx, pptx,
docx), and `guess_content_type` raises rather than mislabelling an unknown
one. Storage renders this field as `max-age=N` and gives no way to add
`immutable`, so nothing claims it does.
- A new `db:repair-storage` re-stamps objects already in the bucket by copying
each over itself, which re-records metadata without re-uploading bytes.
The copy endpoint takes `metadata: {cacheControl, mimetype}` and, critically,
ignores fields it does not recognise instead of rejecting them — so a payload
with the wrong shape returns success and changes nothing. This script has never
run against a real bucket, so it reads every object back after writing and
fails loudly if the headers did not move, and `--limit N` lets the first real
run risk one object rather than sixteen.
Constants and the MIME table moved to `_db_common.py`, which imports only the
standard library, so the repair script does not pull in the TUS client to learn
a file's type.
What this does NOT close: 17.41 MiB of documents still live in Storage and are
fetched on click, the largest a 6.55 MiB poster. A warm visitor costs nothing,
but a crawler has no cache and pulls the lot on every pass — roughly 290 full
crawls would exhaust 5 GB. If that ever bites, the answer is the move the two
preceding commits already prove out: put the documents on GitHub Pages too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DAJJ63FFnLoD5XLpu6yJaG
Files changed (5)
| backend/supabase_db/DATABASE.md | +46 | −0 |
| backend/supabase_db/scripts/_db_common.py | +56 | −0 |
| backend/supabase_db/scripts/repair_storage_metadata.py | +264 | −0 |
| backend/supabase_db/scripts/upload_storage_assets.py | +15 | −7 |
| package.json | +1 | −0 |