Docker Build and Publish / build-and-push (push) Failing after 11s
The archive root was filtered by prefix, but the recursive walk below it was
not, so anything inside a profile directory got indexed. NAS filesystems put
sidecar metadata *inside* every folder rather than only at the share root:
Synology writes @eaDir (thumbnails and indexing data), #recycle holds
deletions, .sync is Resilio state. On the live share those account for 12,516
of 123,023 files.
None currently sit inside a profile directory, so nothing was miscounted yet —
but the moment that share gets indexed for Photos, every generated thumbnail
would be counted as archive media and stat'd one by one over the network, which
is the cost the index exists to avoid.
One isSystemDirectory rule now applies at every level, and the root listing uses
it too instead of keeping a second copy of the pattern.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uBWhwV3wFQ5MBCcMHHem7
Security
- Fix path traversal in GET /api/archives/:name/files. Express decodes route
params after segment matching, so `..%2f..%2fetc` escaped ARCHIVES_DIR and
returned a recursive listing of arbitrary directories.
- Add CSP and baseline security headers; disable x-powered-by.
- Stop baking GEMINI_API_KEY into the client bundle (the SDK was unused).
- Run the container as `node` instead of root.
Performance
- Add a directory-mtime-keyed archive index, warmed in the background and
persisted. Listing 110k files went from ~52s to ~0.1s; the largest archive
(24k files) serves in ~0.3s. Per-file stat over CIFS costs ~1.4ms and does
not parallelise, so it is now done once rather than per request.
- Build media URLs from the File directly instead of
`new Blob([await file.arrayBuffer()])`, which read every media file fully
into memory (a 20GB archive tried to become 20GB of resident blobs).
- Track and revoke object URLs; previously none were ever revoked.
- Give `requestThumbnail` a stable identity so a completed thumbnail stops
re-running the effect in every mounted thumbnail.
- Namespace IndexedDB keys so listing archives no longer deserializes every
cached thumbnail blob, and thumbnails no longer collide across archives.
- Serve real file sizes: RemoteArchiveFile was constructed with size 0, which
silently disabled high-res thumbnailing for every server archive.
Correctness
- Local archives cached media as blob: URLs, which die with the document, so
a cached local archive restored as an archive of broken images. Media now
carries a stable path and is rehydrated from a persisted directory handle
(File System Access API), falling back to re-prompting for the folder.
- Fix permalinks: the URL-writing effect erased ?a= on mount before the
archive list arrived to consume it, so deep links never resolved.
- Make cache invalidation detect nested changes via a directory signature.
- Add an error boundary and tolerate unparseable dates, which previously
threw a RangeError and blanked the app.
- Default video to muted so autoplay is not blocked by Safari/Firefox.
Features
- Fold sidecar directories into their base profile: `<user> - reels`,
`story - <user>` and `story highlights - <user> - <title>` now appear as
reels, the story ring and Instagram-style highlight circles rather than as
separate archives.
Housekeeping
- Add @types/react; React was previously type-checked against its JavaScript
source, so `npm run lint` gave almost no type safety on components.
- Vendor fonts and PWA icons locally; the app made third-party CDN requests
despite advertising offline support and local-only processing.
- Drop unused better-sqlite3 (a native module that broke `npm install`).
- Add vitest with 36 tests over the filename and directory-naming rules.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uBWhwV3wFQ5MBCcMHHem7