An album whose folder cannot be made no longer stops the backup (closes #197)
check / check (push) Waiting to run
check / check (push) Waiting to run
Each album's folder, links and JSON are written inside one try. A failure prints `FAILED album <name>: <reason>`, lists the album's ID in the result's new `failedAlbums`, and so in `--json`, and makes `quak backup` exit 1. The other albums and `failures.json` are still written. An album whose folder would have another album's JSON name, ignoring case (`x.json` beside `x`), gets its ID added. A folder or link name over 255 bytes is cut short between two characters by `shortenToBytes` in `src/filename.ts`, and gets the ID; a folder name leaves room for `.json`. Apart from its call to `uniqueNames`, the album loop is only indented into the try. Model: opus-5-5
This commit is contained in:
+138
-1
@@ -1498,13 +1498,15 @@ describe("backup album folders", () => {
|
||||
files: EnteFile[];
|
||||
}
|
||||
|
||||
const libraryOf = (albums: Album[]): BackupLibrary => ({
|
||||
// The original of the file `notFound`, if given, always fails to download.
|
||||
const libraryOf = (albums: Album[], notFound?: number): BackupLibrary => ({
|
||||
refresh: async () => {},
|
||||
listCollections: () => albums.map((a) => a.collection),
|
||||
listFiles: (id) =>
|
||||
albums.find((a) => a.collection.id === id)?.files ?? [],
|
||||
undecryptable: () => [],
|
||||
original: async (fileID) => {
|
||||
if (fileID === notFound) throw new Error("HTTP 404");
|
||||
const path = join(root, `source-${fileID}`);
|
||||
writeFileSync(path, `original ${fileID}`);
|
||||
return { path };
|
||||
@@ -1600,6 +1602,141 @@ describe("backup album folders", () => {
|
||||
expect(albumID(outDir, "Trip (11) (12).json")).toBe(12);
|
||||
});
|
||||
|
||||
// The next three tests end with the album "ok" (14), whose one file
|
||||
// always fails to download, so each run has a failure to record in
|
||||
// failures.json after the album folders are written.
|
||||
const ok: Album = {
|
||||
collection: collection(14, "ok"),
|
||||
files: [file(5, 14, "e.jpg")],
|
||||
};
|
||||
|
||||
// The folder of an album named "x.json" would be the JSON file of the
|
||||
// album "x", and once ended every run there. Two runs, as on two nights,
|
||||
// write the same tree, and the failed file's attempt count goes up.
|
||||
it("adds the ID to an album whose folder would have another album's JSON name", async () => {
|
||||
const outDir = join(root, "backup");
|
||||
const lib = libraryOf(
|
||||
[
|
||||
{
|
||||
collection: collection(10, "x"),
|
||||
files: [file(1, 10, "a.jpg")],
|
||||
},
|
||||
{
|
||||
collection: collection(11, "x.json"),
|
||||
files: [file(2, 11, "b.jpg")],
|
||||
},
|
||||
{
|
||||
collection: collection(12, "Trip"),
|
||||
files: [file(3, 12, "c.jpg")],
|
||||
},
|
||||
{
|
||||
collection: collection(13, "TRIP.json"),
|
||||
files: [file(4, 13, "d.jpg")],
|
||||
},
|
||||
ok,
|
||||
],
|
||||
5,
|
||||
);
|
||||
|
||||
await runBackup(lib, { downloadDirectory: outDir });
|
||||
const result = await runBackup(lib, { downloadDirectory: outDir });
|
||||
|
||||
expect(result.failedAlbums).toEqual([]);
|
||||
expect(tree(outDir)).toEqual([
|
||||
"TRIP.json (13)/",
|
||||
`TRIP.json (13)/d.jpg -> ${linkTo("4.jpg")}`,
|
||||
"TRIP.json (13).json",
|
||||
"Trip/",
|
||||
`Trip/c.jpg -> ${linkTo("3.jpg")}`,
|
||||
"Trip.json",
|
||||
"ok/",
|
||||
"ok.json",
|
||||
"x/",
|
||||
`x/a.jpg -> ${linkTo("1.jpg")}`,
|
||||
"x.json",
|
||||
"x.json (11)/",
|
||||
`x.json (11)/b.jpg -> ${linkTo("2.jpg")}`,
|
||||
"x.json (11).json",
|
||||
]);
|
||||
expect(albumID(outDir, "x.json")).toBe(10);
|
||||
expect(albumID(outDir, "x.json (11).json")).toBe(11);
|
||||
expect(albumID(outDir, "TRIP.json (13).json")).toBe(13);
|
||||
expect(albumID(outDir, "ok.json")).toBe(14);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(readLedger(outDir).files["5"]).toMatchObject({ attempts: 2 });
|
||||
});
|
||||
|
||||
// A name over 255 bytes is refused as a file name, and an album folder's
|
||||
// name leaves room for `.json` in its JSON's. "é" takes two bytes, and a
|
||||
// name is cut between two characters.
|
||||
it("cuts an album name and a title too long for a file name short and adds the ID", async () => {
|
||||
const outDir = join(root, "backup");
|
||||
const lib = libraryOf(
|
||||
[
|
||||
{
|
||||
collection: collection(10, "L".repeat(300)),
|
||||
files: [file(1, 10, `${"é".repeat(150)}.jpg`)],
|
||||
},
|
||||
ok,
|
||||
],
|
||||
5,
|
||||
);
|
||||
|
||||
await runBackup(lib, { downloadDirectory: outDir });
|
||||
const result = await runBackup(lib, { downloadDirectory: outDir });
|
||||
|
||||
const folder = `${"L".repeat(245)} (10)`;
|
||||
const link = `${"é".repeat(123)} (1).jpg`;
|
||||
expect(Buffer.byteLength(`${folder}.json`)).toBe(255);
|
||||
expect(Buffer.byteLength(link)).toBe(254);
|
||||
expect(result.failedAlbums).toEqual([]);
|
||||
expect(tree(outDir)).toEqual([
|
||||
`${folder}/`,
|
||||
`${folder}/${link} -> ${linkTo("1.jpg")}`,
|
||||
`${folder}.json`,
|
||||
"ok/",
|
||||
"ok.json",
|
||||
]);
|
||||
expect(albumID(outDir, `${folder}.json`)).toBe(10);
|
||||
expect(albumID(outDir, "ok.json")).toBe(14);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(readLedger(outDir).files["5"]).toMatchObject({ attempts: 2 });
|
||||
});
|
||||
|
||||
// The user has put a file in collections/ with the name of the album
|
||||
// Trip's folder, so the folder cannot be made.
|
||||
it("goes on to the next album when an album's folder cannot be made, and lists it as failed", async () => {
|
||||
const outDir = join(root, "backup");
|
||||
const lib = libraryOf(
|
||||
[
|
||||
{
|
||||
collection: collection(10, "Trip"),
|
||||
files: [file(1, 10, "a.jpg")],
|
||||
},
|
||||
ok,
|
||||
],
|
||||
5,
|
||||
);
|
||||
mkdirSync(join(outDir, "collections"), { recursive: true });
|
||||
writeFileSync(join(outDir, "collections", "Trip"), "mine");
|
||||
const logged: string[] = [];
|
||||
|
||||
const result = await runBackup(lib, {
|
||||
downloadDirectory: outDir,
|
||||
onProgress: (message) => logged.push(message),
|
||||
});
|
||||
|
||||
expect(result.failedAlbums).toEqual([10]);
|
||||
expect(logged).toContainEqual(
|
||||
expect.stringMatching(/^FAILED album Trip: EEXIST: /),
|
||||
);
|
||||
expect(tree(outDir)).toEqual(["Trip", "ok/", "ok.json"]);
|
||||
expect(albumID(outDir, "ok.json")).toBe(14);
|
||||
expect(existsSync(saved(outDir, "1.jpg"))).toBe(true);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(readLedger(outDir).files["5"]).toMatchObject({ attempts: 1 });
|
||||
});
|
||||
|
||||
it("changes nothing on a second run over an unchanged account", async () => {
|
||||
const outDir = join(root, "backup");
|
||||
const lib = libraryOf([
|
||||
|
||||
Reference in New Issue
Block a user