From fc06b9100b1bc0702919272a0e3db30429150534 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 1 Oct 2026 21:01:33 +0000 Subject: [PATCH] Have photo.download() write to the save path of the Photo's own file copy Photo.download() now hands the content cache the file copy the Photo holds, the one savePath and isLocal are dated from, instead of the fileID the cache then looked up in the store. A Photo held across a refresh that brings a date edited in Ente is now downloaded to the path its savePath names. A test covers that case. Model: opus-5-5 --- src/library/content.ts | 21 +++++++------- src/library/read.ts | 2 +- test/library/content-library.test.ts | 42 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/library/content.ts b/src/library/content.ts index e8436d7..396fdb8 100644 --- a/src/library/content.ts +++ b/src/library/content.ts @@ -117,8 +117,9 @@ export interface ContentOptions { export interface PhotoContent { original(fileID: number, opts?: ContentOptions): Promise; thumbnail(fileID: number, opts?: ContentOptions): Promise; - // Put the original at its save path and return it there. - download(fileID: number): Promise; + // Put the original at the save path of `file`, the copy the `Photo` holds, + // and return it there. + download(file: EnteFile): Promise; } export interface EnsureResult { @@ -531,18 +532,18 @@ export class ContentCache implements PhotoContent, ThumbnailsAPI { return this.get(fileID, "thumbnail", "on-demand", opts?.onProgress); } - // Put the original at its save path under the download directory and - // return it there. One already stored there is returned as it is; one the - // cache holds is copied from it; any other is fetched straight to the - // save path, with no copy left in the cache. - async download(fileID: number): Promise { - const file = this.getFile(fileID); - if (!file) throw new Error(`content cache: unknown file ${fileID}`); + // Put the original at the save path of `file` under the download directory + // and return it there. `file` is the copy the `Photo` holds, so the path is + // the one its `savePath` names, even after a refresh changed the date. One + // already stored there is returned as it is; one the cache holds is copied + // from it; any other is fetched straight to the save path, with no copy + // left in the cache. + async download(file: EnteFile): Promise { const root = this.downloadDirectory; const saved = storedAtSavePath(root, file) ?? (await placeOriginal(root, file, (dest) => - this.backupOriginal(fileID, dest), + this.backupOriginal(file.id, dest), )); return { ...saved, bytes: fileSize(saved.path) ?? 0 }; } diff --git a/src/library/read.ts b/src/library/read.ts index 694f2e8..ae040cb 100644 --- a/src/library/read.ts +++ b/src/library/read.ts @@ -134,7 +134,7 @@ export class Photo { // it, it is copied from there; otherwise it is fetched straight to // `savePath`. async download(): Promise { - return this.cacheOrThrow().download(this.rec.fileID); + return this.cacheOrThrow().download(this.file); } // As `original`, for the thumbnail, through the thumbnail pool. diff --git a/test/library/content-library.test.ts b/test/library/content-library.test.ts index 8fee61d..198afd9 100644 --- a/test/library/content-library.test.ts +++ b/test/library/content-library.test.ts @@ -466,6 +466,48 @@ describe("Photo save path, local copy, content and EXIF", () => { await lib.close(); }); + it("downloads a photo held across a refresh that edits its date to the save path it names", async () => { + // The same account, whose second refresh brings a date edited in Ente. + const edited = file(1, 1); + edited.updationTime = 2; + edited.pubMagicMetadata = { + editedTime: new Date(2026, 3, 15, 12).getTime() * 1000, + }; + class DateEditedClient extends MockClient { + refreshes = 0; + override async collectionsSince(): Promise { + this.refreshes++; + return { + collections: [ + { ...collection(1), updationTime: this.refreshes }, + ], + deleted: [], + cursor: this.refreshes, + }; + } + override async filesSince(): Promise { + return { + files: [this.refreshes === 1 ? file(1, 1) : edited], + deleted: [], + cursor: this.refreshes, + }; + } + } + const lib = await open({ client: new DateEditedClient() }); + const photo = lib.photos.byID({ fileID: 1 })!; + + await lib.fresh(); + expect(lib.photos.byID({ fileID: 1 })!.takenAt).toBe( + new Date(2026, 3, 15, 12).getTime(), + ); + + const saved = await photo.download(); + expect(saved.path).toBe(photo.savePath); + expect(saved.path).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 })!;