// How the CLI's read commands obtain current data. // // `collections`, `files`, `get`, and `get-thumb` must answer for // the account's state at the moment the command runs, not for whatever the // local cache last happened to hold (owner amendment, issue #36). `get`, // `get-thumb` and `files` without a collection ID therefore force a server // round-trip through `Library.fresh()` and only then read, so a change made // elsewhere is visible. `collections` asks the library for the collection list // alone through `Library.freshCollections()` (issue #210): a refresh would also // fetch the files of every collection, which on a large account takes long and // which `collections` does not print. `files` with a collection ID reads that // list too, then that one collection's files through `Library.freshFiles()` // (issue #217). On an empty cache, `get` and `get-thumb` read that list, then // one collection's files at a time, until they find the file (issue #222). // `helper fix-missing-thumbnails --file` does the same until it finds every // file it names (issue #223). // // `collections` and `files` with a collection ID list in the server's order. // `files` without one lists in the library's own enumeration order // (`listFiles()`). Neither command uses the `albums`/`photos` projection's // newest-first order, which re-sorts the rows. The field values still come // from each record's raw metadata via `cli-output.ts`. `files` without a // collection ID lists every file in the account once (issue #211): the library // keeps a record of a file for each collection that holds it, and only the // first collection's record is listed. import type { Collection, EnteFile } from "./model/types.js"; // The slice of `Library` these helpers read. `Library` satisfies it // structurally; a test can drive them with a stand-in that records each fresh // read and serves records in a known enumeration order. export interface FreshReadLibrary { fresh(): Promise; freshCollections(): Promise; freshFiles(collection: Collection): Promise; listCollections(): Collection[]; listFiles(collectionID: number): EnteFile[]; getFileByID(fileID: number): EnteFile | undefined; } // Every live collection, current as of one collections request, in the // server's order. export const freshCollections = ( lib: FreshReadLibrary, ): Promise => lib.freshCollections(); // The files of one collection, as the server holds them now, in the server's // order. `undefined` (not an empty list) when the collection does not exist, so // the caller can tell "no such collection" from "an empty collection". Without // a collection ID, every file in the account as of a forced refresh, each once, // as held by the first collection that holds it. export const freshFiles = async ( lib: FreshReadLibrary, collectionID?: number, ): Promise => { if (collectionID !== undefined) { const collections = await lib.freshCollections(); const collection = collections.find((c) => c.id === collectionID); return collection ? lib.freshFiles(collection) : undefined; } await lib.fresh(); const listed = new Set(); const files: EnteFile[] = []; for (const collection of lib.listCollections()) { for (const file of lib.listFiles(collection.id)) { if (listed.has(file.id)) continue; listed.add(file.id); files.push(file); } } return files; }; // One file as the server holds it now, or `undefined` when no collection holds // it. When the cache holds the account's collections, it refreshes and looks // the file up in the cache. On an empty cache, it reads the collection list, // then one collection's files at a time, and stops at the first collection // that holds the file. That search stores nothing. export const freshFile = async ( lib: FreshReadLibrary, fileID: number, ): Promise => { // Only a refresh stores collections, so a cache holding any holds them all. if (lib.listCollections().length > 0) { await lib.fresh(); return lib.getFileByID(fileID); } for (const collection of await lib.freshCollections()) { const files = await lib.freshFiles(collection); const file = files.find((f) => f.id === fileID); if (file) return file; } return undefined; }; // The most recently updated collection first, and the higher ID first on a // tie. `Library.albums.list()` lists albums in this order. const byNewestCollection = (a: Collection, b: Collection): number => b.updationTime - a.updationTime || b.id - a.id; // Each file named by `fileIDs` that the cache holds, mapped to its record and // the collection it was found in. Collections are searched in // `byNewestCollection` order, and a file is found in the first that holds it. // So `helper fix-missing-thumbnails` names a file after the same album as // `helper list-missing-thumbnails`. It reads the cache alone, so the caller // refreshes first. export const cachedFilesByID = ( lib: FreshReadLibrary, fileIDs: number[], ): Map => { const wanted = new Set(fileIDs); const found = new Map(); const collections = [...lib.listCollections()].sort(byNewestCollection); for (const collection of collections) { for (const file of lib.listFiles(collection.id)) { if (wanted.has(file.id) && !found.has(file.id)) { found.set(file.id, { file, collection }); } } } return found; }; // Each file named by `fileIDs` as the server holds it now, mapped to its record // and collection as `cachedFilesByID` maps it. When the cache holds the // account's collections, it refreshes and looks the files up in the cache. On // an empty cache, it reads the collection list, then one collection's files at // a time, and stops once it has found every file. That search stores nothing. export const freshFilesByID = async ( lib: FreshReadLibrary, fileIDs: number[], ): Promise> => { // Only a refresh stores collections, so a cache holding any holds them all. if (lib.listCollections().length > 0) { await lib.fresh(); return cachedFilesByID(lib, fileIDs); } const wanted = new Set(fileIDs); const found = new Map(); const collections = await lib.freshCollections(); for (const collection of [...collections].sort(byNewestCollection)) { if (found.size === wanted.size) break; for (const file of await lib.freshFiles(collection)) { if (wanted.has(file.id) && !found.has(file.id)) { found.set(file.id, { file, collection }); } } } return found; };