quak get and get-thumb stop fetching collections once they find the file (closes #222)
check / check (push) Waiting to run

`quak get` and `quak get-thumb` used to wait for a full refresh, which on
an empty cache fetched the files of every collection. `freshFile` now
reads the collection list, then one collection's files at a time, and
stops at the first that holds the file. It tries first the collections
the cache says hold the file, and stores nothing. The commands fetch the
content through the new `Library.originalOf()` and
`Library.thumbnailOf()`, which take the file record itself, since the
store may not hold it.

They no longer print a refresh's progress lines. The `-q` test that
relied on `get` refreshing now runs `helper fix-missing-thumbnails`.

Model: opus-5-5
This commit is contained in:
2026-10-26 08:20:51 +00:00
parent af1491966c
commit 085f445b80
11 changed files with 299 additions and 138 deletions
+47 -36
View File
@@ -3,15 +3,16 @@
* issue #36, issue #52).
*
* The `collections`, `files`, `get`, and `get-thumb` commands must answer for
* current server state, not the local cache. `get`, `get-thumb` and `files`
* without a collection ID force a `Library.fresh()` round-trip before they
* read. The stand-in library below serves no cached record until `fresh()` has
* been awaited, so a helper that read without refreshing would come back empty
* and fail here. `collections` asks the library for the collection list alone
* current server state, not the local cache. `files` without a collection ID
* forces a `Library.fresh()` round-trip before it reads. The stand-in library
* below serves no cached record until `fresh()` has been awaited, so a helper
* that read without refreshing would come back empty and fail here.
* `collections` asks the library for the collection list alone
* (`freshCollections()`, issue #210). `files` with a collection ID asks for
* that list, then for that one collection's files (`freshFiles()`, issue
* #217). Neither may wait for a refresh, which would fetch the files of every
* collection.
* #217). `get` and `get-thumb` ask for that list, then for one collection's
* files at a time, until they find the file (issue #222). None of these may
* wait for a refresh, which would fetch the files of every collection.
*
* `collections` and `files` also list in the order the library gives them, not
* the albums/photos projection's newest-first order. The fixtures are seeded in
@@ -29,7 +30,6 @@ import {
type FreshReadLibrary,
} from "../../src/cli-read.js";
import { fileListRow } from "../../src/cli-output.js";
import type { Photo } from "../../src/library/index.js";
import type { Collection, EnteFile } from "../../src/model/types.js";
const collection = (id: number, updationTime: number): Collection => ({
@@ -66,8 +66,7 @@ const file = (
// A library that reveals its cached records only after `fresh()` has been
// awaited, and serves them in the enumeration order it was given.
// `freshCollections()` and `freshFiles()` answer in that order without a
// refresh. `photos.byID` returns a stand-in `Photo` carrying just the fileID
// the helper passes through.
// refresh.
class FakeLibrary implements FreshReadLibrary {
freshCalls = 0;
freshCollectionsCalls = 0;
@@ -106,18 +105,9 @@ class FakeLibrary implements FreshReadLibrary {
: [];
}
getFileByID(fileID: number): EnteFile | undefined {
if (!this.refreshed) return undefined;
return this.files.find((f) => f.id === fileID);
getFile(collectionID: number, fileID: number): EnteFile | undefined {
return this.listFiles(collectionID).find((f) => f.id === fileID);
}
photos = {
byID: ({ fileID }: { fileID: number }): Photo | undefined => {
if (!this.refreshed) return undefined;
if (!this.files.some((f) => f.id === fileID)) return undefined;
return { fileID } as unknown as Photo;
},
};
}
describe("CLI read helpers (issue #36 amendment, issue #52)", () => {
@@ -220,23 +210,44 @@ describe("CLI read helpers (issue #36 amendment, issue #52)", () => {
expect(rows).toBeUndefined();
});
it("freshFile refreshes first, then resolves the photo and its raw record", async () => {
const f = file(10, 1, 1_700_000_000_000_000);
const lib = new FakeLibrary([collection(1, 100)], [f]);
const resolved = await freshFile(lib, 10);
expect(lib.freshCalls).toBe(1);
expect(resolved?.photo.fileID).toBe(10);
expect(resolved?.file.metadata.title).toBe("file-10.jpg");
expect(resolved?.file.metadata.creationTime).toBe(
1_700_000_000_000_000,
// Three collections, each holding one file.
const threeCollections = (): FakeLibrary =>
new FakeLibrary(
[collection(1, 100), collection(2, 200), collection(3, 300)],
[
file(10, 1, 1_700_000_000_000_000),
file(20, 2, 1_700_000_000_000_001),
file(30, 3, 1_700_000_000_000_002),
],
);
it("freshFile reads each collection's files in the server's order until one holds the file", async () => {
const lib = threeCollections();
const found = await freshFile(lib, 20);
expect(lib.freshCalls).toBe(0);
expect(lib.freshCollectionsCalls).toBe(1);
expect(lib.freshFilesCalls).toEqual([1, 2]);
expect(found?.metadata.title).toBe("file-20.jpg");
expect(found?.metadata.creationTime).toBe(1_700_000_000_000_001);
});
it("freshFile returns undefined for an unknown file", async () => {
const lib = new FakeLibrary([collection(1, 100)], []);
const resolved = await freshFile(lib, 404);
expect(lib.freshCalls).toBe(1);
expect(resolved).toBeUndefined();
// The stand-in's cache holds what the server holds once `fresh()` has run.
it("freshFile reads first the files of the collection the cache says holds the file", async () => {
const lib = threeCollections();
await lib.fresh();
const found = await freshFile(lib, 30);
expect(lib.freshFilesCalls).toEqual([3]);
expect(found?.collectionID).toBe(3);
});
it("freshFile returns undefined for an unknown file, after reading every collection's files", async () => {
const lib = threeCollections();
const found = await freshFile(lib, 404);
expect(lib.freshCalls).toBe(0);
expect(lib.freshFilesCalls).toEqual([1, 2, 3]);
expect(found).toBeUndefined();
});
});