From 79b1b8020b3f4ed560caadbf582e395073e8c24f Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 14 Oct 2026 04:26:56 +0000 Subject: [PATCH] lib.backup() fails only once every worker has stopped A worker that threw, as one does when onProgress throws on a FAILED line, made lib.backup() reject at once, so the lock was removed while the other workers still wrote originals into the backup. The backup now waits for every worker to settle and then throws the first error found. The README no longer says thumbnails are fetched one at a time, which no test checks. Model: opus-5-5 --- README.md | 3 +-- src/backup.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d8bb308..a4a046c 100644 --- a/README.md +++ b/README.md @@ -1056,8 +1056,7 @@ in-flight fetches by key, and an idle pool never lends its slots to a busy one. `lib.backup()`, and so `quak backup`, works on up to the content pool's limit of originals at once, 5 unless the library was opened with other `pools`. The limit covers everything it does for one original, hashing it for `verify` included, so -however large the account is, no more originals than that are open at once. With -`includeThumbnails`, it fetches the thumbnails one at a time. +however large the account is, no more originals than that are open at once. ### On-disk cache layout diff --git a/src/backup.ts b/src/backup.ts index b0cf52d..e7b9ea6 100644 --- a/src/backup.ts +++ b/src/backup.ts @@ -610,8 +610,7 @@ const runLockedBackup = async ( // present original is hashed first, and one that does not match the hash // its metadata records is removed and fetched like a missing one. One // that cannot be read is recorded as failed and left where it is. - // `storeOriginal` does this for one original, and records its failure - // rather than throwing. + // `storeOriginal` does this for one original, and records its failure. const storeOriginal = async (file: EnteFile): Promise => { let stored = storedAtSavePath(downloadDirectory, file); let mismatch = false; @@ -682,15 +681,20 @@ const runLockedBackup = async ( // Up to the content pool's limit of originals are worked on at once, // their `verify` hashing included, so however large the account is, no // more than that many are open. Each worker takes the next file from the - // iterator they all share until none is left. + // iterator they all share until none is left. A worker that throws, as + // one does when `onProgress` throws, fails the backup only once every + // worker has stopped, so none writes into the backup after it has ended. if (includeOriginals) { const pending = distinct.values(); const worker = async (): Promise => { for (const file of pending) await storeOriginal(file); }; - await Promise.all( + const workers = await Promise.allSettled( Array.from({ length: lib.contentConcurrency }, worker), ); + for (const w of workers) { + if (w.status === "rejected") throw w.reason; + } } if (includeThumbnails) {