Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fde6f19c86 |
@@ -687,21 +687,22 @@ answer, so they report current account state rather than whatever the cache last
|
||||
held. `collections` asks the server for the collection list alone, in one
|
||||
request, and neither fetches any collection's files nor changes the cache.
|
||||
`files` with a collection ID asks for the same list, then for that one
|
||||
collection's files, and does not change the cache either. `get` and `get-thumb`
|
||||
ask for the same list, then for one collection's files at a time, until they
|
||||
find the file. They ask first for the collections the cache says hold the file.
|
||||
They store nothing they read. The original or thumbnail they fetch is kept in
|
||||
the cache. The others wait for a refresh, and so does `files` without a
|
||||
collection ID. With an empty cache, as on a first run, that refresh fetches the
|
||||
files of every collection, one collection after another, and stores nothing
|
||||
until it has them all, which on a large account takes a while. Meanwhile the
|
||||
command logs a line on stderr for each collection whose files it has fetched,
|
||||
such as `Fetched the files of 3 of 120 collections`, with `--json` too; `backup`
|
||||
does the same while the refresh it starts with runs. stdout holds only the
|
||||
command's output. If that round-trip fails, the command prints the error on one
|
||||
line and exits 1, or 3 when the server no longer accepts the saved session (see
|
||||
"Session handling"). An album or file quak cannot decrypt does not fail it; the
|
||||
command answers from what did decrypt (see "Default reads vs. fresh reads").
|
||||
collection's files, and does not change the cache either. On an empty cache,
|
||||
`get` and `get-thumb` ask for the same list, then for one collection's files at
|
||||
a time, until they find the file. They store none of the collections or file
|
||||
records they read. The original or thumbnail they fetch is kept in the cache.
|
||||
Once the cache holds the account's collections, they wait for a refresh instead.
|
||||
The others wait for a refresh, and so does `files` without a collection ID. With
|
||||
an empty cache, as on a first run, that refresh fetches the files of every
|
||||
collection, one collection after another, and stores nothing until it has them
|
||||
all, which on a large account takes a while. Meanwhile the command logs a line
|
||||
on stderr for each collection whose files it has fetched, such as
|
||||
`Fetched the files of 3 of 120 collections`, with `--json` too; `backup` does
|
||||
the same while the refresh it starts with runs. stdout holds only the command's
|
||||
output. If that round-trip fails, the command prints the error on one line and
|
||||
exits 1, or 3 when the server no longer accepts the saved session (see "Session
|
||||
handling"). An album or file quak cannot decrypt does not fail it; the command
|
||||
answers from what did decrypt (see "Default reads vs. fresh reads").
|
||||
`--cache-dir` overrides where the cache lives; without it each account gets its
|
||||
own directory under the per-user cache path.
|
||||
|
||||
@@ -1190,7 +1191,8 @@ https://git.eeqj.de/sneak/quak/issues/210).
|
||||
It answers with every live file in that collection that decrypts, in the
|
||||
server's order. `collection` is a record that `freshCollections()` returned. The
|
||||
method fetches no other collection's files and stores nothing. `quak files` with
|
||||
a collection ID uses it, and so do `quak get` and `quak get-thumb`.
|
||||
a collection ID uses it, and so do `quak get` and `quak get-thumb` on an empty
|
||||
cache.
|
||||
|
||||
An album or file in the server's answer that quak cannot decrypt, or whose
|
||||
metadata is not JSON, does not fail the refresh. The refresh leaves it out, an
|
||||
@@ -1275,8 +1277,9 @@ in the cache, which does not make `isLocal` true; only `download()` and
|
||||
|
||||
`await lib.originalOf(file, opts?)` and `await lib.thumbnailOf(file, opts?)`
|
||||
fetch and cache as `photo.original()` and `photo.thumbnail()` do. `file` is an
|
||||
`EnteFile` the caller holds, and the library need not hold it. `quak get` and
|
||||
`quak get-thumb` pass one that `lib.freshFiles()` returned.
|
||||
`EnteFile` the caller holds, and the library need not hold it. On an empty
|
||||
cache, `quak get` and `quak get-thumb` pass one that `lib.freshFiles()`
|
||||
returned.
|
||||
|
||||
Lower-level accessors that return decrypted model objects (which hold key
|
||||
material) are also available: `listCollections()`, `getCollection(id)`,
|
||||
|
||||
@@ -26,13 +26,14 @@ declares one.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-26: `quak get` and `quak get-thumb` stop fetching collections' files
|
||||
once they find the file (https://git.eeqj.de/sneak/quak/issues/222). They used
|
||||
to wait for a refresh, which on an empty cache fetched the files of every
|
||||
collection. They now read the collection list, then one collection's files at
|
||||
a time, first those the cache says hold the file. They store nothing they
|
||||
read, and fetch the file through the new `lib.originalOf()` and
|
||||
`lib.thumbnailOf()`.
|
||||
- 2026-10-26: On an empty cache, `quak get` and `quak get-thumb` stop fetching
|
||||
collections' files once they find the file
|
||||
(https://git.eeqj.de/sneak/quak/issues/222). They used to wait for a refresh,
|
||||
which on an empty cache fetched the files of every collection. Now they read
|
||||
the collection list, then one collection's files at a time. They store none of
|
||||
the collections or file records they read, and fetch the original or thumbnail
|
||||
through the new `lib.originalOf()` and `lib.thumbnailOf()`. Once the cache
|
||||
holds the account's collections, they still wait for a refresh.
|
||||
|
||||
- 2026-10-26: `quak files` with a collection ID fetches that collection's files
|
||||
alone (https://git.eeqj.de/sneak/quak/issues/217). It used to wait for a
|
||||
|
||||
+8
-8
@@ -132,10 +132,10 @@ const readLibraryClient = (client: Client): LibraryClient => ({
|
||||
// precache (issue #48) is off, because a one-shot `collections` or `get` must
|
||||
// not start downloading the whole account. No refresh starts as the library
|
||||
// opens or on a timer. Each command asks for the refresh it needs.
|
||||
// `collections` needs none (issue #210). Nor do `get`, `get-thumb` and `files`
|
||||
// with a collection ID (issues #217 and #222). While a refresh runs, each
|
||||
// collection whose files it has fetched is logged, so a large account shows
|
||||
// progress rather than nothing.
|
||||
// `collections` and `files` with a collection ID need none (issues #210 and
|
||||
// #217). Nor do `get` and `get-thumb` on an empty cache (issue #222). While a
|
||||
// refresh runs, each collection whose files it has fetched is logged, so a
|
||||
// large account shows progress rather than nothing.
|
||||
const openReadLibrary = (ctx: CliContext, client: Client): Promise<Library> =>
|
||||
Library.open({
|
||||
client: readLibraryClient(client),
|
||||
@@ -323,8 +323,8 @@ export const getCommand = async (
|
||||
|
||||
const lib = await openReadLibrary(ctx, client);
|
||||
try {
|
||||
// Ask the server for the file as it is now (issue #36 amendment,
|
||||
// issues #52 and #222).
|
||||
// Force a server round-trip so the file resolves against current state
|
||||
// (issue #36 amendment, issue #52).
|
||||
const file = await freshFile(lib, fileID);
|
||||
if (!file) {
|
||||
ctx.log("fatal", `File ${fileID} not found`);
|
||||
@@ -380,8 +380,8 @@ export const getThumbCommand = async (
|
||||
|
||||
const lib = await openReadLibrary(ctx, client);
|
||||
try {
|
||||
// Ask the server for the file as it is now (issue #36 amendment,
|
||||
// issues #52 and #222).
|
||||
// Force a server round-trip so the file resolves against current state
|
||||
// (issue #36 amendment, issue #52).
|
||||
const file = await freshFile(lib, fileID);
|
||||
if (!file) {
|
||||
ctx.log("fatal", `File ${fileID} not found`);
|
||||
|
||||
+21
-24
@@ -2,16 +2,16 @@
|
||||
//
|
||||
// `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). `files`
|
||||
// without a collection ID therefore forces a server round-trip through
|
||||
// `Library.fresh()` and only then reads, 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). `get` and `get-thumb` read that list, then one collection's files at a
|
||||
// time, until they find the file (issue #222).
|
||||
// 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).
|
||||
//
|
||||
// `collections` and `files` with a collection ID list in the server's order.
|
||||
// `files` without one lists in the library's own enumeration order
|
||||
@@ -33,7 +33,7 @@ export interface FreshReadLibrary {
|
||||
freshFiles(collection: Collection): Promise<EnteFile[]>;
|
||||
listCollections(): Collection[];
|
||||
listFiles(collectionID: number): EnteFile[];
|
||||
getFile(collectionID: number, fileID: number): EnteFile | undefined;
|
||||
getFileByID(fileID: number): EnteFile | undefined;
|
||||
}
|
||||
|
||||
// Every live collection, current as of one collections request, in the
|
||||
@@ -70,23 +70,20 @@ export const freshFiles = async (
|
||||
};
|
||||
|
||||
// One file as the server holds it now, or `undefined` when no collection holds
|
||||
// it. It reads the collection list, then one collection's files at a time, and
|
||||
// stops at the first collection that holds the file. It tries first the
|
||||
// collections the cache says hold the file, then the rest in the server's
|
||||
// order. It stores nothing.
|
||||
// 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<EnteFile | undefined> => {
|
||||
const collections = await lib.freshCollections();
|
||||
// Whether the cache holds the file in collection `c`.
|
||||
const inCache = (c: Collection): boolean =>
|
||||
lib.getFile(c.id, fileID) !== undefined;
|
||||
const ordered = [
|
||||
...collections.filter(inCache),
|
||||
...collections.filter((c) => !inCache(c)),
|
||||
];
|
||||
for (const collection of ordered) {
|
||||
// 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;
|
||||
|
||||
@@ -647,8 +647,8 @@ export class Library {
|
||||
}
|
||||
|
||||
// The original of `file`, fetched and cached as `Photo.original()` does.
|
||||
// `file` is used as given, so the store need not hold it. `quak get`
|
||||
// passes a file that `freshFiles()` returned.
|
||||
// `file` is used as given, so the store need not hold it. On an empty
|
||||
// cache, `quak get` passes a file that `freshFiles()` returned.
|
||||
async originalOf(
|
||||
file: EnteFile,
|
||||
opts?: ContentOptions,
|
||||
@@ -662,8 +662,8 @@ export class Library {
|
||||
}
|
||||
|
||||
// The thumbnail of `file`, fetched and cached as `Photo.thumbnail()` does.
|
||||
// `file` is used as given, so the store need not hold it. `quak get-thumb`
|
||||
// passes a file that `freshFiles()` returned.
|
||||
// `file` is used as given, so the store need not hold it. On an empty
|
||||
// cache, `quak get-thumb` passes a file that `freshFiles()` returned.
|
||||
async thumbnailOf(
|
||||
file: EnteFile,
|
||||
opts?: ContentOptions,
|
||||
|
||||
@@ -1816,7 +1816,7 @@ describe("an account with many albums", () => {
|
||||
|
||||
// Photo 200 is in album 2, so `get` asks for no file of album 3 or later.
|
||||
// The last request downloads the photo's original.
|
||||
it("get asks for the albums' files only until one holds the photo", async () => {
|
||||
it("get on an empty cache asks for the albums' files only until one holds the photo", async () => {
|
||||
const out = join(root, "photo.jpg");
|
||||
expect(await getCommand(accountContext(), "200", { out })).toBe(0);
|
||||
|
||||
@@ -1831,9 +1831,9 @@ describe("an account with many albums", () => {
|
||||
expect(existsSync(join(root, "cache", "metadata.json"))).toBe(false);
|
||||
});
|
||||
|
||||
// `files` first fills the cache. The cache then says album 40, the last,
|
||||
// holds photo 4000, so `get` asks for that album's files alone.
|
||||
it("get asks first for the files of the album the cache says holds the photo", async () => {
|
||||
// `files` first fills the cache, so `get` waits for a refresh. No album has
|
||||
// changed since, so the refresh asks for no album's files.
|
||||
it("get on a cache that holds the account asks for no unchanged album's files, also for a photo in no album", async () => {
|
||||
const ctx = accountContext();
|
||||
expect(await filesCommand(ctx, undefined, {})).toBe(0);
|
||||
requests.length = 0;
|
||||
@@ -1841,14 +1841,18 @@ describe("an account with many albums", () => {
|
||||
|
||||
const out = join(root, "photo.jpg");
|
||||
expect(await getCommand(ctx, "4000", { out })).toBe(0);
|
||||
|
||||
expect(readFileSync(out, "utf-8")).toBe("photo-40.jpg");
|
||||
expect(stderr.text).toBe(`12 bytes -> ${out}\n`);
|
||||
expect(requests).toEqual([
|
||||
"/collections/v2?sinceTime=0",
|
||||
"/collections/v2/diff?collectionID=40&sinceTime=0",
|
||||
"/collections/v2?sinceTime=1",
|
||||
"/?fileID=4000",
|
||||
]);
|
||||
|
||||
requests.length = 0;
|
||||
stderr.text = "";
|
||||
expect(await getCommand(ctx, "999", { out })).toBe(1);
|
||||
expect(stderr.text).toBe("File 999 not found\n");
|
||||
expect(requests).toEqual(["/collections/v2?sinceTime=1"]);
|
||||
});
|
||||
|
||||
it("files counts on stderr each album whose files its refresh fetched", async () => {
|
||||
|
||||
+17
-14
@@ -3,16 +3,16 @@
|
||||
* issue #36, issue #52).
|
||||
*
|
||||
* The `collections`, `files`, `get`, and `get-thumb` commands must answer for
|
||||
* 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
|
||||
* 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
|
||||
* (`freshCollections()`, issue #210). `files` with a collection ID asks for
|
||||
* that list, then for that one collection's files (`freshFiles()`, issue
|
||||
* #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.
|
||||
* #217). Neither may wait for a refresh, which would fetch the files of every
|
||||
* collection. On an empty cache, `get` and `get-thumb` ask for that list, then
|
||||
* for one collection's files at a time, until they find the file (issue #222).
|
||||
*
|
||||
* `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
|
||||
@@ -105,8 +105,9 @@ class FakeLibrary implements FreshReadLibrary {
|
||||
: [];
|
||||
}
|
||||
|
||||
getFile(collectionID: number, fileID: number): EnteFile | undefined {
|
||||
return this.listFiles(collectionID).find((f) => f.id === fileID);
|
||||
getFileByID(fileID: number): EnteFile | undefined {
|
||||
if (!this.refreshed) return undefined;
|
||||
return this.files.find((f) => f.id === fileID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +222,7 @@ describe("CLI read helpers (issue #36 amendment, issue #52)", () => {
|
||||
],
|
||||
);
|
||||
|
||||
it("freshFile reads each collection's files in the server's order until one holds the file", async () => {
|
||||
it("freshFile on an empty cache 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);
|
||||
@@ -233,16 +234,18 @@ describe("CLI read helpers (issue #36 amendment, issue #52)", () => {
|
||||
});
|
||||
|
||||
// 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 () => {
|
||||
it("freshFile on a cache that holds the collections refreshes, then looks the file up in the cache", async () => {
|
||||
const lib = threeCollections();
|
||||
await lib.fresh();
|
||||
|
||||
const found = await freshFile(lib, 30);
|
||||
expect(lib.freshFilesCalls).toEqual([3]);
|
||||
expect(lib.freshCalls).toBe(2);
|
||||
expect(lib.freshCollectionsCalls).toBe(0);
|
||||
expect(lib.freshFilesCalls).toEqual([]);
|
||||
expect(found?.collectionID).toBe(3);
|
||||
});
|
||||
|
||||
it("freshFile returns undefined for an unknown file, after reading every collection's files", async () => {
|
||||
it("freshFile on an empty cache returns undefined for an unknown file, after reading every collection's files", async () => {
|
||||
const lib = threeCollections();
|
||||
|
||||
const found = await freshFile(lib, 404);
|
||||
|
||||
Reference in New Issue
Block a user