quak login --server: log in to a self-hosted Ente server (closes #243)
check / check (push) Successful in 3m2s

`quak login` takes the server's API address from `--server` or
`QUAK_SERVER`. `isServerAddress` refuses anything but an http or https
URL with no query or fragment, there and in `Client.fromJSON`. The
client's snapshot keeps a non-default `apiOrigin`, so the session file
names the server and every later command uses it.

Two servers can give two accounts, and two files, the same ID. A
self-hosted account's default cache directory therefore names the
server, and `metadata.json` records it: `Library.open` deletes a cache
from another server whole. `originalOf` and `thumbnailOf` record the
account in a cache that names none before storing a file, and `open`
treats a cache with no collection as empty. This fixes
#254 (closes #254).

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-13 19:34:59 +00:00
1 parent 9bd7e8f099
commit f9af8e3f70
10 files changed
+643 -67

No files matched your search

+11 -12
View File
@@ -1,11 +1,12 @@
// On-disk JSON metadata store for the local cache.
//
// The store keeps one `metadata.json` file holding the account's server
// state: the user id, a schema version, the cursor for the incremental
// collections listing, the decrypted collection and file records, and the
// collections and files the refresh could not decrypt. The whole file is read
// into RAM on load and rewritten as a whole on save; there is no partial
// update and no lock file. A separate refresh unit populates the store from
// state: the user id, the server's address for an account on a self-hosted
// server, a schema version, the cursor for the incremental collections
// listing, the decrypted collection and file records, and the collections and
// files the refresh could not decrypt. The whole file is read into RAM on
// load and rewritten as a whole on save; there is no partial update and no
// lock file. A separate refresh unit populates the store from
// the server — this module only stores what it is given.
//
// The file is a cache, so it is never trusted to exist or to be intact: a
@@ -46,6 +47,7 @@ export interface Undecryptable {
interface StoredMetadata {
schemaVersion: number;
userID: number;
apiOrigin?: string;
collectionsSinceTime: Microseconds;
collections: StoredCollection[];
files: StoredFile[];
@@ -68,14 +70,10 @@ export class MetadataStore {
readonly path: string;
readonly schemaVersion = METADATA_SCHEMA_VERSION;
userID = 0;
// The account's `apiOrigin`, or undefined for Ente's hosted service.
apiOrigin: string | undefined;
collectionsSinceTime: Microseconds = 0;
// True when `load` populated this store from a valid existing file; false
// on a first run or a missing/corrupt/wrong-version file that loaded empty.
// `Library.open` reads it to decide whether the first refresh may run in
// the background (an existing copy already serves reads) or must be awaited.
loadedFromDisk = false;
private readonly collections = new Map<number, Collection>();
private readonly files = new Map<string, EnteFile>();
private undecryptable: Undecryptable[] = [];
@@ -102,8 +100,8 @@ export class MetadataStore {
if (parsed.schemaVersion !== METADATA_SCHEMA_VERSION) {
return store;
}
store.loadedFromDisk = true;
store.userID = parsed.userID ?? 0;
store.apiOrigin = parsed.apiOrigin;
store.collectionsSinceTime = parsed.collectionsSinceTime ?? 0;
for (const stored of parsed.collections ?? []) {
const collection: Collection = {
@@ -143,6 +141,7 @@ export class MetadataStore {
const model: StoredMetadata = {
schemaVersion: METADATA_SCHEMA_VERSION,
userID: this.userID,
apiOrigin: this.apiOrigin,
collectionsSinceTime: this.collectionsSinceTime,
collections: [...this.collections.values()].map((c) => ({
...c,