Save originals at photos/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.fileID.ext; add photo.download() (closes #143)
check / check (push) Successful in 1m25s
check / check (push) Successful in 1m25s
Each original's save path is now YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.<fileID><ext> under the library's download directory, dated by takenAt in the machine's time zone. The directory defaults to photos/ in the working directory, resolved once at open. savePath is always a string, with or without a content cache, and isLocal is true only when the original is there. photo.download() puts the original at its save path: copied from the cache when the cache holds it, fetched straight there otherwise. lib.backup() uses the same code for each file, writes each file's JSON beside it, and links collections/ to the save paths. The backup has no originals/ folder. Model: opus-5-5
This commit is contained in:
+115
-100
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* Tests for the `quak backup` logic, now built on the library API (issue #51).
|
||||
*
|
||||
* `lib.backup({ downloadDirectory })` refreshes the library, fetches each
|
||||
* pending file's original through the content cache/pools, and materialises the
|
||||
* unchanged on-disk layout:
|
||||
* `lib.backup({ downloadDirectory })` refreshes the library, puts each pending
|
||||
* file's original at its save path through the content cache/pools, and
|
||||
* materialises the on-disk layout:
|
||||
*
|
||||
* <downloadDirectory>/
|
||||
* originals/
|
||||
* <fileID>.<ext> the decrypted bytes ("present means complete")
|
||||
* <fileID>.json per-file metadata sidecar (rebuilt each run)
|
||||
* YYYY/YYYY-MM/YYYY-MM-DD/
|
||||
* YYYY-MM-DD.<fileID>.<ext> the decrypted bytes ("present means complete")
|
||||
* YYYY-MM-DD.<fileID>.json per-file metadata sidecar (rebuilt each run)
|
||||
* collections/
|
||||
* <name>/<title> symlink into ../originals (rebuilt each run)
|
||||
* <name>/<title> symlink to the original (rebuilt each run)
|
||||
* <name>.json per-collection metadata (rebuilt each run)
|
||||
* failures.json durable ledger of unresolved failures
|
||||
*
|
||||
@@ -94,6 +94,18 @@ const USER_ID = 42;
|
||||
// Decrypted-byte length each stub original writes, keyed by fileID.
|
||||
const SIZE_BY_ID: Record<number, number> = { 100: 3000, 101: 2000, 200: 1500 };
|
||||
|
||||
// Every file is taken at noon local time on 2026-03-01, in microseconds as Ente
|
||||
// stores times, so the machine's time zone cannot move it to another day. Its
|
||||
// original is saved in the folder `DAY`, named `2026-03-01.<fileID>.<ext>`.
|
||||
const TAKEN = new Date(2026, 2, 1, 12).getTime() * 1000;
|
||||
const DAY = join("2026", "2026-03", "2026-03-01");
|
||||
|
||||
// Where the backup in `outDir` saves `name` (`<fileID>.<ext>`), and the target
|
||||
// of an album folder's symlink to it.
|
||||
const saved = (outDir: string, name: string): string =>
|
||||
join(outDir, DAY, `2026-03-01.${name}`);
|
||||
const linkTo = (name: string): string => `../../${DAY}/2026-03-01.${name}`;
|
||||
|
||||
const collection = (id: number, name: string): Collection => ({
|
||||
id,
|
||||
ownerID: USER_ID,
|
||||
@@ -112,7 +124,7 @@ const file = (id: number, collectionID: number, title: string): EnteFile => ({
|
||||
metadata: {
|
||||
title,
|
||||
fileType: "image",
|
||||
creationTime: 1,
|
||||
creationTime: TAKEN,
|
||||
modificationTime: 1,
|
||||
},
|
||||
file: { decryptionHeader: "aGVhZGVy" },
|
||||
@@ -253,7 +265,11 @@ describe("lib.backup", () => {
|
||||
it("throws before any network when no downloadDirectory is given", async () => {
|
||||
const source = stubSource();
|
||||
const lib = await openLibrary(source);
|
||||
await expect(lib.backup()).rejects.toThrow(/downloadDirectory/i);
|
||||
// The library always has a download directory, so only an empty one
|
||||
// given to backup() reaches runBackup as none.
|
||||
await expect(lib.backup({ downloadDirectory: "" })).rejects.toThrow(
|
||||
/downloadDirectory/i,
|
||||
);
|
||||
expect(source.originalCalls).toBe(0);
|
||||
lib.close();
|
||||
});
|
||||
@@ -271,28 +287,22 @@ describe("lib.backup", () => {
|
||||
expect(result.failed).toBe(0);
|
||||
expect(result.errors).toEqual([]);
|
||||
|
||||
// Originals under originals/<fileID>.<ext>.
|
||||
expect(readFileSync(join(outDir, "originals", "100.jpg")).length).toBe(
|
||||
3000,
|
||||
);
|
||||
expect(readFileSync(join(outDir, "originals", "101.jpg")).length).toBe(
|
||||
2000,
|
||||
);
|
||||
expect(readFileSync(join(outDir, "originals", "200.png")).length).toBe(
|
||||
1500,
|
||||
);
|
||||
// Originals at YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.<fileID>.<ext>.
|
||||
expect(readFileSync(saved(outDir, "100.jpg")).length).toBe(3000);
|
||||
expect(readFileSync(saved(outDir, "101.jpg")).length).toBe(2000);
|
||||
expect(readFileSync(saved(outDir, "200.png")).length).toBe(1500);
|
||||
|
||||
// Per-file metadata sidecar.
|
||||
const sidecar = JSON.parse(
|
||||
readFileSync(join(outDir, "originals", "100.json"), "utf-8"),
|
||||
readFileSync(saved(outDir, "100.json"), "utf-8"),
|
||||
);
|
||||
expect(sidecar.id).toBe(100);
|
||||
expect(sidecar.metadata.title).toBe("beach.jpg");
|
||||
|
||||
// Collection dirs contain symlinks into ../originals.
|
||||
// Collection dirs contain symlinks into the date folders.
|
||||
const beach = join(outDir, "collections", "Vacation", "beach.jpg");
|
||||
expect(lstatSync(beach).isSymbolicLink()).toBe(true);
|
||||
expect(readlinkSync(beach)).toContain("originals");
|
||||
expect(readlinkSync(beach)).toContain(DAY);
|
||||
expect(readFileSync(beach).length).toBe(3000);
|
||||
|
||||
// Per-collection metadata JSON.
|
||||
@@ -316,7 +326,7 @@ describe("lib.backup", () => {
|
||||
|
||||
expect(result.failed).toBe(0);
|
||||
// The title has no usable extension, so the original is `.bin`.
|
||||
expect(existsSync(join(outDir, "originals", "300.bin"))).toBe(true);
|
||||
expect(existsSync(saved(outDir, "300.bin"))).toBe(true);
|
||||
const link = join(
|
||||
outDir,
|
||||
"collections",
|
||||
@@ -366,9 +376,9 @@ describe("lib.backup", () => {
|
||||
expect(result.errors[0]!.title).toBe("sunset.jpg");
|
||||
|
||||
// The two good files are on disk; the failed one is not.
|
||||
expect(existsSync(join(outDir, "originals", "100.jpg"))).toBe(true);
|
||||
expect(existsSync(join(outDir, "originals", "200.png"))).toBe(true);
|
||||
expect(existsSync(join(outDir, "originals", "101.jpg"))).toBe(false);
|
||||
expect(existsSync(saved(outDir, "100.jpg"))).toBe(true);
|
||||
expect(existsSync(saved(outDir, "200.png"))).toBe(true);
|
||||
expect(existsSync(saved(outDir, "101.jpg"))).toBe(false);
|
||||
expect(
|
||||
existsSync(join(outDir, "collections", "Vacation", "sunset.jpg")),
|
||||
).toBe(false);
|
||||
@@ -403,7 +413,7 @@ describe("lib.backup", () => {
|
||||
const r3 = await lib.backup({ downloadDirectory: outDir });
|
||||
expect(r3.failed).toBe(0);
|
||||
expect(r3.skipped).toBe(2);
|
||||
expect(existsSync(join(outDir, "originals", "101.jpg"))).toBe(true);
|
||||
expect(existsSync(saved(outDir, "101.jpg"))).toBe(true);
|
||||
// A ledger with no remaining failures is removed.
|
||||
expect(existsSync(join(outDir, "failures.json"))).toBe(false);
|
||||
lib.close();
|
||||
@@ -423,8 +433,8 @@ describe("lib.backup", () => {
|
||||
const result = await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
// Every original still downloads despite the symlink failure.
|
||||
expect(existsSync(join(outDir, "originals", "100.jpg"))).toBe(true);
|
||||
expect(existsSync(join(outDir, "originals", "200.png"))).toBe(true);
|
||||
expect(existsSync(saved(outDir, "100.jpg"))).toBe(true);
|
||||
expect(existsSync(saved(outDir, "200.png"))).toBe(true);
|
||||
// The other symlinks are still built.
|
||||
expect(
|
||||
lstatSync(
|
||||
@@ -454,7 +464,7 @@ describe("lib.backup", () => {
|
||||
await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
// Corrupt a sidecar and delete a symlink between runs.
|
||||
writeFileSync(join(outDir, "originals", "100.json"), "not json");
|
||||
writeFileSync(saved(outDir, "100.json"), "not json");
|
||||
rmSync(join(outDir, "collections", "Vacation", "beach.jpg"));
|
||||
|
||||
const result = await lib.backup({ downloadDirectory: outDir });
|
||||
@@ -462,7 +472,7 @@ describe("lib.backup", () => {
|
||||
|
||||
// The derived views are repaired from the model.
|
||||
const sidecar = JSON.parse(
|
||||
readFileSync(join(outDir, "originals", "100.json"), "utf-8"),
|
||||
readFileSync(saved(outDir, "100.json"), "utf-8"),
|
||||
);
|
||||
expect(sidecar.metadata.title).toBe("beach.jpg");
|
||||
expect(
|
||||
@@ -485,8 +495,8 @@ describe("lib.backup", () => {
|
||||
|
||||
expect(result.totalFiles).toBe(1);
|
||||
expect(result.downloaded).toBe(1);
|
||||
expect(existsSync(join(outDir, "originals", "200.png"))).toBe(true);
|
||||
expect(existsSync(join(outDir, "originals", "100.jpg"))).toBe(false);
|
||||
expect(existsSync(saved(outDir, "200.png"))).toBe(true);
|
||||
expect(existsSync(saved(outDir, "100.jpg"))).toBe(false);
|
||||
expect(existsSync(join(outDir, "collections", "Work.json"))).toBe(true);
|
||||
expect(existsSync(join(outDir, "collections", "Vacation.json"))).toBe(
|
||||
false,
|
||||
@@ -530,7 +540,7 @@ describe("lib.backup", () => {
|
||||
|
||||
expect(result.totalFiles).toBe(1);
|
||||
expect(result.failed).toBe(0);
|
||||
expect(existsSync(join(outDir, "originals", "200.png"))).toBe(true);
|
||||
expect(existsSync(saved(outDir, "200.png"))).toBe(true);
|
||||
expect(existsSync(join(outDir, "failures.json"))).toBe(false);
|
||||
lib.close();
|
||||
});
|
||||
@@ -572,7 +582,7 @@ describe("lib.backup", () => {
|
||||
it("fetches each original once and writes it only into the backup", async () => {
|
||||
// A backup of a 500 GB account must write 500 GB, not a copy in the
|
||||
// cache as well: an original fetched for the backup goes straight
|
||||
// into its originals/, and the cache records it there.
|
||||
// to its save path, and the cache records it there.
|
||||
const source = stubSource();
|
||||
const lib = await openLibrary(source);
|
||||
const outDir = join(root, "backup");
|
||||
@@ -582,14 +592,18 @@ describe("lib.backup", () => {
|
||||
expect(result.downloaded).toBe(3);
|
||||
expect(source.originalCalls).toBe(3);
|
||||
expect(readdirSync(join(root, "cache", "originals"))).toEqual([]);
|
||||
const stored = readdirSync(join(outDir, "originals")).filter(
|
||||
const stored = readdirSync(join(outDir, DAY)).filter(
|
||||
(name) => !name.endsWith(".json"),
|
||||
);
|
||||
expect(stored.sort()).toEqual(["100.jpg", "101.jpg", "200.png"]);
|
||||
expect(stored.sort()).toEqual([
|
||||
"2026-03-01.100.jpg",
|
||||
"2026-03-01.101.jpg",
|
||||
"2026-03-01.200.png",
|
||||
]);
|
||||
// The cache counts the backup's copy as present: reading the
|
||||
// original afterwards fetches nothing and answers with that copy.
|
||||
const read = await lib.photos.byID({ fileID: 100 })!.original();
|
||||
expect(read.path).toBe(join(outDir, "originals", "100.jpg"));
|
||||
expect(read.path).toBe(saved(outDir, "100.jpg"));
|
||||
expect(source.originalCalls).toBe(3);
|
||||
await lib.close();
|
||||
});
|
||||
@@ -597,8 +611,8 @@ describe("lib.backup", () => {
|
||||
it("fsyncs an original copied from the cache before the rename and its directory after", async () => {
|
||||
const lib = await openLibrary(stubSource());
|
||||
const outDir = join(root, "backup");
|
||||
const originals = join(outDir, "originals");
|
||||
const dest = join(originals, "100.jpg");
|
||||
const day = join(outDir, DAY);
|
||||
const dest = saved(outDir, "100.jpg");
|
||||
// Only an original already in the cache is copied into the backup;
|
||||
// one fetched for the backup is written there by the download writer.
|
||||
await lib.photos.byID({ fileID: 100 })!.original();
|
||||
@@ -609,29 +623,29 @@ describe("lib.backup", () => {
|
||||
const at = fsEvents.indexOf(`rename:${dest}`);
|
||||
expect(at).toBeGreaterThan(0);
|
||||
expect(fsEvents[at - 1]).toMatch(
|
||||
/^sync:.*\/\.quak-backup-100\.jpg-\d+-[0-9a-z]*\.tmp$/,
|
||||
/^sync:.*\/\.quak-backup-2026-03-01\.100\.jpg-\d+-[0-9a-z]*\.tmp$/,
|
||||
);
|
||||
expect(fsEvents[at + 1]).toBe(`sync:${originals}`);
|
||||
expect(fsEvents[at + 1]).toBe(`sync:${day}`);
|
||||
lib.close();
|
||||
});
|
||||
|
||||
it("removes temp files left by a killed backup but not those of one still running", async () => {
|
||||
const outDir = join(root, "backup");
|
||||
const originals = join(outDir, "originals");
|
||||
mkdirSync(originals, { recursive: true });
|
||||
const day = join(outDir, DAY);
|
||||
mkdirSync(day, { recursive: true });
|
||||
// A child that has already exited: its process ID is not running.
|
||||
const exitedPID = spawnSync(process.execPath, ["-e", ""]).pid;
|
||||
const leftover = `.quak-backup-100.jpg-${exitedPID}-abc123.tmp`;
|
||||
const leftover = `.quak-backup-2026-03-01.100.jpg-${exitedPID}-abc123.tmp`;
|
||||
// This test's own process stands in for a backup running at the same
|
||||
// time.
|
||||
const inProgress = `.quak-backup-101.jpg-${process.pid}-def456.tmp`;
|
||||
writeFileSync(join(originals, leftover), "partial");
|
||||
writeFileSync(join(originals, inProgress), "partial");
|
||||
const inProgress = `.quak-backup-2026-03-01.101.jpg-${process.pid}-def456.tmp`;
|
||||
writeFileSync(join(day, leftover), "partial");
|
||||
writeFileSync(join(day, inProgress), "partial");
|
||||
const lib = await openLibrary(stubSource());
|
||||
|
||||
await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
const names = readdirSync(originals);
|
||||
const names = readdirSync(day);
|
||||
expect(names).not.toContain(leftover);
|
||||
expect(names).toContain(inProgress);
|
||||
lib.close();
|
||||
@@ -709,7 +723,7 @@ describe("the refresh before a backup", () => {
|
||||
const result = await backup;
|
||||
|
||||
expect(result.totalFiles).toBe(4);
|
||||
expect(existsSync(join(outDir, "originals", "300.jpg"))).toBe(true);
|
||||
expect(existsSync(saved(outDir, "300.jpg"))).toBe(true);
|
||||
await lib.close();
|
||||
});
|
||||
|
||||
@@ -728,7 +742,7 @@ describe("the refresh before a backup", () => {
|
||||
|
||||
expect(source.originalCalls).toBe(0);
|
||||
expect(readFileSync(ledgerPath, "utf-8")).toBe(ledgerBefore);
|
||||
expect(existsSync(join(outDir, "originals"))).toBe(false);
|
||||
expect(existsSync(join(outDir, DAY))).toBe(false);
|
||||
await lib.close();
|
||||
});
|
||||
|
||||
@@ -819,13 +833,13 @@ describe("backup album folders", () => {
|
||||
expect(result.failed).toBe(0);
|
||||
expect(tree(outDir)).toEqual([
|
||||
"Trip (10)/",
|
||||
"Trip (10)/IMG_0001 (1).JPG -> ../../originals/1.JPG",
|
||||
"Trip (10)/IMG_0001 (2).JPG -> ../../originals/2.JPG",
|
||||
"Trip (10)/img_0001 (4).jpg -> ../../originals/4.jpg",
|
||||
"Trip (10)/other.jpg -> ../../originals/3.jpg",
|
||||
`Trip (10)/IMG_0001 (1).JPG -> ${linkTo("1.JPG")}`,
|
||||
`Trip (10)/IMG_0001 (2).JPG -> ${linkTo("2.JPG")}`,
|
||||
`Trip (10)/img_0001 (4).jpg -> ${linkTo("4.jpg")}`,
|
||||
`Trip (10)/other.jpg -> ${linkTo("3.jpg")}`,
|
||||
"Trip (10).json",
|
||||
"Trip (11)/",
|
||||
"Trip (11)/other.jpg -> ../../originals/3.jpg",
|
||||
`Trip (11)/other.jpg -> ${linkTo("3.jpg")}`,
|
||||
"Trip (11).json",
|
||||
]);
|
||||
expect(albumID(outDir, "Trip (10).json")).toBe(10);
|
||||
@@ -858,14 +872,14 @@ describe("backup album folders", () => {
|
||||
expect(result.failed).toBe(0);
|
||||
expect(tree(outDir)).toEqual([
|
||||
"Trip (10)/",
|
||||
"Trip (10)/IMG (6) (5).JPG -> ../../originals/5.JPG",
|
||||
"Trip (10)/IMG (6).JPG -> ../../originals/6.JPG",
|
||||
"Trip (10)/IMG (7).JPG -> ../../originals/7.JPG",
|
||||
`Trip (10)/IMG (6) (5).JPG -> ${linkTo("5.JPG")}`,
|
||||
`Trip (10)/IMG (6).JPG -> ${linkTo("6.JPG")}`,
|
||||
`Trip (10)/IMG (7).JPG -> ${linkTo("7.JPG")}`,
|
||||
"Trip (10).json",
|
||||
"Trip (11)/",
|
||||
"Trip (11)/a.jpg -> ../../originals/8.jpg",
|
||||
`Trip (11)/a.jpg -> ${linkTo("8.jpg")}`,
|
||||
"Trip (11) (12)/",
|
||||
"Trip (11) (12)/b.jpg -> ../../originals/9.jpg",
|
||||
`Trip (11) (12)/b.jpg -> ${linkTo("9.jpg")}`,
|
||||
"Trip (11) (12).json",
|
||||
"Trip (11).json",
|
||||
]);
|
||||
@@ -931,13 +945,13 @@ describe("backup album folders", () => {
|
||||
expect(scoped.failed).toBe(0);
|
||||
expect(before).toEqual([
|
||||
"Trip (10)/",
|
||||
"Trip (10)/a.jpg -> ../../originals/1.jpg",
|
||||
`Trip (10)/a.jpg -> ${linkTo("1.jpg")}`,
|
||||
"Trip (10).json",
|
||||
"Work/",
|
||||
"Work/c.jpg -> ../../originals/3.jpg",
|
||||
`Work/c.jpg -> ${linkTo("3.jpg")}`,
|
||||
"Work.json",
|
||||
"trip (11)/",
|
||||
"trip (11)/b.jpg -> ../../originals/2.jpg",
|
||||
`trip (11)/b.jpg -> ${linkTo("2.jpg")}`,
|
||||
"trip (11).json",
|
||||
]);
|
||||
expect(tree(outDir)).toEqual(before);
|
||||
@@ -990,13 +1004,13 @@ describe("backup album folders", () => {
|
||||
"Mine/",
|
||||
"Mine/keep.txt",
|
||||
"Office/",
|
||||
"Office/a.jpg -> ../../originals/5.jpg",
|
||||
`Office/a.jpg -> ${linkTo("5.jpg")}`,
|
||||
"Office.json",
|
||||
"Trip/",
|
||||
"Trip/IMG_0001.JPG -> ../../originals/1.JPG",
|
||||
`Trip/IMG_0001.JPG -> ${linkTo("1.JPG")}`,
|
||||
"Trip/mine -> ../elsewhere",
|
||||
"Trip/notes.txt",
|
||||
"Trip/other.jpg -> ../../originals/3.jpg",
|
||||
`Trip/other.jpg -> ${linkTo("3.jpg")}`,
|
||||
"Trip.json",
|
||||
"Work/",
|
||||
"Work/keep.txt",
|
||||
@@ -1005,7 +1019,7 @@ describe("backup album folders", () => {
|
||||
});
|
||||
|
||||
// One album backed up, then a folder the user made beside it holding a
|
||||
// symlink into originals/, with `json` (if given) as its sibling JSON.
|
||||
// symlink to an original, with `json` (if given) as its sibling JSON.
|
||||
const backupWithUserFolder = async (
|
||||
json: string | undefined,
|
||||
): Promise<{ outDir: string; failed: number }> => {
|
||||
@@ -1019,10 +1033,7 @@ describe("backup album folders", () => {
|
||||
await runBackup(lib, { downloadDirectory: outDir });
|
||||
const collectionsDir = join(outDir, "collections");
|
||||
mkdirSync(join(collectionsDir, "Mine"));
|
||||
symlinkSync(
|
||||
"../../originals/1.jpg",
|
||||
join(collectionsDir, "Mine", "a.jpg"),
|
||||
);
|
||||
symlinkSync(linkTo("1.jpg"), join(collectionsDir, "Mine", "a.jpg"));
|
||||
if (json !== undefined) {
|
||||
writeFileSync(join(collectionsDir, "Mine.json"), json);
|
||||
}
|
||||
@@ -1036,9 +1047,9 @@ describe("backup album folders", () => {
|
||||
expect(failed).toBe(0);
|
||||
expect(tree(outDir)).toEqual([
|
||||
"Mine/",
|
||||
"Mine/a.jpg -> ../../originals/1.jpg",
|
||||
`Mine/a.jpg -> ${linkTo("1.jpg")}`,
|
||||
"Trip/",
|
||||
"Trip/a.jpg -> ../../originals/1.jpg",
|
||||
`Trip/a.jpg -> ${linkTo("1.jpg")}`,
|
||||
"Trip.json",
|
||||
]);
|
||||
});
|
||||
@@ -1050,10 +1061,10 @@ describe("backup album folders", () => {
|
||||
expect(failed).toBe(0);
|
||||
expect(tree(outDir)).toEqual([
|
||||
"Mine/",
|
||||
"Mine/a.jpg -> ../../originals/1.jpg",
|
||||
`Mine/a.jpg -> ${linkTo("1.jpg")}`,
|
||||
"Mine.json",
|
||||
"Trip/",
|
||||
"Trip/a.jpg -> ../../originals/1.jpg",
|
||||
`Trip/a.jpg -> ${linkTo("1.jpg")}`,
|
||||
"Trip.json",
|
||||
]);
|
||||
expect(
|
||||
@@ -1090,20 +1101,25 @@ describe("backup of live photos", () => {
|
||||
// What an earlier version stored for live photo 500: the ZIP under the
|
||||
// image's name, and its link.
|
||||
const earlierZIP = (outDir: string): void => {
|
||||
mkdirSync(join(outDir, "originals"), { recursive: true });
|
||||
mkdirSync(join(outDir, DAY), { recursive: true });
|
||||
mkdirSync(join(outDir, "collections", "Trip"), { recursive: true });
|
||||
writeFileSync(join(outDir, "originals", "500.HEIC"), livePhotoZip());
|
||||
writeFileSync(saved(outDir, "500.HEIC"), livePhotoZip());
|
||||
symlinkSync(
|
||||
"../../originals/500.HEIC",
|
||||
linkTo("500.HEIC"),
|
||||
join(outDir, "collections", "Trip", "IMG_0500.HEIC"),
|
||||
);
|
||||
};
|
||||
|
||||
const stored = ["500.heic", "500.json", "500.livephoto.json", "500.mov"];
|
||||
const stored = [
|
||||
"2026-03-01.500.heic",
|
||||
"2026-03-01.500.json",
|
||||
"2026-03-01.500.livephoto.json",
|
||||
"2026-03-01.500.mov",
|
||||
];
|
||||
const linked = [
|
||||
"Trip/",
|
||||
"Trip/IMG_0500.heic -> ../../originals/500.heic",
|
||||
"Trip/IMG_0500.mov -> ../../originals/500.mov",
|
||||
`Trip/IMG_0500.heic -> ${linkTo("500.heic")}`,
|
||||
`Trip/IMG_0500.mov -> ${linkTo("500.mov")}`,
|
||||
"Trip.json",
|
||||
];
|
||||
|
||||
@@ -1113,16 +1129,15 @@ describe("backup of live photos", () => {
|
||||
);
|
||||
const lib = await open([live], new Map([[500, body]]));
|
||||
const outDir = join(root, "backup");
|
||||
const originals = join(outDir, "originals");
|
||||
|
||||
const result = await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
expect(result).toMatchObject({ downloaded: 1, failed: 0 });
|
||||
expect(readdirSync(originals).sort()).toEqual(stored);
|
||||
expect(readFileSync(join(originals, "500.heic"))).toEqual(
|
||||
expect(readdirSync(join(outDir, DAY)).sort()).toEqual(stored);
|
||||
expect(readFileSync(saved(outDir, "500.heic"))).toEqual(
|
||||
Buffer.from(IMAGE),
|
||||
);
|
||||
expect(readFileSync(join(originals, "500.mov"))).toEqual(
|
||||
expect(readFileSync(saved(outDir, "500.mov"))).toEqual(
|
||||
Buffer.from(VIDEO),
|
||||
);
|
||||
expect(tree(outDir)).toEqual(linked);
|
||||
@@ -1149,10 +1164,10 @@ describe("backup of live photos", () => {
|
||||
|
||||
expect(tree(outDir)).toEqual([
|
||||
"Trip/",
|
||||
"Trip/IMG_0001 (500).heic -> ../../originals/500.heic",
|
||||
"Trip/IMG_0001 (500).mov -> ../../originals/500.mov",
|
||||
"Trip/IMG_0001 (501).heic -> ../../originals/501.heic",
|
||||
"Trip/IMG_0001 (501).mov -> ../../originals/501.mov",
|
||||
`Trip/IMG_0001 (500).heic -> ${linkTo("500.heic")}`,
|
||||
`Trip/IMG_0001 (500).mov -> ${linkTo("500.mov")}`,
|
||||
`Trip/IMG_0001 (501).heic -> ${linkTo("501.heic")}`,
|
||||
`Trip/IMG_0001 (501).mov -> ${linkTo("501.mov")}`,
|
||||
"Trip.json",
|
||||
]);
|
||||
await lib.close();
|
||||
@@ -1169,7 +1184,7 @@ describe("backup of live photos", () => {
|
||||
const result = await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
expect(result).toMatchObject({ downloaded: 1, failed: 0 });
|
||||
expect(readdirSync(join(outDir, "originals")).sort()).toEqual(stored);
|
||||
expect(readdirSync(join(outDir, DAY)).sort()).toEqual(stored);
|
||||
expect(tree(outDir)).toEqual(linked);
|
||||
await lib.close();
|
||||
});
|
||||
@@ -1189,10 +1204,10 @@ describe("backup of live photos", () => {
|
||||
expect(result).toMatchObject({ downloaded: 0, failed: 1 });
|
||||
expect(result.errors.map((e) => e.fileID)).toEqual([500]);
|
||||
expect(Object.keys(readLedger(outDir).files)).toEqual(["500"]);
|
||||
expect(readdirSync(join(outDir, "originals"))).toEqual(["500.HEIC"]);
|
||||
expect(readdirSync(join(outDir, DAY))).toEqual(["2026-03-01.500.HEIC"]);
|
||||
expect(tree(outDir)).toEqual([
|
||||
"Trip/",
|
||||
"Trip/IMG_0500.HEIC -> ../../originals/500.HEIC",
|
||||
`Trip/IMG_0500.HEIC -> ${linkTo("500.HEIC")}`,
|
||||
"Trip.json",
|
||||
]);
|
||||
await lib.close();
|
||||
@@ -1209,8 +1224,8 @@ describe("backup of live photos", () => {
|
||||
const result = await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
expect(result).toMatchObject({ downloaded: 1, failed: 0 });
|
||||
expect(readdirSync(join(outDir, "originals")).sort()).toEqual(stored);
|
||||
expect(readFileSync(join(outDir, "originals", "500.mov"))).toEqual(
|
||||
expect(readdirSync(join(outDir, DAY)).sort()).toEqual(stored);
|
||||
expect(readFileSync(saved(outDir, "500.mov"))).toEqual(
|
||||
Buffer.from(VIDEO),
|
||||
);
|
||||
expect(tree(outDir)).toEqual(linked);
|
||||
@@ -1231,7 +1246,7 @@ describe("backup of live photos", () => {
|
||||
const result = await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
expect(result).toMatchObject({ downloaded: 1, failed: 0 });
|
||||
expect(readdirSync(join(outDir, "originals")).sort()).toEqual(stored);
|
||||
expect(readdirSync(join(outDir, DAY)).sort()).toEqual(stored);
|
||||
expect(tree(outDir)).toEqual(linked);
|
||||
await lib.close();
|
||||
});
|
||||
@@ -1245,7 +1260,7 @@ describe("backup of live photos", () => {
|
||||
const lib = await open([live], new Map([[500, body]]));
|
||||
const outDir = join(root, "backup");
|
||||
await lib.backup({ downloadDirectory: outDir });
|
||||
const video = join(outDir, "originals", "500.mov");
|
||||
const video = saved(outDir, "500.mov");
|
||||
if (state === "missing") rmSync(video);
|
||||
else writeFileSync(video, "");
|
||||
|
||||
@@ -1271,7 +1286,7 @@ describe("backup of live photos", () => {
|
||||
const lib = await open([live], new Map([[500, body]]));
|
||||
const outDir = join(root, "backup");
|
||||
await lib.backup({ downloadDirectory: outDir });
|
||||
const image = join(outDir, "originals", "500.heic");
|
||||
const image = saved(outDir, "500.heic");
|
||||
if (state === "missing") rmSync(image);
|
||||
else writeFileSync(image, "");
|
||||
|
||||
@@ -1310,8 +1325,8 @@ describe("backup of live photos", () => {
|
||||
const read = await reader.photos.byID({ fileID: 500 })!.original();
|
||||
|
||||
expect(read).toEqual({
|
||||
path: join(outDir, "originals", "500.heic"),
|
||||
videoPath: join(outDir, "originals", "500.mov"),
|
||||
path: saved(outDir, "500.heic"),
|
||||
videoPath: saved(outDir, "500.mov"),
|
||||
bytes: IMAGE.length,
|
||||
});
|
||||
await reader.close();
|
||||
|
||||
@@ -731,7 +731,7 @@ describe("backup", () => {
|
||||
expect(code).toBe(1);
|
||||
expect(runText).toBe("quak: HTTP 401 from server\n");
|
||||
expect(stderr.text).toBe("Starting backup...\nRefreshing library...\n");
|
||||
expect(existsSync(join(dir, "originals"))).toBe(false);
|
||||
expect(existsSync(dir)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user