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

`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 was merged in pull request #253.
This commit is contained in:
clawbot committed 2026-11-13 22:57:49 +01:00
1 parent 9bd7e8f099
commit 2492946e9d
10 files changed
+768 -68

No files matched your search

+44 -7
View File
@@ -229,7 +229,7 @@ below belong to quak itself, and go before or after the command's name.
### The commands
```
quak login
quak login [--server <url>]
quak whoami
quak logout
quak collections [--json]
@@ -246,13 +246,26 @@ Every command but `login` and `logout` needs a saved session. Every command but
`login`, `whoami` and `logout` opens the library. How each one gets current data
from the server is described under "How a command gets current data".
#### `quak login`
#### `quak login [--server <url>]`
Logs in to an account and saves the session. It reads the email from
`QUAK_EMAIL` and the password from `QUAK_PASSWORD`. For each one that is not
set, it asks on the terminal. The password shows as `*` characters while it is
typed.
quak logs in to Ente's hosted service unless you name a self-hosted Ente server.
Name it with `--server`, or with `QUAK_SERVER` when `--server` is not given.
Give the address of the server's API, for example
`quak login --server https://ente.example.com`. The address is an http or https
URL. It may include a path, but no query or fragment. quak refuses any other
address before it asks for anything. It logs
`Invalid server address "<url>": it must be an http or https URL with no query or fragment`,
and the exit status is 1.
The session file keeps the server's address. Every later command, cron runs
included, uses that server without further setting. To change server, log in
again.
When the account has a second factor, quak asks for the code. It asks
`TOTP code: ` for a time-based code from an authenticator app. An account whose
only second factor is a passkey cannot log in with quak. When the account has
@@ -597,9 +610,11 @@ Linux that is `$XDG_DATA_HOME/quak/session.json`, or
mode `0600`, in a directory it creates with mode `0700`.
The file holds the account's email, its user ID, the session token and the
account's three keys, in base64 and unencrypted. Treat it as you would treat the
password. A missing file means "not logged in". A file that cannot be read back
is reported as corrupt, with the reason. Run `quak logout`, then `quak login`.
account's three keys, in base64 and unencrypted. After a login to a self-hosted
server, it also holds the server's address as `apiOrigin`. Treat it as you would
treat the password. A missing file means "not logged in". A file that cannot be
read back is reported as corrupt, with the reason. Run `quak logout`, then
`quak login`.
A command that needs a session exits 3 when the file is missing or corrupt. It
also exits 3 when the server no longer accepts the session, and prints
@@ -617,6 +632,11 @@ Without `--cache-dir`, each account has its own cache directory:
the variable is not set;
- on macOS, `~/Library/Caches/quak/<userID>`.
Two servers can give two accounts the same user ID. So an account on a
self-hosted server has the server's address, URL-encoded, between `quak` and the
user ID. For `https://ente.example.com`, that is
`quak/https%3A%2F%2Fente.example.com/<userID>`.
The directory is readable by its owner only (mode `0700`), because it holds
decrypted data, file keys included.
@@ -641,6 +661,13 @@ the library deletes it and `mldata/` and starts from an empty cache. Cached
originals and thumbnails are kept. They are reached only through the records of
the current account, so another account's files are never served.
When `metadata.json` belongs to an account on another server, opening the
library also deletes the originals and thumbnails. Two servers can give two
files the same ID, so one server's cached original could be served for the
other's file. quak records the account in `metadata.json` before it stores any
original or thumbnail. It does so also when `quak get` finds a file on an empty
cache.
A file appears in the cache only once it is complete: each one is written to a
temporary file and renamed into place. Ente's clients record a hash of each
file's bytes at upload, the content hash. Every original whose metadata records
@@ -1044,7 +1071,8 @@ onto what the cache holds, empty or not. A refresh then starts only when
- `client`, required: the account's `Client`, or anything that satisfies
`LibraryClient`.
- `cacheDirectory`: where `metadata.json` and the content cache live. Default:
`quak/<userID>` under the per-user cache directory (see "The cache").
`quak/<userID>` under the per-user cache directory, with the URL-encoded
`apiOrigin` before `<userID>` for a client that has one (see "The cache").
- `downloadDirectory`: the root of the save paths. Default: `photos` in the
working directory, resolved once when the library opens;
`lib.downloadDirectory` holds the result. An original stored at its save path
@@ -1241,7 +1269,8 @@ do.
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` use them; on an empty cache they pass a file that
`lib.freshFiles()` returned.
`lib.freshFiles()` returned. In a cache that names no account yet, they first
save the client's user ID and `apiOrigin` in `metadata.json`.
Lower-level reads return the decrypted model objects, which hold key material:
`lib.listCollections()`, `lib.getCollection(id)`, `lib.listFiles(collectionID)`,
@@ -1477,6 +1506,14 @@ write wherever it likes. `Client.fromJSON(snapshot, apiOptions?)` restores a
working client from that snapshot without logging in again. It checks every
field and each key's length first, and throws an error naming the bad field.
A client whose `apiOptions` named an `apiOrigin` puts it in its snapshot. The
restored client then uses that server, even when the `apiOptions` given to
`Client.fromJSON` name another. A snapshot without `apiOrigin` uses the
`apiOrigin` of those `apiOptions`, or the default server. `client.apiOrigin`
holds the one in use, or undefined when none was named. `Client.fromJSON`
refuses an `apiOrigin` that is not an http or https URL, or that has a query or
fragment, as `quak login --server` does.
`Client.fromJSON` works only once libsodium, the cryptography library quak uses,
is loaded. `Client.login` loads it, and `Client.fromJSON` does not. A program
that restores a session in a new process first runs `await sodium.ready`, with