Compare commits

..
1 Commits
Author SHA1 Message Date
sneak 97c4b1185b Rebuild backup on the library API with a durable failure ledger (closes #51)
check / check (push) Successful in 26s
backup() is a Library method: it refreshes, fetches each pending original
(and optional thumbnails) through the content cache and pools, then rebuilds
the sidecar, symlink, and per-collection JSON views from the model. The
downloadDirectory layout and exit-code contract are unchanged.

An original already on disk is complete and never re-fetched, so runs are
idempotent and an interrupted run resumes. Per-file download and symlink
failures are recorded in a durable failures.json and no longer abort the run,
subsuming #8.

Each run reconciles the ledger against the files it attempted: an entry
survives only for a file that failed this run, so a failure for a
since-deleted or out-of-scope file clears instead of failing every future
scheduled backup.

Model: opus-4-8
2026-09-22 17:20:42 +00:00
2 changed files with 1 additions and 26 deletions
-4
View File
@@ -289,10 +289,6 @@ export const runBackup = async (
collection: string,
err: unknown,
): void => {
// Count at most one attempt per file per run: a file whose original
// and thumbnail both fail this run must not double its attempt count
// or appear twice in errors.
if (failedThisRun.has(file.id)) return;
const error = errorMessage(err);
errors.push({
fileID: file.id,
+1 -22
View File
@@ -111,7 +111,6 @@ class MockClient {
// told to fail one fileID's original, to exercise per-file resilience.
interface StubSource extends ContentSource {
failID?: number;
failThumbID?: number;
originalCalls: number;
}
@@ -125,8 +124,7 @@ const stubSource = (): StubSource => {
writeFileSync(destination, Buffer.alloc(size));
return { bytesWritten: size };
},
thumbnail: async ({ file: f, destination }) => {
if (s.failThumbID === f.id) throw new Error("HTTP 500 from server");
thumbnail: async ({ destination }) => {
writeFileSync(destination, Buffer.alloc(5));
return { bytesWritten: 5 };
},
@@ -456,23 +454,4 @@ describe("lib.backup", () => {
expect(existsSync(join(outDir, "thumbnails", "200.jpg"))).toBe(true);
lib.close();
});
it("counts one attempt when a file fails both its original and thumbnail in a run", async () => {
const source = stubSource();
source.failID = 101;
source.failThumbID = 101;
const lib = await openLibrary(source);
const outDir = join(root, "backup");
const result = await lib.backup({
downloadDirectory: outDir,
includeThumbnails: true,
});
// Both kinds fail for 101, but the run counts it once.
const errs = result.errors.filter((e) => e.fileID === 101);
expect(errs.length).toBe(1);
expect(readLedger(outDir).files["101"]!.attempts).toBe(1);
lib.close();
});
});