Compare commits

1 Commits
Author SHA1 Message Date
sneak 5f7b786dda Rebuild backup on the library API with a durable failure ledger (closes #51)
check / check (push) Successful in 30s
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 resume after interruption. Per-file download and symlink
failures go to a durable failures.json and no longer abort the run
(subsuming #8); a file failing more than one way in a run counts one attempt
and is listed once.

Each run reconciles the ledger against the files it attempted, so a failure
for a since-deleted or out-of-scope file clears instead of failing every
future backup.

Model: opus-4-8
2026-09-22 18:04:50 +00:00
2 changed files with 26 additions and 1 deletions
+4
View File
@@ -289,6 +289,10 @@ 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,
+22 -1
View File
@@ -111,6 +111,7 @@ class MockClient {
// told to fail one fileID's original, to exercise per-file resilience.
interface StubSource extends ContentSource {
failID?: number;
failThumbID?: number;
originalCalls: number;
}
@@ -124,7 +125,8 @@ const stubSource = (): StubSource => {
writeFileSync(destination, Buffer.alloc(size));
return { bytesWritten: size };
},
thumbnail: async ({ destination }) => {
thumbnail: async ({ file: f, destination }) => {
if (s.failThumbID === f.id) throw new Error("HTTP 500 from server");
writeFileSync(destination, Buffer.alloc(5));
return { bytesWritten: 5 };
},
@@ -454,4 +456,23 @@ 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();
});
});