Have photo.download() write to the save path of the Photo's own file copy
check / check (push) Successful in 4m9s

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
This commit is contained in:
2026-10-01 21:01:33 +00:00
parent 8992c7c5a9
commit fc06b9100b
3 changed files with 54 additions and 11 deletions
+11 -10
View File
@@ -117,8 +117,9 @@ export interface ContentOptions {
export interface PhotoContent { export interface PhotoContent {
original(fileID: number, opts?: ContentOptions): Promise<ContentResult>; original(fileID: number, opts?: ContentOptions): Promise<ContentResult>;
thumbnail(fileID: number, opts?: ContentOptions): Promise<ContentResult>; thumbnail(fileID: number, opts?: ContentOptions): Promise<ContentResult>;
// Put the original at its save path and return it there. // Put the original at the save path of `file`, the copy the `Photo` holds,
download(fileID: number): Promise<ContentResult>; // and return it there.
download(file: EnteFile): Promise<ContentResult>;
} }
export interface EnsureResult { export interface EnsureResult {
@@ -531,18 +532,18 @@ export class ContentCache implements PhotoContent, ThumbnailsAPI {
return this.get(fileID, "thumbnail", "on-demand", opts?.onProgress); return this.get(fileID, "thumbnail", "on-demand", opts?.onProgress);
} }
// Put the original at its save path under the download directory and // Put the original at the save path of `file` under the download directory
// return it there. One already stored there is returned as it is; one the // and return it there. `file` is the copy the `Photo` holds, so the path is
// cache holds is copied from it; any other is fetched straight to the // the one its `savePath` names, even after a refresh changed the date. One
// save path, with no copy left in the cache. // already stored there is returned as it is; one the cache holds is copied
async download(fileID: number): Promise<ContentResult> { // from it; any other is fetched straight to the save path, with no copy
const file = this.getFile(fileID); // left in the cache.
if (!file) throw new Error(`content cache: unknown file ${fileID}`); async download(file: EnteFile): Promise<ContentResult> {
const root = this.downloadDirectory; const root = this.downloadDirectory;
const saved = const saved =
storedAtSavePath(root, file) ?? storedAtSavePath(root, file) ??
(await placeOriginal(root, file, (dest) => (await placeOriginal(root, file, (dest) =>
this.backupOriginal(fileID, dest), this.backupOriginal(file.id, dest),
)); ));
return { ...saved, bytes: fileSize(saved.path) ?? 0 }; return { ...saved, bytes: fileSize(saved.path) ?? 0 };
} }
+1 -1
View File
@@ -134,7 +134,7 @@ export class Photo {
// it, it is copied from there; otherwise it is fetched straight to // it, it is copied from there; otherwise it is fetched straight to
// `savePath`. // `savePath`.
async download(): Promise<ContentResult> { async download(): Promise<ContentResult> {
return this.cacheOrThrow().download(this.rec.fileID); return this.cacheOrThrow().download(this.file);
} }
// As `original`, for the thumbnail, through the thumbnail pool. // As `original`, for the thumbnail, through the thumbnail pool.
+42
View File
@@ -466,6 +466,48 @@ describe("Photo save path, local copy, content and EXIF", () => {
await lib.close(); 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<CollectionsPage> {
this.refreshes++;
return {
collections: [
{ ...collection(1), updationTime: this.refreshes },
],
deleted: [],
cursor: this.refreshes,
};
}
override async filesSince(): Promise<FilesPage> {
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 () => { it("has a save path, and is not local, without a content source", async () => {
const lib = await open({ contentSource: undefined }); const lib = await open({ contentSource: undefined });
const photo = lib.photos.byID({ fileID: 1 })!; const photo = lib.photos.byID({ fileID: 1 })!;