70f5a8c54e
* fix(gallery): bound and reclaim storage reads in the remaining zip builders PR 1402 fixes the cached-zip builder. The same unguarded pattern — one storage read appended per photo, archiver draining one at a time, nothing destroying the rest — is still present in three other places, two of which a gallery guest reaches with no admin credentials: - routes/gallery/downloads.js, download-all and download-selected - services/downloadJobService.js, the custom-resolution job builder, which is worse in one respect: its per-photo catch skips a bad source without ever destroying the stream it had already opened, so every skipped photo leaked a socket for the life of the process An unread S3 response body holds its socket open indefinitely — the SDK arms its socket timeout on a 3s delay and clears it the moment response headers land, so a fast response never gets one — and archiver's abort() does not touch its source streams. That is the mechanism behind the incident described in PR 1402: 43 of 50 pooled sockets held with unread bytes, uploads and gallery reads starved behind them, a process restart the only way out. utils/archiveStreamGuard.js caps reads in flight at 2 and destroys whatever is still open on every exit: an error, a failed append, and — for the two guest routes — the client closing the tab mid-download, which previously left every appended-but-undrained read parked forever. Deliberately does not touch downloadZipService.js, so there is no conflict with 1402. Once that lands, its inline equivalent can move onto this helper. Local-filesystem installs are unaffected either way: they take archiver's archive.file(path) branch and open no sockets. Relates to issue 1399 * fix(gallery): survive a cancelled download and a read that dies while queued Two failures found reviewing the previous commit, both reachable on an ordinary download. A client hanging up mid-download aborted the archive, but the append loop's `break` still fell through to archive.finalize(), which rejects with ABORTED. The catch then called errorResponse over a response whose ZIP headers had already gone out, throwing ERR_HTTP_HEADERS_SENT from an async Express 4 handler with nothing to catch it — an unhandled rejection, on a cancelled download, which can take the process down. Both bulk routes now return without finalizing, and the catch stays quiet once headers are sent. A read that errored while still QUEUED behind another was absorbed by the guard's own error listener. archiver had not attached its source listener yet, so it never learned the stream had died, and the dead stream stayed in the queue: the archive hung when it reached it, and in downloadJobService the build held its concurrency slot with it. The guard now reports such a failure through onFatalError so the caller aborts the archive and reclaims the rest. Streams the guard destroyed itself are deliberately not reported — that is the caller's own teardown, already running. Relates to issue 1399 --------- Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>