diff --git a/src/download/index.ts b/src/download/index.ts index 7729890..e27184b 100644 --- a/src/download/index.ts +++ b/src/download/index.ts @@ -361,8 +361,8 @@ const openPart = async ( // written unpacked: each part is named `destination` with the extension // replaced by its own entry's, and the two must differ ignoring case. When the // file records a hash, `:` must match it, each over that -// part's own bytes. Only then is whatever was at `destination` removed and the -// image, then the video, renamed into place; on any failure neither is stored. +// part's own bytes. Only then are the image, then the video, renamed into +// place; on any failure neither is stored. // // The ZIP is chosen by its uploader and may expand enormously, so each part is // written as it decompresses and never held, and the ZIP is refused once the @@ -486,7 +486,6 @@ const decryptLivePhoto = async ( await part.handle.sync(); await part.handle.close(); } - await rm(destination, { force: true }); await rename(image.tmpPath, path); try { await rename(video.tmpPath, videoPath); @@ -570,8 +569,7 @@ const fetchAndDecrypt = async ( }, api.getRetryOptions()); // Write `file`'s original to `outPath`. A live photo is written as its image -// and its video beside `outPath` instead, and whatever was at `outPath` is -// removed (see `decryptLivePhoto`). +// and its video beside `outPath` instead (see `decryptLivePhoto`). export const downloadFile = async ( api: ApiClient, file: EnteFile, diff --git a/src/library/content.ts b/src/library/content.ts index 213e4b8..383319b 100644 --- a/src/library/content.ts +++ b/src/library/content.ts @@ -687,7 +687,10 @@ export class ContentCache implements PhotoContent, ThumbnailsAPI { await this.touch(cached.path); return { ...cached, bytes: size, cached: true }; } - // A recorded file that has since gone re-fetches below. + // A recorded file that has since gone re-fetches below. So does a + // live photo recorded with no video: the cache opened before the + // library's records said it is a live photo, while its image and + // video had no JSON file beside them yet. known.delete(fileID); } diff --git a/test/download/download.test.ts b/test/download/download.test.ts index 8a9d612..06788fd 100644 --- a/test/download/download.test.ts +++ b/test/download/download.test.ts @@ -1883,15 +1883,6 @@ describe("downloadFile live photos", () => { expect(readdirSync(t.dir).sort()).toEqual(["f.JPG", "f.bin"]); }); - it("replaces what was at the destination", async () => { - const t = setup(livePhotoZip(), livePhoto); - writeFileSync(t.outPath, livePhotoZip()); - - await t.run(); - - expect(readdirSync(t.dir).sort()).toEqual(["f.heic", "f.mov"]); - }); - it("renames the image and then the video into place, each from its own temp file", async () => { const t = setup(livePhotoZip(), livePhoto); diff --git a/test/library/content.test.ts b/test/library/content.test.ts index b2c644a..b461742 100644 --- a/test/library/content.test.ts +++ b/test/library/content.test.ts @@ -596,6 +596,36 @@ describe("ContentCache live photos", () => { expect(second!.pathsFor(5)).toEqual({}); }); + it("fetches a live photo's image and video again when the cache opened before knowing it is a live photo and no JSON file names them", async () => { + const { file: live, body } = await asLivePhoto(file(5, "IMG_5.HEIC")); + mkdirSync(originals(), { recursive: true }); + writeFileSync(join(originals(), "5.heic"), "an image"); + writeFileSync(join(originals(), "5.mov"), "a video"); + const cache = cacheOf([live], new Map([[5, body]])); + // Opened without being told that file 5 is a live photo, the cache + // records one of the two files as its original, with no video. + await cache.open(); + + const events: string[] = []; + const result = await cache.original(5, { + onProgress: (e) => events.push(e.status), + }); + + expect(events.at(-1)).toBe("done"); + expect(result).toEqual({ + path: join(originals(), "5.heic"), + videoPath: join(originals(), "5.mov"), + bytes: IMAGE.length, + }); + expect(readFileSync(result.path)).toEqual(Buffer.from(IMAGE)); + expect(readFileSync(result.videoPath!)).toEqual(Buffer.from(VIDEO)); + expect( + JSON.parse( + readFileSync(join(originals(), "5.livephoto.json"), "utf-8"), + ), + ).toEqual({ image: "5.heic", video: "5.mov" }); + }); + it.each(["missing", "empty"])( "fetches a live photo again when the video its JSON file names is %s", async (state) => {