quak backup writes the account and album records backup-metadata writes (closes #166)
check / check (push) Successful in 1m23s

quak backup now writes account.json with the account's email and user ID,
adds each album's ownerID, isShared, updationTime and, when present, its
magicMetadata, pubMagicMetadata and sharedMagicMetadata to the album's
JSON, and adds updationTime to each file's JSON. The fields and their order
match backup-metadata's account.json, _collection.json and per-file JSON.
The interface runBackup drives gains whoami, which the library passes
through from its client.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #175.
This commit is contained in:
2026-10-06 07:13:24 +02:00
committed by clawbot
parent ab4d5d2cc2
commit 2483c85321
5 changed files with 159 additions and 10 deletions
+102
View File
@@ -12,6 +12,7 @@
* collections/
* <name>/<title> symlink to the original (rebuilt each run)
* <name>.json per-collection metadata (rebuilt each run)
* account.json the account's email and user ID
* failures.json durable ledger of unresolved failures
*
* The properties that distinguish backup from a naive download loop, and that
@@ -917,6 +918,106 @@ describe("ML data in each file's JSON", () => {
});
});
// The fields `RecordsClient` gives the Vacation album: shared by another
// account, with all three layers of magic metadata.
const SHARED_ALBUM = {
ownerID: 7,
isShared: true,
updationTime: 1700,
magicMetadata: { visibility: 0 },
pubMagicMetadata: { coverID: 100 },
sharedMagicMetadata: { visibility: 2 },
};
// Serves the Vacation album with `SHARED_ALBUM`'s fields, and each file with
// the update time 5000 + its ID.
class RecordsClient extends MockClient {
override async collectionsSince(): Promise<CollectionsPage> {
const page = await super.collectionsSince();
return {
...page,
collections: page.collections.map((c) =>
c.id === 1 ? { ...c, ...SHARED_ALBUM } : c,
),
};
}
override async filesSince(args: {
collectionID: number;
}): Promise<FilesPage> {
const page = await super.filesSince(args);
return {
...page,
files: page.files.map((f) => ({
...f,
updationTime: 5000 + f.id,
})),
};
}
}
// The JSON the backup in `outDir` wrote for the album named `name`.
const albumJSON = (outDir: string, name: string): Record<string, unknown> =>
JSON.parse(
readFileSync(join(outDir, "collections", `${name}.json`), "utf-8"),
);
describe("account and album records", () => {
it("writes account.json with the account's email and user ID", async () => {
const lib = await openLibrary(stubSource());
const outDir = join(root, "backup");
await lib.backup({ downloadDirectory: outDir });
expect(
JSON.parse(readFileSync(join(outDir, "account.json"), "utf-8")),
).toEqual({ email: "backup@example.com", userID: USER_ID });
await lib.close();
});
it("writes each album's owner, sharing, update time and magic metadata into its JSON", async () => {
const lib = await openLibrary(stubSource(), new RecordsClient());
const outDir = join(root, "backup");
await lib.backup({ downloadDirectory: outDir });
expect(albumJSON(outDir, "Vacation")).toEqual({
id: 1,
name: "Vacation",
type: "album",
...SHARED_ALBUM,
files: [
{ id: 100, metadata: file(100, 1, "beach.jpg").metadata },
{ id: 101, metadata: file(101, 1, "sunset.jpg").metadata },
],
});
// An album with no magic metadata gets no magic metadata fields.
expect(albumJSON(outDir, "Work")).toEqual({
id: 2,
name: "Work",
type: "album",
ownerID: USER_ID,
isShared: false,
updationTime: 1,
files: [
{ id: 200, metadata: file(200, 2, "diagram.png").metadata },
],
});
await lib.close();
});
it("writes each file's update time into its JSON", async () => {
const lib = await openLibrary(stubSource(), new RecordsClient());
const outDir = join(root, "backup");
await lib.backup({ downloadDirectory: outDir });
for (const fileID of [100, 101, 200]) {
expect(fileJSON(outDir, fileID).updationTime).toBe(5000 + fileID);
}
await lib.close();
});
});
// Every entry under collections/, one level of directories deep, with each
// symlink's target.
const tree = (outDir: string): string[] => {
@@ -962,6 +1063,7 @@ describe("backup album folders", () => {
},
fetchMLData: async () => {},
mlData: async () => undefined,
whoami: () => ({ email: "backup@example.com", userID: USER_ID }),
});
const albumID = (outDir: string, jsonName: string): number =>