Keep a Photo's save path after its file leaves; refuse an empty download directory
check / check (push) Successful in 1m31s
check / check (push) Successful in 1m31s
A Photo now keeps the file its record was made from (the record projection holds one membership of each file), so savePath and isLocal still answer after a refresh removes the file. Library.open rejects an empty downloadDirectory. A backup clears leftover temp files in every date folder under its directory, not only in those of the files in its scope. placeOriginal no longer deletes what is at the save path before copying a live photo, and the tests that depended on that are removed. The TODO.md entry for issue 143 describes only the current layout. Model: opus-5-5
This commit is contained in:
+19
-53
@@ -651,6 +651,22 @@ describe("lib.backup", () => {
|
||||
lib.close();
|
||||
});
|
||||
|
||||
it("removes temp files left in a date folder no file in the backup is saved in", async () => {
|
||||
const outDir = join(root, "backup");
|
||||
// As for a file since deleted, or given another date, after a run was
|
||||
// killed while writing it.
|
||||
const otherDay = join(outDir, "2025", "2025-01", "2025-01-02");
|
||||
mkdirSync(otherDay, { recursive: true });
|
||||
const exitedPID = spawnSync(process.execPath, ["-e", ""]).pid;
|
||||
writeFileSync(join(otherDay, `.quak-${exitedPID}-abc123.tmp`), "x");
|
||||
const lib = await openLibrary(stubSource());
|
||||
|
||||
await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
expect(readdirSync(otherDay)).toEqual([]);
|
||||
lib.close();
|
||||
});
|
||||
|
||||
it("removes leftover temp files in thumbnails/ but not those of a backup still running", async () => {
|
||||
const outDir = join(root, "backup");
|
||||
const thumbnails = join(outDir, "thumbnails");
|
||||
@@ -1098,18 +1114,6 @@ describe("backup of live photos", () => {
|
||||
const open = (files: EnteFile[], bodies: Map<number, Uint8Array>) =>
|
||||
openLibrary(cdnSource(bodies), new TripClient(files));
|
||||
|
||||
// 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, DAY), { recursive: true });
|
||||
mkdirSync(join(outDir, "collections", "Trip"), { recursive: true });
|
||||
writeFileSync(saved(outDir, "500.HEIC"), livePhotoZip());
|
||||
symlinkSync(
|
||||
linkTo("500.HEIC"),
|
||||
join(outDir, "collections", "Trip", "IMG_0500.HEIC"),
|
||||
);
|
||||
};
|
||||
|
||||
const stored = [
|
||||
"2026-03-01.500.heic",
|
||||
"2026-03-01.500.json",
|
||||
@@ -1173,30 +1177,13 @@ describe("backup of live photos", () => {
|
||||
await lib.close();
|
||||
});
|
||||
|
||||
it("replaces the ZIP an earlier version stored, and its link", async () => {
|
||||
const { file: live, body } = await asLivePhoto(
|
||||
file(500, 10, "IMG_0500.HEIC"),
|
||||
);
|
||||
const outDir = join(root, "backup");
|
||||
earlierZIP(outDir);
|
||||
const lib = await open([live], new Map([[500, body]]));
|
||||
|
||||
const result = await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
expect(result).toMatchObject({ downloaded: 1, failed: 0 });
|
||||
expect(readdirSync(join(outDir, DAY)).sort()).toEqual(stored);
|
||||
expect(tree(outDir)).toEqual(linked);
|
||||
await lib.close();
|
||||
});
|
||||
|
||||
it("stores nothing for a live photo that fails its hash, and keeps what was there", async () => {
|
||||
it("stores nothing for a live photo that fails its hash", async () => {
|
||||
const { file: live, body } = await asLivePhoto(
|
||||
file(500, 10, "IMG_0500.HEIC"),
|
||||
livePhotoZip(),
|
||||
"not:the recorded hash",
|
||||
);
|
||||
const outDir = join(root, "backup");
|
||||
earlierZIP(outDir);
|
||||
const lib = await open([live], new Map([[500, body]]));
|
||||
|
||||
const result = await lib.backup({ downloadDirectory: outDir });
|
||||
@@ -1204,12 +1191,8 @@ 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, DAY))).toEqual(["2026-03-01.500.HEIC"]);
|
||||
expect(tree(outDir)).toEqual([
|
||||
"Trip/",
|
||||
`Trip/IMG_0500.HEIC -> ${linkTo("500.HEIC")}`,
|
||||
"Trip.json",
|
||||
]);
|
||||
expect(readdirSync(join(outDir, DAY))).toEqual([]);
|
||||
expect(tree(outDir)).toEqual(["Trip/", "Trip.json"]);
|
||||
await lib.close();
|
||||
});
|
||||
|
||||
@@ -1234,23 +1217,6 @@ describe("backup of live photos", () => {
|
||||
await lib.close();
|
||||
});
|
||||
|
||||
it("replaces an earlier ZIP and its link with the image and video the cache holds", async () => {
|
||||
const { file: live, body } = await asLivePhoto(
|
||||
file(500, 10, "IMG_0500.HEIC"),
|
||||
);
|
||||
const lib = await open([live], new Map([[500, body]]));
|
||||
await lib.photos.byID({ fileID: 500 })!.original();
|
||||
const outDir = join(root, "backup");
|
||||
earlierZIP(outDir);
|
||||
|
||||
const result = await lib.backup({ downloadDirectory: outDir });
|
||||
|
||||
expect(result).toMatchObject({ downloaded: 1, failed: 0 });
|
||||
expect(readdirSync(join(outDir, DAY)).sort()).toEqual(stored);
|
||||
expect(tree(outDir)).toEqual(linked);
|
||||
await lib.close();
|
||||
});
|
||||
|
||||
it.each(["missing", "empty"])(
|
||||
"fetches a live photo again when the video its JSON file names is %s",
|
||||
async (state) => {
|
||||
|
||||
@@ -379,6 +379,34 @@ describe("Photo save path, local copy, content and EXIF", () => {
|
||||
await lib.close();
|
||||
});
|
||||
|
||||
it("refuses an empty download directory", async () => {
|
||||
await expect(open({ downloadDirectory: "" })).rejects.toThrow(
|
||||
/downloadDirectory is empty/,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a photo's save path after a refresh removes its file", async () => {
|
||||
// The same account, whose album is deleted on the second refresh.
|
||||
class AlbumDeletedClient extends MockClient {
|
||||
override async collectionsSince(): Promise<CollectionsPage> {
|
||||
if (!this.served) return super.collectionsSince();
|
||||
return { collections: [], deleted: [1], cursor: 2 };
|
||||
}
|
||||
}
|
||||
const lib = await open({ client: new AlbumDeletedClient() });
|
||||
const photo = lib.photos.byID({ fileID: 1 })!;
|
||||
await photo.download();
|
||||
|
||||
await lib.fresh();
|
||||
|
||||
expect(lib.photos.byID({ fileID: 1 })).toBeUndefined();
|
||||
expect(photo.savePath).toBe(
|
||||
join(root, "backup", DAY, "2026-03-01.1.jpg"),
|
||||
);
|
||||
expect(photo.isLocal).toBe(true);
|
||||
await lib.close();
|
||||
});
|
||||
|
||||
it("has a save path, and is not local, without a content source", async () => {
|
||||
const lib = await open({ contentSource: undefined });
|
||||
const photo = lib.photos.byID({ fileID: 1 })!;
|
||||
|
||||
Reference in New Issue
Block a user