Save path ./photos/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.fileID.ext; download() from the cache first (closes #143) #146

Merged
clawbot merged 5 commits from issue-143-save-path-layout into next 2026-10-01 23:20:58 +02:00
3 changed files with 54 additions and 11 deletions
Showing only changes of commit fc06b9100b - Show all commits
+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 })!;