Save path ./photos/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.fileID.ext; download() from the cache first (closes #143) #146
+2
-2
@@ -44,8 +44,8 @@ const byNewestAlbum = (a: AlbumRecord, b: AlbumRecord): number =>
|
||||
|
||||
// A single photo. Field access mirrors `PhotoRecord`; `record()` returns the
|
||||
// underlying plain record for callers that need the IPC-safe value. `file` is
|
||||
// the file the record was made from, so the save path stays known after a
|
||||
// refresh removes the file from the library.
|
||||
// the membership the record is read from, so the save path carries the date of
|
||||
// `takenAt` and stays known after a refresh removes the file from the library.
|
||||
export class Photo {
|
||||
constructor(
|
||||
private readonly rec: PhotoRecord,
|
||||
|
||||
+18
-14
@@ -86,9 +86,9 @@ export interface LibraryChange {
|
||||
export interface DerivedRecords {
|
||||
albums: Map<number, AlbumRecord>;
|
||||
photos: Map<number, PhotoRecord>;
|
||||
// One membership of each photo's file, for its `Photo`'s save path. It
|
||||
// holds the file's key, so it stays in this process: no snapshot or change
|
||||
// carries it.
|
||||
// The membership each photo's record is read from, for its `Photo`'s save
|
||||
// path. It holds the file's key, so it stays in this process: no snapshot
|
||||
// or change carries it.
|
||||
files: Map<number, EnteFile>;
|
||||
}
|
||||
|
||||
@@ -113,10 +113,20 @@ export const takenAtOf = (file: EnteFile): number =>
|
||||
file.metadata.creationTime,
|
||||
);
|
||||
|
||||
// The membership a file's record is read from: the most recently synced, lowest
|
||||
// collection id to break ties.
|
||||
const representative = (memberships: EnteFile[]): EnteFile =>
|
||||
memberships.reduce((best, m) =>
|
||||
m.updationTime > best.updationTime ||
|
||||
(m.updationTime === best.updationTime &&
|
||||
m.collectionID < best.collectionID)
|
||||
? m
|
||||
: best,
|
||||
);
|
||||
|
||||
// Build one PhotoRecord from every membership of a file. The memberships share
|
||||
// the same underlying file, so metadata is read from a single representative
|
||||
// (the most recently synced, lowest collection id to break ties); `albumIDs`
|
||||
// gathers them all.
|
||||
// the same underlying file, so metadata is read from a single representative;
|
||||
// `albumIDs` gathers them all.
|
||||
const toPhotoRecord = (
|
||||
fileID: number,
|
||||
memberships: EnteFile[],
|
||||
@@ -124,13 +134,7 @@ const toPhotoRecord = (
|
||||
const albumIDs = memberships
|
||||
.map((m) => m.collectionID)
|
||||
.sort((a, b) => a - b);
|
||||
const rep = memberships.reduce((best, m) =>
|
||||
m.updationTime > best.updationTime ||
|
||||
(m.updationTime === best.updationTime &&
|
||||
m.collectionID < best.collectionID)
|
||||
? m
|
||||
: best,
|
||||
);
|
||||
const rep = representative(memberships);
|
||||
|
||||
const pub = rep.pubMagicMetadata ?? {};
|
||||
const priv = rep.magicMetadata ?? {};
|
||||
@@ -221,7 +225,7 @@ export const deriveRecords = (
|
||||
record.thumbnailPath = paths.thumbnailPath;
|
||||
}
|
||||
photos.set(fileID, record);
|
||||
photoFiles.set(fileID, memberships[0]!);
|
||||
photoFiles.set(fileID, representative(memberships));
|
||||
takenAtByFile.set(fileID, record.takenAt);
|
||||
}
|
||||
|
||||
|
||||
@@ -416,6 +416,50 @@ describe("Photo save path, local copy, content and EXIF", () => {
|
||||
await lib.close();
|
||||
});
|
||||
|
||||
it("dates the save path by the membership its takenAt comes from", async () => {
|
||||
// One file in two albums. The date edited in Ente has reached album 2's
|
||||
// copy, synced later, but album 1 still has an earlier edit.
|
||||
const older = file(1, 1);
|
||||
older.pubMagicMetadata = {
|
||||
editedTime: new Date(2026, 1, 1, 12).getTime() * 1000,
|
||||
};
|
||||
const newer = file(1, 2);
|
||||
newer.updationTime = 2;
|
||||
newer.pubMagicMetadata = {
|
||||
editedTime: new Date(2026, 3, 15, 12).getTime() * 1000,
|
||||
};
|
||||
const client = {
|
||||
whoami: () => ({ email: "u@example.com", userID: USER_ID }),
|
||||
collectionsSince: async (): Promise<CollectionsPage> => ({
|
||||
collections: [collection(1), collection(2)],
|
||||
deleted: [],
|
||||
cursor: 1,
|
||||
}),
|
||||
filesSince: async (args: {
|
||||
collectionID: number;
|
||||
}): Promise<FilesPage> => ({
|
||||
files: [args.collectionID === 1 ? older : newer],
|
||||
deleted: [],
|
||||
cursor: 1,
|
||||
}),
|
||||
};
|
||||
const lib = await open({ client });
|
||||
const photo = lib.photos.byID({ fileID: 1 })!;
|
||||
|
||||
expect(photo.takenAt).toBe(new Date(2026, 3, 15, 12).getTime());
|
||||
expect(photo.savePath).toBe(
|
||||
join(
|
||||
root,
|
||||
"backup",
|
||||
"2026",
|
||||
"2026-04",
|
||||
"2026-04-15",
|
||||
"2026-04-15.1.jpg",
|
||||
),
|
||||
);
|
||||
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