Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78dd2b8ebf |
+3
-11
@@ -23,10 +23,7 @@
|
|||||||
// download or a failed symlink is caught, recorded in `failures.json` with a
|
// download or a failed symlink is caught, recorded in `failures.json` with a
|
||||||
// classification, a running attempt count, and the last-tried time, and the run
|
// classification, a running attempt count, and the last-tried time, and the run
|
||||||
// continues. `result.failed` — and thus the CLI's exit code — stays non-zero
|
// continues. `result.failed` — and thus the CLI's exit code — stays non-zero
|
||||||
// while any failure remains unresolved and clears once every one succeeds. Each
|
// while any failure remains unresolved and clears once every one succeeds.
|
||||||
// run reconciles the ledger against the files it attempted, so an entry for a
|
|
||||||
// file that has since left the library (deleted) or this run's scope is dropped
|
|
||||||
// rather than counted forever, which would poison a scheduled backup's exit code.
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
copyFileSync,
|
copyFileSync,
|
||||||
@@ -401,13 +398,8 @@ export const runBackup = async (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconcile the ledger against what this run actually attempted: an entry
|
// A file in scope that did not fail this run has no unresolved failure.
|
||||||
// survives only for a file that failed this run. A file that succeeded had
|
for (const fileID of distinct.keys()) {
|
||||||
// its failure resolved; a file gone from the library (deleted) or outside
|
|
||||||
// this run's scope is not something this run can resolve, so keeping its
|
|
||||||
// stale entry would keep the exit code non-zero forever — a single
|
|
||||||
// since-deleted photo would fail every future scheduled backup.
|
|
||||||
for (const fileID of [...ledger.keys()]) {
|
|
||||||
if (!failedThisRun.has(fileID)) ledger.delete(fileID);
|
if (!failedThisRun.has(fileID)) ledger.delete(fileID);
|
||||||
}
|
}
|
||||||
saveLedger(ledgerPath, ledger);
|
saveLedger(ledgerPath, ledger);
|
||||||
|
|||||||
@@ -147,28 +147,6 @@ const readLedger = (
|
|||||||
): { files: Record<string, Record<string, unknown>> } =>
|
): { files: Record<string, Record<string, unknown>> } =>
|
||||||
JSON.parse(readFileSync(join(outDir, "failures.json"), "utf-8"));
|
JSON.parse(readFileSync(join(outDir, "failures.json"), "utf-8"));
|
||||||
|
|
||||||
// Write a durable ledger holding one prior failure, to exercise pruning of
|
|
||||||
// entries the current run cannot resolve.
|
|
||||||
const seedLedger = (outDir: string, fileID: number, title: string): void => {
|
|
||||||
mkdirSync(outDir, { recursive: true });
|
|
||||||
writeFileSync(
|
|
||||||
join(outDir, "failures.json"),
|
|
||||||
JSON.stringify({
|
|
||||||
version: 1,
|
|
||||||
files: {
|
|
||||||
[String(fileID)]: {
|
|
||||||
fileID,
|
|
||||||
title,
|
|
||||||
classification: "transient",
|
|
||||||
attempts: 1,
|
|
||||||
lastTriedAt: Date.now(),
|
|
||||||
error: "HTTP 500 from server",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
root = mkdtempSync(join(tmpdir(), "quak-backup-test-"));
|
root = mkdtempSync(join(tmpdir(), "quak-backup-test-"));
|
||||||
});
|
});
|
||||||
@@ -399,47 +377,6 @@ describe("lib.backup", () => {
|
|||||||
lib.close();
|
lib.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prunes a ledger entry for a file no longer in the library and exits zero", async () => {
|
|
||||||
const source = stubSource();
|
|
||||||
const lib = await openLibrary(source);
|
|
||||||
const outDir = join(root, "backup");
|
|
||||||
|
|
||||||
// A prior failure for a file that has since left the library (deleted
|
|
||||||
// from the account). This run has no way to resolve it, so it must not
|
|
||||||
// keep the exit code non-zero forever.
|
|
||||||
seedLedger(outDir, 999, "gone.jpg");
|
|
||||||
|
|
||||||
const result = await lib.backup({ downloadDirectory: outDir });
|
|
||||||
|
|
||||||
// Everything still present is backed up cleanly, and the stale entry is
|
|
||||||
// dropped rather than counted.
|
|
||||||
expect(result.downloaded).toBe(3);
|
|
||||||
expect(result.failed).toBe(0);
|
|
||||||
expect(existsSync(join(outDir, "failures.json"))).toBe(false);
|
|
||||||
lib.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("prunes an out-of-scope ledger entry on a scoped run and exits zero", async () => {
|
|
||||||
const source = stubSource();
|
|
||||||
const lib = await openLibrary(source);
|
|
||||||
const outDir = join(root, "backup");
|
|
||||||
|
|
||||||
// A prior failure for a Vacation file; this run is scoped to Work and
|
|
||||||
// never attempts it, so it must not poison the scoped run's exit code.
|
|
||||||
seedLedger(outDir, 100, "beach.jpg");
|
|
||||||
|
|
||||||
const result = await lib.backup({
|
|
||||||
downloadDirectory: outDir,
|
|
||||||
onlyAlbumNames: ["Work"],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.totalFiles).toBe(1);
|
|
||||||
expect(result.failed).toBe(0);
|
|
||||||
expect(existsSync(join(outDir, "originals", "200.png"))).toBe(true);
|
|
||||||
expect(existsSync(join(outDir, "failures.json"))).toBe(false);
|
|
||||||
lib.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("also stores thumbnails when includeThumbnails is set", async () => {
|
it("also stores thumbnails when includeThumbnails is set", async () => {
|
||||||
const source = stubSource();
|
const source = stubSource();
|
||||||
const lib = await openLibrary(source);
|
const lib = await openLibrary(source);
|
||||||
|
|||||||
Reference in New Issue
Block a user