quak backup writes each file's ML data into its JSON (closes #163)
check / check (push) Successful in 1m37s
check / check (push) Successful in 1m37s
lib.backup() now waits for an ML data fetch, joining the one its refresh started or starting one, before it writes the per-file JSON, and each file's JSON carries the cached payload as mlData. When the fetch fails, each file with no cached ML data gets mlDataError and an entry in failures.json, so the result counts it as failed and quak backup exits 1; the next run fetches again. Judgement call: the wait comes after the originals are downloaded, so the fetch runs alongside the downloads. Judgement call: a failed ML fetch is recorded per file in failures.json, which is how the exit code goes non-zero without changing src/cli-commands.ts. Model: opus-5-5
This commit was merged in pull request #172.
This commit is contained in:
@@ -52,6 +52,7 @@ import { runBackup, type BackupLibrary } from "../../src/backup.js";
|
||||
import { Library } from "../../src/library/index.js";
|
||||
import type { ContentSource } from "../../src/library/content.js";
|
||||
import type { CollectionsPage, FilesPage } from "../../src/client.js";
|
||||
import type { MLData } from "../../src/mldata-fetch.js";
|
||||
import type { Collection, EnteFile } from "../../src/model/types.js";
|
||||
import {
|
||||
asLivePhoto,
|
||||
@@ -828,6 +829,94 @@ describe("the refresh before a backup", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Also serves ML data: file 100 has `ML_PAYLOAD`, the other two have none.
|
||||
// While `mlError` is set, every ML data request fails with it.
|
||||
const ML_PAYLOAD = { face: { faces: [] }, clip: { embedding: [0.5, 0.25] } };
|
||||
|
||||
class MLClient extends MockClient {
|
||||
mlError?: string;
|
||||
async fetchMLData(args: {
|
||||
fileIDs: number[];
|
||||
}): Promise<Map<number, MLData>> {
|
||||
if (this.mlError) throw new Error(this.mlError);
|
||||
const result = new Map<number, MLData>();
|
||||
if (args.fileIDs.includes(100)) result.set(100, ML_PAYLOAD);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// The JSON the backup in `outDir` wrote beside the original of `fileID`.
|
||||
const fileJSON = (outDir: string, fileID: number): Record<string, unknown> =>
|
||||
JSON.parse(readFileSync(saved(outDir, `${fileID}.json`), "utf-8"));
|
||||
|
||||
describe("ML data in each file's JSON", () => {
|
||||
it("writes a file's ML data, and no ML field for a file that has none", async () => {
|
||||
const lib = await openLibrary(stubSource(), new MLClient());
|
||||
const outDir = join(root, "backup");
|
||||
|
||||
const result = await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
expect(result.failed).toBe(0);
|
||||
expect(fileJSON(outDir, 100).mlData).toEqual(ML_PAYLOAD);
|
||||
expect(fileJSON(outDir, 101)).not.toHaveProperty("mlData");
|
||||
expect(fileJSON(outDir, 101)).not.toHaveProperty("mlDataError");
|
||||
await lib.close();
|
||||
});
|
||||
|
||||
it("gives each file with no cached ML data the reason when the fetch fails, and fails the run", async () => {
|
||||
const client = new MLClient();
|
||||
const outDir = join(root, "backup");
|
||||
const first = await openLibrary(stubSource(), client);
|
||||
await first.backup({ downloadDirectory: outDir });
|
||||
await first.close();
|
||||
|
||||
client.mlError = "HTTP 503 from server";
|
||||
const lib = await openLibrary(stubSource(), client);
|
||||
const result = await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
// File 100's ML data was cached by the first run and is kept.
|
||||
expect(fileJSON(outDir, 100).mlData).toEqual(ML_PAYLOAD);
|
||||
expect(fileJSON(outDir, 100)).not.toHaveProperty("mlDataError");
|
||||
for (const fileID of [101, 200]) {
|
||||
expect(fileJSON(outDir, fileID).mlDataError).toBe(
|
||||
"HTTP 503 from server",
|
||||
);
|
||||
}
|
||||
expect(result.failed).toBe(2);
|
||||
expect(result.errors.map((e) => [e.fileID, e.error])).toEqual([
|
||||
[101, "ML data: HTTP 503 from server"],
|
||||
[200, "ML data: HTTP 503 from server"],
|
||||
]);
|
||||
expect(Object.keys(readLedger(outDir).files)).toEqual(["101", "200"]);
|
||||
await lib.close();
|
||||
});
|
||||
|
||||
it("writes the ML data on the run after a failed fetch", async () => {
|
||||
const client = new MLClient();
|
||||
client.mlError = "HTTP 503 from server";
|
||||
const outDir = join(root, "backup");
|
||||
const first = await openLibrary(stubSource(), client);
|
||||
expect((await first.backup({ downloadDirectory: outDir })).failed).toBe(
|
||||
3,
|
||||
);
|
||||
expect(fileJSON(outDir, 100)).not.toHaveProperty("mlData");
|
||||
await first.close();
|
||||
|
||||
client.mlError = undefined;
|
||||
const lib = await openLibrary(stubSource(), client);
|
||||
const result = await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
expect(result.failed).toBe(0);
|
||||
expect(result.skipped).toBe(3);
|
||||
expect(fileJSON(outDir, 100).mlData).toEqual(ML_PAYLOAD);
|
||||
for (const fileID of [100, 101, 200]) {
|
||||
expect(fileJSON(outDir, fileID)).not.toHaveProperty("mlDataError");
|
||||
}
|
||||
expect(existsSync(join(outDir, "failures.json"))).toBe(false);
|
||||
await lib.close();
|
||||
});
|
||||
});
|
||||
|
||||
// Every entry under collections/, one level of directories deep, with each
|
||||
// symlink's target.
|
||||
const tree = (outDir: string): string[] => {
|
||||
@@ -871,6 +960,8 @@ describe("backup album folders", () => {
|
||||
thumbnail: async () => {
|
||||
throw new Error("no thumbnails in this stand-in");
|
||||
},
|
||||
fetchMLData: async () => {},
|
||||
mlData: async () => undefined,
|
||||
});
|
||||
|
||||
const albumID = (outDir: string, jsonName: string): number =>
|
||||
|
||||
@@ -732,6 +732,22 @@ describe("backup", () => {
|
||||
expect(stderr.text).toBe("Starting backup...\n");
|
||||
});
|
||||
|
||||
it("exits 1 and lists each file when the ML data fetch fails", async () => {
|
||||
const client = {
|
||||
...fakeClient(),
|
||||
fetchMLData: async () => {
|
||||
throw new Error("HTTP 503 from server");
|
||||
},
|
||||
} as unknown as Client;
|
||||
expect(
|
||||
await backupCommand(context(client), join(root, "backup"), {}),
|
||||
).toBe(1);
|
||||
expect(stderr.text).toContain(" Failed: 3\n");
|
||||
expect(stderr.text).toContain(
|
||||
" [Vacation] beach.jpg (id 100): ML data: HTTP 503 from server\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("exits 1 with the error on one line when the refresh fails", async () => {
|
||||
const client = {
|
||||
...fakeClient(),
|
||||
|
||||
Reference in New Issue
Block a user