Compare commits
1
Commits
next
..
c80651b43d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c80651b43d |
@@ -72,8 +72,7 @@ quak is not published to a package registry: `package.json` marks it `private`,
|
|||||||
and `dist/` is not committed. Clone and build it as above, then add the clone to
|
and `dist/` is not committed. Clone and build it as above, then add the clone to
|
||||||
your own program's `package.json` by its path, for example
|
your own program's `package.json` by its path, for example
|
||||||
`yarn add file:../quak`. The package's `main`, `types` and `exports` fields
|
`yarn add file:../quak`. The package's `main`, `types` and `exports` fields
|
||||||
point into `dist/`, so the clone must be built first. The package is an ES
|
point into `dist/`, so the clone must be built first.
|
||||||
module only, so a CommonJS program cannot `require` it.
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { Client, Library } from "quak";
|
import { Client, Library } from "quak";
|
||||||
@@ -89,9 +88,7 @@ const client = await Client.login({
|
|||||||
const lib = await Library.open({
|
const lib = await Library.open({
|
||||||
client,
|
client,
|
||||||
// Without these two, open() also starts downloading every thumbnail in
|
// Without these two, open() also starts downloading every thumbnail in
|
||||||
// the account, and the originals of the favorites album and the latest
|
// the account and the latest week's originals into the cache.
|
||||||
// week, into the cache. It fetches machine-learning data after each
|
|
||||||
// refresh either way.
|
|
||||||
precacheThumbnails: false,
|
precacheThumbnails: false,
|
||||||
precacheOriginals: false,
|
precacheOriginals: false,
|
||||||
});
|
});
|
||||||
@@ -121,13 +118,8 @@ await lib.close();
|
|||||||
`Library.open({ client })` with no other options does more than the example asks
|
`Library.open({ client })` with no other options does more than the example asks
|
||||||
for. It refreshes every 3 seconds until `close()`. It also downloads, in the
|
for. It refreshes every 3 seconds until `close()`. It also downloads, in the
|
||||||
background, every thumbnail in the account and the originals of the favorites
|
background, every thumbnail in the account and the originals of the favorites
|
||||||
album and of the latest week (see "Opening a library"). The example turns those
|
album and of the latest week (see "Opening a library"). The example turns the
|
||||||
two downloads off. After each refresh that succeeds, the library also fetches
|
downloads off.
|
||||||
each file's machine-learning data into the cache. That is the faces and the
|
|
||||||
other data Ente's apps compute for a photo (see "The JSON beside each
|
|
||||||
original"). No option in `LibraryOptions` turns this off. A `client` without a
|
|
||||||
`fetchMLData` method fetches none, which is how quak's own read commands open
|
|
||||||
their library.
|
|
||||||
|
|
||||||
The `Client` under the library handles login, saved sessions and the raw
|
The `Client` under the library handles login, saved sessions and the raw
|
||||||
requests. The "Library reference" below covers both.
|
requests. The "Library reference" below covers both.
|
||||||
@@ -136,9 +128,9 @@ requests. The "Library reference" below covers both.
|
|||||||
|
|
||||||
`quak backup` never prompts, so cron can run it. `make install` builds quak as
|
`quak backup` never prompts, so cron can run it. `make install` builds quak as
|
||||||
one binary and copies it to `~/bin/quak`. Besides what Getting Started needs,
|
one binary and copies it to `~/bin/quak`. Besides what Getting Started needs,
|
||||||
`make install` needs `make` and [nix](https://nixos.org/download). It uses bun,
|
`make install` needs [nix](https://nixos.org/download). It uses bun, a
|
||||||
a JavaScript runtime, to bundle the built `dist/` into one executable. It runs
|
JavaScript runtime, to bundle the built `dist/` into one executable. It runs bun
|
||||||
bun through `nix-shell -p bun`.
|
through `nix-shell -p bun`.
|
||||||
|
|
||||||
Log in once with `~/bin/quak`, as the user the cron job will run as:
|
Log in once with `~/bin/quak`, as the user the cron job will run as:
|
||||||
|
|
||||||
@@ -158,10 +150,9 @@ output to `~/quak-backup.log`. Sunday's run adds `--verify`:
|
|||||||
|
|
||||||
A run with `--verify` does all a plain run does. It also hashes each original
|
A run with `--verify` does all a plain run does. It also hashes each original
|
||||||
already in the backup and compares the result with the content hash. The content
|
already in the backup and compares the result with the content hash. The content
|
||||||
hash is the hash of the file's bytes that Ente recorded at upload. The run
|
hash is the hash of the file's bytes that Ente recorded at upload. It replaces
|
||||||
fetches a new copy of each original that does not match, and replaces the
|
any original that does not match. Sunday's run is the `--verify` run, rather
|
||||||
original once the copy matches. Sunday's run is the `--verify` run, rather than
|
than a second job that night, because two backups of one directory cannot run at
|
||||||
a second job that night, because two backups of one directory cannot run at
|
|
||||||
once. The second one exits 2 without doing anything.
|
once. The second one exits 2 without doing anything.
|
||||||
|
|
||||||
Cron runs the job with a short `PATH`, usually `/usr/bin:/bin`, so the crontab
|
Cron runs the job with a short `PATH`, usually `/usr/bin:/bin`, so the crontab
|
||||||
@@ -230,12 +221,9 @@ set, it asks on the terminal. The password shows as `*` characters while it is
|
|||||||
typed.
|
typed.
|
||||||
|
|
||||||
When the account has a second factor, quak asks for the code. It asks
|
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
|
`TOTP code: ` for a time-based code from an authenticator app, and
|
||||||
only second factor is a passkey cannot log in with quak. When the account has
|
`Email verification code: ` for a code sent by email. An account whose only
|
||||||
email verification on, quak asks `Email verification code: ` for the code sent
|
second factor is a passkey cannot log in with quak.
|
||||||
by email. The password is then used only to unlock the keys. After the email
|
|
||||||
code quak expects the login to complete. When the server asks for a second
|
|
||||||
factor at that point, login fails.
|
|
||||||
|
|
||||||
On success it logs `Logged in as <email> (user <id>)` and
|
On success it logs `Logged in as <email> (user <id>)` and
|
||||||
`Session saved to <path>`. On failure it logs `Login failed: <reason>`, and the
|
`Session saved to <path>`. On failure it logs `Login failed: <reason>`, and the
|
||||||
@@ -245,9 +233,7 @@ to stdout.
|
|||||||
#### `quak whoami`
|
#### `quak whoami`
|
||||||
|
|
||||||
Prints the saved session's account on stdout as one line of JSON:
|
Prints the saved session's account on stdout as one line of JSON:
|
||||||
`{"email":"you@example.com","userID":12345}`. It reads the session file only and
|
`{"email":"you@example.com","userID":12345}`.
|
||||||
sends no request, so it succeeds even when the server no longer accepts the
|
|
||||||
session.
|
|
||||||
|
|
||||||
#### `quak logout`
|
#### `quak logout`
|
||||||
|
|
||||||
@@ -314,22 +300,21 @@ refused, because both files would land on it.
|
|||||||
#### `quak get-thumb <fileID> [--out <path>]`
|
#### `quak get-thumb <fileID> [--out <path>]`
|
||||||
|
|
||||||
Downloads and decrypts one file's thumbnail, the small JPEG Ente shows in a
|
Downloads and decrypts one file's thumbnail, the small JPEG Ente shows in a
|
||||||
grid. It writes one file, also for a live photo. Without `--out`, the name is
|
grid. It works as `get` does. Without `--out`, the name is `thumb_` followed by
|
||||||
`thumb_` followed by the title. In all else it works as `get` does.
|
the title.
|
||||||
|
|
||||||
#### `quak backup <dir> [--json] [--verify]`
|
#### `quak backup <dir> [--json] [--verify]`
|
||||||
|
|
||||||
Backs up every file in the account to `<dir>`. The layout is described under
|
Backs up every file in the account to `<dir>`. The layout is described under
|
||||||
"The backup directory". The command downloads only the originals that are not
|
"The backup directory". It downloads only the originals that are not yet in the
|
||||||
yet in the backup, and it goes on past a file that fails. A file is downloaded
|
backup, and it goes on past a file that fails. A file is downloaded once,
|
||||||
once, however many collections hold it. An original the cache already holds is
|
however many collections hold it. An original the cache already holds is copied
|
||||||
copied from there instead.
|
from there instead.
|
||||||
|
|
||||||
Progress goes to stderr: one line per original fetched, and a `FAILED` line for
|
Progress goes to stderr: one line per original fetched, and a `FAILED` line for
|
||||||
each original or album that failed. The run ends with a summary of counts and,
|
each file or album that failed. The run ends with a summary of counts and a
|
||||||
when any file failed, a `Failed files:` list of every failed file. With
|
`Failed files:` list. With `--json`, the summary and the list are replaced by
|
||||||
`--json`, the summary and the list are replaced by one JSON object on stdout.
|
one JSON object on stdout. Its fields are:
|
||||||
Its fields are:
|
|
||||||
|
|
||||||
- `totalFiles`: how many distinct files the run covered, however many
|
- `totalFiles`: how many distinct files the run covered, however many
|
||||||
collections hold each;
|
collections hold each;
|
||||||
@@ -362,29 +347,26 @@ Writes the metadata quak keeps about the account, its collections and its files
|
|||||||
as JSON files under `<dir>`. The layout is described under "The metadata dump
|
as JSON files under `<dir>`. The layout is described under "The metadata dump
|
||||||
directory". Photo content is not written. With `--exif`, or its alias `--all`,
|
directory". Photo content is not written. With `--exif`, or its alias `--all`,
|
||||||
the command also fetches each file's original and records the image's own
|
the command also fetches each file's original and records the image's own
|
||||||
metadata from it (see "The metadata dump directory"). Each original it fetches
|
metadata from it (see "The metadata dump directory").
|
||||||
stays in the cache.
|
|
||||||
|
|
||||||
Ente's apps compute machine-learning data for each photo, such as the faces in
|
The command fetches the machine-learning data Ente holds for each file, in
|
||||||
it (see "The JSON beside each original"). The command fetches it in requests of
|
requests of up to 200 files. When a request fails, each of its files is written
|
||||||
up to 200 files. When a request fails, each of its files is written with the
|
with the reason in `mlDataError` instead of `mlData`, and the dump goes on. A
|
||||||
reason in `mlDataError` instead of `mlData`, and the dump goes on. A file whose
|
file whose data does not decrypt, decompress or parse also gets `mlDataError`.
|
||||||
data does not decrypt, decompress or parse also gets `mlDataError`. When any
|
When any file was written with `mlDataError`, the last line says how many and
|
||||||
file was written with `mlDataError`, the last line says how many and the exit
|
the exit status is 1.
|
||||||
status is 1.
|
|
||||||
|
|
||||||
A collection or file that quak cannot decrypt is left out of the dump without a
|
A collection or file that quak cannot decrypt is left out of the dump without a
|
||||||
word, and does not change the exit status.
|
word, and does not change the exit status.
|
||||||
|
|
||||||
#### `quak helper list-missing-thumbnails [--json]`
|
#### `quak helper list-missing-thumbnails [--json]`
|
||||||
|
|
||||||
Finds the files whose thumbnail is missing on the server. It downloads the
|
Finds the files whose thumbnail is missing on the server. It downloads every
|
||||||
thumbnail of every file you own, one collection at a time, newest updated first,
|
thumbnail in the account, one collection at a time, newest collection first, so
|
||||||
so on a large account it takes a while. A thumbnail counts as missing when the
|
on a large account it takes a while. A thumbnail counts as missing when the
|
||||||
server answers with an empty body or with HTTP 404. Any other failure is logged
|
server answers with an empty body or with HTTP 404. Any other failure is logged
|
||||||
as `[<collection>] Could not check <title>: <reason> (not reported as missing)`,
|
as `[<collection>] Could not check <title>: <reason> (not reported as missing)`,
|
||||||
and the file is left out of the list. The command exits 0 even when some files
|
and the file is left out of the list.
|
||||||
could not be checked.
|
|
||||||
|
|
||||||
The server lets only a file's owner replace its thumbnail. A file another
|
The server lets only a file's owner replace its thumbnail. A file another
|
||||||
account owns, in a collection shared with you, is skipped and not checked.
|
account owns, in a collection shared with you, is skipped and not checked.
|
||||||
@@ -417,7 +399,7 @@ Each file ends in one of three states:
|
|||||||
|
|
||||||
- `fixed`: a thumbnail was uploaded.
|
- `fixed`: a thumbnail was uploaded.
|
||||||
- `skipped`: the helper cannot make a thumbnail for the file, or the server
|
- `skipped`: the helper cannot make a thumbnail for the file, or the server
|
||||||
would refuse it. This covers:
|
would refuse it. A skipped file does not affect the exit status. This covers:
|
||||||
- a file another account owns;
|
- a file another account owns;
|
||||||
- a file that is not an image;
|
- a file that is not an image;
|
||||||
- an image that is not a JPEG;
|
- an image that is not a JPEG;
|
||||||
@@ -427,11 +409,10 @@ Each file ends in one of three states:
|
|||||||
`--file` is held by no collection.
|
`--file` is held by no collection.
|
||||||
|
|
||||||
Without `--json`, the command ends with counts of fixed, skipped and failed
|
Without `--json`, the command ends with counts of fixed, skipped and failed
|
||||||
files. A `Skipped:` list and a `Failed files:` list follow when they have
|
files, a `Skipped:` list and a `Failed files:` list. With `--json`, one JSON
|
||||||
entries. With `--json`, one JSON array on stdout replaces them; each object
|
array on stdout replaces them; each object holds `fileID`, `title`,
|
||||||
holds `fileID`, `title`, `collection`, `status` and, for a skipped or failed
|
`collection`, `status` and, for a skipped or failed file, `reason`. When any
|
||||||
file, `reason`. When any file failed, the exit status is 1. A skipped file does
|
file failed, the exit status is 1. When nothing needs fixing, the command logs
|
||||||
not change it. When nothing needs fixing, the command logs
|
|
||||||
`No missing thumbnails found.` and exits 0. With `--json` it then writes nothing
|
`No missing thumbnails found.` and exits 0. With `--json` it then writes nothing
|
||||||
on stdout.
|
on stdout.
|
||||||
|
|
||||||
@@ -464,9 +445,9 @@ collection fetched, such as `Fetched the files of 3 of 120 collections`.
|
|||||||
|
|
||||||
When a refresh or a search fails, the command logs the error on one line and
|
When a refresh or a search fails, the command logs the error on one line and
|
||||||
exits 1, or 3 when the server no longer accepts the saved session. A collection
|
exits 1, or 3 when the server no longer accepts the saved session. A collection
|
||||||
or file that quak cannot decrypt does not stop a refresh or a search: the
|
or file that quak cannot decrypt does not stop a refresh: the command answers
|
||||||
command leaves it out and answers from what did decrypt (see "Default reads and
|
from what did decrypt (see "Default reads and fresh reads"). `backup` counts it
|
||||||
fresh reads"). `backup` counts it as failed (see "What quak cannot decrypt").
|
as failed (see "What quak cannot decrypt").
|
||||||
|
|
||||||
### IDs
|
### IDs
|
||||||
|
|
||||||
@@ -478,20 +459,24 @@ before any request is sent. The command logs `Invalid file ID: <arg>` or
|
|||||||
|
|
||||||
### What quak prints
|
### What quak prints
|
||||||
|
|
||||||
stdout holds a command's output, such as the listing or the JSON, and the
|
stdout holds only a command's output: the listing, the JSON, or nothing. The
|
||||||
prompts of `login`. Every other line goes to stderr as a log line at one of six
|
prompts of `login` go to stdout too. Every other line goes to stderr, and every
|
||||||
levels. From the quietest, the levels are fatal, error, warning, info, debug and
|
stderr line is a log line at one of six levels. From the quietest, the levels
|
||||||
trace. A run prints the lines at its own level and at every quieter one. The
|
are fatal, error, warning, info, debug and trace. A run prints the lines at its
|
||||||
level is info unless `-v`, `-vv` or `-q` says otherwise.
|
own level and at every quieter one. The level is info unless `-v`, `-vv` or `-q`
|
||||||
|
says otherwise.
|
||||||
|
|
||||||
- Fatal: why a command exits non-zero. A command that runs and exits non-zero
|
`yarn quak` prints yarn's own lines on stdout as well. To pipe a command's
|
||||||
logs at least one fatal line. A command that exits 1 because files or albums
|
output, run `yarn --silent quak files --json` or
|
||||||
failed logs it last, saying how many failed and where to look.
|
`node dist/bin/quak.js files --json` instead.
|
||||||
- Error: something that failed without ending the run. These lines are:
|
|
||||||
- the `FAILED` lines of `backup` and `fix-missing-thumbnails`;
|
- Fatal: why a command exits non-zero. Every command that exits non-zero logs at
|
||||||
- their `Failed files:` lists;
|
least one fatal line. A command that exits 1 because files or albums failed
|
||||||
- the `ML data` lines of `backup-metadata`;
|
logs it last, saying how many failed and where to look.
|
||||||
- `[<collection>] Could not check <title>` from the thumbnail helpers.
|
- Error: something that failed without ending the run. These are the `FAILED`
|
||||||
|
lines of `backup` and `fix-missing-thumbnails`, their `Failed files:` lists,
|
||||||
|
the `ML data` lines of `backup-metadata`, and
|
||||||
|
`[<collection>] Could not check <title>` from the thumbnail helpers.
|
||||||
- Warning: something that did not fail but needs your attention. The only
|
- Warning: something that did not fail but needs your attention. The only
|
||||||
warning is the one `logout` prints: the cache directory still holds decrypted
|
warning is the one `logout` prints: the cache directory still holds decrypted
|
||||||
data.
|
data.
|
||||||
@@ -508,22 +493,12 @@ level is info unless `-v`, `-vv` or `-q` says otherwise.
|
|||||||
one that exits non-zero prints only its fatal lines. The prompts of `login` are
|
one that exits non-zero prints only its fatal lines. The prompts of `login` are
|
||||||
not log lines, and `-q` leaves them alone.
|
not log lines, and `-q` leaves them alone.
|
||||||
|
|
||||||
Two things on stderr are not log lines. A command line that quak cannot parse,
|
|
||||||
such as a missing argument, an unknown option or `-q` with `-v`, is refused with
|
|
||||||
an `error: …` message, and quak exits 1. `-q` does not hide the message. `quak`
|
|
||||||
with no command prints the usage on stderr and exits 1.
|
|
||||||
|
|
||||||
`yarn quak` prints yarn's own lines on stdout as well. To pipe a command's
|
|
||||||
output, run `yarn --silent quak files --json` or
|
|
||||||
`node dist/bin/quak.js files --json` instead.
|
|
||||||
|
|
||||||
With `--json`, a command's JSON on stdout takes the place of its summary on
|
With `--json`, a command's JSON on stdout takes the place of its summary on
|
||||||
stderr. For `backup`, that is the `--- Backup complete ---` line, the counts and
|
stderr. For `backup`, that is the lines from `--- Backup complete ---` to the
|
||||||
the `Failed files:` list. For `list-missing-thumbnails`, it is
|
end of the `Failed files:` list. For `list-missing-thumbnails`, it is
|
||||||
`No missing thumbnails found.` or `<n> file(s) with missing thumbnails:`. For
|
`No missing thumbnails found.` or `<n> file(s) with missing thumbnails:`. For
|
||||||
`fix-missing-thumbnails`, it is the `--- Done ---` line, the counts, the
|
`fix-missing-thumbnails`, it is the lines from `--- Done ---` to the end of the
|
||||||
`Skipped:` list and the `Failed files:` list. Every other line is logged as
|
`Failed files:` list. Every other line is logged as without `--json`.
|
||||||
without `--json`.
|
|
||||||
|
|
||||||
### Exit codes
|
### Exit codes
|
||||||
|
|
||||||
@@ -543,8 +518,9 @@ without `--json`.
|
|||||||
|
|
||||||
An unexpected error is printed as one line, `quak: <message>`, with no stack
|
An unexpected error is printed as one line, `quak: <message>`, with no stack
|
||||||
trace. `<message>` is the error's own message, followed by the message of each
|
trace. `<message>` is the error's own message, followed by the message of each
|
||||||
error that caused it, separated by `: `. Some lines a command logs itself, such
|
error that caused it, separated by `: `. An error a command expects, such as
|
||||||
as `File <id> not found` and `Login failed: <reason>`, have no prefix.
|
`File <id> not found` or `Login failed: <reason>`, is printed without the
|
||||||
|
prefix.
|
||||||
|
|
||||||
`yarn quak` runs quak under Node. Node's `fetch` reports every network failure
|
`yarn quak` runs quak under Node. Node's `fetch` reports every network failure
|
||||||
as `fetch failed`, with the reason as its cause. A run that could not reach the
|
as `fetch failed`, with the reason as its cause. A run that could not reach the
|
||||||
@@ -577,8 +553,8 @@ 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
|
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`.
|
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
|
Both cases exit 3, and so does a session the server no longer accepts: the
|
||||||
also exits 3 when the server no longer accepts the session, and prints
|
command then prints
|
||||||
`quak: the saved session is no longer valid; run "quak login"`. `quak backup`
|
`quak: the saved session is no longer valid; run "quak login"`. `quak backup`
|
||||||
learns this at its first refresh. By then it has written only its lock. When the
|
learns this at its first refresh. By then it has written only its lock. When the
|
||||||
session stops working partway through a backup, each remaining file goes into
|
session stops working partway through a backup, each remaining file goes into
|
||||||
@@ -630,8 +606,8 @@ holds a second image or video. It also refuses one whose files unpack to more
|
|||||||
than 20 times its size plus 16 MiB. An original with no recorded hash, uploaded
|
than 20 times its size plus 16 MiB. An original with no recorded hash, uploaded
|
||||||
by a very old client, is stored unchecked.
|
by a very old client, is stored unchecked.
|
||||||
|
|
||||||
The cache has a size limit for originals (see "Opening a library"). Thumbnails
|
The originals in the cache are bounded (see "Opening a library"). Thumbnails are
|
||||||
have none. A live photo's image and video leave the cache together.
|
not. A live photo's image and video leave the cache together.
|
||||||
|
|
||||||
### The backup directory
|
### The backup directory
|
||||||
|
|
||||||
@@ -689,13 +665,13 @@ The JSON file beside an original holds:
|
|||||||
|
|
||||||
Update times are in microseconds, as Ente records them.
|
Update times are in microseconds, as Ente records them.
|
||||||
|
|
||||||
The JSON also holds the machine-learning data Ente computed for the photo, as
|
The file also holds the machine-learning data Ente computed for the file, as
|
||||||
`mlData`. Ente's apps detect faces in each photo and compute a CLIP embedding
|
`mlData`. Ente's apps detect faces in each photo and compute a CLIP embedding
|
||||||
for it, a list of 512 numbers that describes what the photo shows. `mlData`
|
for it, a list of 512 numbers that describes what the photo shows. `mlData`
|
||||||
holds everything but the `clip` block, which holds that embedding. In its place
|
holds everything but that embedding. In its place the file holds `keywords`,
|
||||||
the JSON holds `keywords`, derived from the embedding as described under
|
derived from the embedding as described under "Keywords". A file with no
|
||||||
"Keywords". A file with no machine-learning data has neither field. A file whose
|
machine-learning data has neither field. A file whose data holds no usable
|
||||||
data holds no usable embedding has no `keywords`.
|
embedding has no `keywords`.
|
||||||
|
|
||||||
The backup waits for the library's fetch of machine-learning data to finish
|
The backup waits for the library's fetch of machine-learning data to finish
|
||||||
before it writes the JSON files. When a file's data cannot be fetched,
|
before it writes the JSON files. When a file's data cannot be fetched,
|
||||||
@@ -705,13 +681,13 @@ with an error starting `ML data: `. Nothing is cached for it, so the next run
|
|||||||
asks for its data again. The JSON files are rewritten on every run, so data that
|
asks for its data again. The JSON files are rewritten on every run, so data that
|
||||||
arrived since the last run appears.
|
arrived since the last run appears.
|
||||||
|
|
||||||
Every file but a video also holds `imageMetadata`: what the original itself
|
For an image, the file also holds `imageMetadata`: what the original itself
|
||||||
records. That is its `format`, `width` and `height` for a JPEG, its EXIF tags as
|
records. That is its `format`, `width` and `height` for a JPEG, its EXIF tags as
|
||||||
`exif`, and its XMP packet as `xmp`. EXIF and XMP are the two blocks of metadata
|
`exif`, and its XMP packet as `xmp`. EXIF and XMP are the two blocks of metadata
|
||||||
a camera or an editor writes inside an image file. For a live photo, these come
|
a camera or an editor writes inside an image file. For a live photo, these come
|
||||||
from its image. An original with none of them gets `{}`. When the original
|
from its image. An original with none of them gets `{}`. A video gets no
|
||||||
cannot be read, the reason is in `imageMetadataError` instead, and neither the
|
`imageMetadata`. When the original cannot be read, the reason is in
|
||||||
file nor the run fails.
|
`imageMetadataError` instead, and neither the file nor the run fails.
|
||||||
|
|
||||||
EXIF is read with [exifreader](https://github.com/mattiasw/ExifReader) from any
|
EXIF is read with [exifreader](https://github.com/mattiasw/ExifReader) from any
|
||||||
image format it reads, such as JPEG, HEIC, AVIF, PNG and WebP. `exif` holds each
|
image format it reads, such as JPEG, HEIC, AVIF, PNG and WebP. `exif` holds each
|
||||||
@@ -748,11 +724,11 @@ derives a file's keywords the way the app sorts it. The categories are:
|
|||||||
|
|
||||||
Each category has a query, and a text embedding of that query from Ente's CLIP
|
Each category has a query, and a text embedding of that query from Ente's CLIP
|
||||||
text model. A file's score for a category is the cosine similarity of the file's
|
text model. A file's score for a category is the cosine similarity of the file's
|
||||||
embedding and the category's. That is a number from -1 to 1; the more alike the
|
embedding and the category's: a number from -1 to 1, higher the more alike the
|
||||||
two embeddings are, the higher it is. The file falls in the category when its
|
two embeddings are. The file falls in the category when its score reaches the
|
||||||
score reaches the category's minimum score. The keywords are the titles of the
|
category's minimum score. The keywords are the titles of the categories the file
|
||||||
categories the file falls in, highest score first. A video never falls in a
|
falls in, highest score first. A video never falls in a category marked "no
|
||||||
category marked "no videos", as in the app.
|
videos", as in the app.
|
||||||
|
|
||||||
#### Collection directories
|
#### Collection directories
|
||||||
|
|
||||||
@@ -775,24 +751,20 @@ ignoring case, each gets its ID added: two albums named `Trip` become
|
|||||||
`Trip (10)/` and `Trip (11)/`. Two files in one collection whose titles match,
|
`Trip (10)/` and `Trip (11)/`. Two files in one collection whose titles match,
|
||||||
ignoring case, get their IDs added before the extension: `IMG_0001 (12345).JPG`
|
ignoring case, get their IDs added before the extension: `IMG_0001 (12345).JPG`
|
||||||
and `IMG_0001 (12346).JPG`. A collection named like another collection's JSON
|
and `IMG_0001 (12346).JPG`. A collection named like another collection's JSON
|
||||||
file, such as `Trip.json` beside `Trip`, also gets its ID added. A file name
|
file, such as `Trip.json` beside `Trip`, also gets its ID added. A name too long
|
||||||
longer than 255 bytes, the limit on most file systems, gets its ID added too. So
|
for a file name, which most file systems limit to 255 bytes, gets its ID added
|
||||||
does a collection name longer than 250 bytes, which leaves room for `.json`.
|
too. It is then cut short before the ID, just far enough to fit. IDs never
|
||||||
Either name is then cut short before the ID, just far enough to fit. IDs never
|
|
||||||
change, so a name stays the same from run to run until such a clash appears or
|
change, so a name stays the same from run to run until such a clash appears or
|
||||||
goes away.
|
goes away.
|
||||||
|
|
||||||
Each run removes every symlink in a collection's directory that points into the
|
Each run removes the symlinks that no longer belong in a collection's directory.
|
||||||
date folders and is not one of its current links, yours included. A file or
|
It removes the directory and JSON of a collection that was deleted or renamed,
|
||||||
symlink of yours at the name of one of its links is replaced by the link. A
|
once the directory holds nothing else. Nothing else under `collections/` is
|
||||||
directory of yours at that name stays, and the photo whose link it blocks goes
|
touched: a file or symlink you put there stays, and a directory that still holds
|
||||||
into `failures.json`. Anything else you put under `collections/` stays. A run
|
one stays too, with its JSON.
|
||||||
removes the directory and JSON of a collection that was deleted or renamed, once
|
|
||||||
the directory holds nothing else. One that still holds something of yours stays,
|
|
||||||
with its JSON.
|
|
||||||
|
|
||||||
A collection whose directory or JSON cannot be written does not stop the run.
|
A collection whose directory or JSON cannot be written does not stop the run.
|
||||||
For example, a file you put there may already have the directory's name. The run
|
One whose directory name is taken by a file you put there is an example. The run
|
||||||
logs `FAILED album <name>: <reason>`, lists the collection's ID in
|
logs `FAILED album <name>: <reason>`, lists the collection's ID in
|
||||||
`failedAlbums`, and exits 1. The other collections and `failures.json` are still
|
`failedAlbums`, and exits 1. The other collections and `failures.json` are still
|
||||||
written.
|
written.
|
||||||
@@ -823,8 +795,7 @@ stores the two files, which a photo viewer can open: each is
|
|||||||
`YYYY-MM-DD.<fileID>.<ext>` with the extension it has inside the ZIP, such as
|
`YYYY-MM-DD.<fileID>.<ext>` with the extension it has inside the ZIP, such as
|
||||||
`2026-03-01.12345.heic` and `2026-03-01.12345.mov`. The file
|
`2026-03-01.12345.heic` and `2026-03-01.12345.mov`. The file
|
||||||
`YYYY-MM-DD.<fileID>.livephoto.json` beside them names which is which. The live
|
`YYYY-MM-DD.<fileID>.livephoto.json` beside them names which is which. The live
|
||||||
photo counts as stored only when its `.livephoto.json` and both files it names
|
photo counts as stored only when both files are present and not empty.
|
||||||
are present and not empty.
|
|
||||||
|
|
||||||
#### `failures.json`
|
#### `failures.json`
|
||||||
|
|
||||||
@@ -834,9 +805,8 @@ Each entry holds:
|
|||||||
|
|
||||||
- `fileID` and `title`;
|
- `fileID` and `title`;
|
||||||
- `classification`: the kind of failure;
|
- `classification`: the kind of failure;
|
||||||
- `attempts`: how many runs in a row have failed the file;
|
- `attempts`: how many runs have tried the file;
|
||||||
- `lastTriedAt`: when the run that last tried it finished its refresh, as a Unix
|
- `lastTriedAt`: when it was last tried, as a Unix time in milliseconds;
|
||||||
time in milliseconds;
|
|
||||||
- `error`: the error, as quak printed it.
|
- `error`: the error, as quak printed it.
|
||||||
|
|
||||||
The kinds are:
|
The kinds are:
|
||||||
@@ -847,30 +817,29 @@ The kinds are:
|
|||||||
|
|
||||||
A file leaves the list once it succeeds, or once it is no longer in the library.
|
A file leaves the list once it succeeds, or once it is no longer in the library.
|
||||||
A library caller that backs up only some albums (`onlyAlbumNames`) also drops a
|
A library caller that backs up only some albums (`onlyAlbumNames`) also drops a
|
||||||
file outside them. `failures.json` is deleted when the list is empty.
|
file outside them. The file is deleted when the list is empty.
|
||||||
|
|
||||||
#### `backup.lock`
|
#### `backup.lock`
|
||||||
|
|
||||||
`backup.lock` keeps two backups of the same directory from running at once, such
|
`backup.lock` keeps two backups of the same directory from running at once, such
|
||||||
as a cron run that starts while the previous one is still going. A backup
|
as a cron run that starts while the previous one is still going. A backup
|
||||||
creates `<dir>` if it is missing and takes the lock before its refresh, so a
|
creates `<dir>` if it is missing and takes the lock before its refresh, so a
|
||||||
refused run sends no request. A run that still holds the lock removes it when it
|
refused run sends no request. It removes the lock when it ends, whether it
|
||||||
ends, whether it succeeded or failed.
|
succeeded or failed.
|
||||||
|
|
||||||
The lock is a directory that
|
The lock is a directory that
|
||||||
[proper-lockfile](https://github.com/moxystudio/node-proper-lockfile) creates.
|
[proper-lockfile](https://github.com/moxystudio/node-proper-lockfile) creates
|
||||||
While the backup runs, it updates the modification time of `backup.lock` every 5
|
and keeps touching every 5 seconds while the backup runs. A second backup of the
|
||||||
seconds. A second backup of `<dir>` fails at once: it prints
|
directory fails at once: it prints `quak: another backup of <dir> is running`
|
||||||
`quak: another backup of <dir> is running` and exits 2. A run that is killed, or
|
and exits 2. A run that is killed, or cut off by a crash, can leave the lock
|
||||||
cut off by a crash, can leave the lock behind. Once the lock has gone 10 seconds
|
behind. Once the lock has gone 10 seconds untouched, the next run takes it over,
|
||||||
without an update, the next run takes it over, so nobody has to remove it by
|
so nobody has to remove it by hand.
|
||||||
hand.
|
|
||||||
|
|
||||||
A running backup loses its lock when someone removes `backup.lock`. Another
|
A running backup loses its lock when someone removes `backup.lock`. It also
|
||||||
backup takes the lock over when this one has not updated it for 10 seconds, for
|
loses it when another backup takes it over, because this one left it untouched
|
||||||
example because this process was stopped. The backup notices at its next update
|
for 10 seconds, as a stopped process does. The backup notices the next time it
|
||||||
of the lock, and stops before it starts another file. The originals it is
|
goes to touch the lock, and stops before it starts another file. The originals
|
||||||
downloading at that moment are still stored. It leaves `failures.json` and
|
it is downloading at that moment are still stored. It leaves `failures.json` and
|
||||||
`backup.lock` as they are, since another backup may be using them. It prints
|
`backup.lock` as they are, since another backup may be using them. It prints
|
||||||
`quak: lost the lock on <dir>: <reason>` and exits 1.
|
`quak: lost the lock on <dir>: <reason>` and exits 1.
|
||||||
|
|
||||||
@@ -903,16 +872,13 @@ The summary and the `--json` output add the counts `verified`, `mismatched` and
|
|||||||
`unchecked`. Each counts only originals that were already stored. A mismatch
|
`unchecked`. Each counts only originals that were already stored. A mismatch
|
||||||
that was replaced with matching bytes does not make the exit code non-zero.
|
that was replaced with matching bytes does not make the exit code non-zero.
|
||||||
|
|
||||||
A run without `--verify` drops two kinds of failure from `failures.json`:
|
A mismatched original whose new copy failed, and a stored original that cannot
|
||||||
|
be read, stay in `failures.json` only until the next run. A run without
|
||||||
|
`--verify` finds the original in place, counts it as skipped, and drops the
|
||||||
|
entry, so that run can exit 0. Only a `--verify` run hashes it again.
|
||||||
|
|
||||||
- a mismatched original whose new copy failed;
|
Without `--verify`, nothing is hashed, the summary is unchanged, and the three
|
||||||
- a stored original that cannot be read.
|
counts are 0 in the JSON.
|
||||||
|
|
||||||
That run finds the original in place, counts it as skipped and drops the entry,
|
|
||||||
so it can exit 0. Only a `--verify` run hashes the original again.
|
|
||||||
|
|
||||||
Without `--verify`, no stored original is hashed, the summary is unchanged, and
|
|
||||||
the three counts are 0 in the JSON.
|
|
||||||
|
|
||||||
#### Temporary files and crash safety
|
#### Temporary files and crash safety
|
||||||
|
|
||||||
@@ -934,12 +900,7 @@ scheme. Opening a library deletes such leftovers in the cache's `originals/` and
|
|||||||
|
|
||||||
The rename replaces whatever was at the destination rather than writing through
|
The rename replaces whatever was at the destination rather than writing through
|
||||||
it. A symlink there is replaced, not followed, and the new file has the
|
it. A symlink there is replaced, not followed, and the new file has the
|
||||||
temporary file's permissions, not those of the file it replaced. Each original
|
temporary file's permissions, not those of the file it replaced.
|
||||||
is written with mode `0600`, readable by its owner only. Everything else the
|
|
||||||
backup writes, its folders and JSON files included, gets the mode your umask
|
|
||||||
allows, which usually lets every user on the machine read it. The JSON beside an
|
|
||||||
original holds the file's location and faces. The `backup-metadata` tree is
|
|
||||||
written the same way.
|
|
||||||
|
|
||||||
### The metadata dump directory
|
### The metadata dump directory
|
||||||
|
|
||||||
@@ -955,21 +916,19 @@ written the same way.
|
|||||||
the collection
|
the collection
|
||||||
```
|
```
|
||||||
|
|
||||||
Each run writes into `<dir>` and deletes nothing. `account.json` holds `email`
|
`account.json` holds `email` and `userID`. The name after the ID in a
|
||||||
and `userID`. In `<id>-<name>`, the name has unsafe characters replaced. It is
|
collection's directory name has unsafe characters replaced and is cut short so
|
||||||
cut short so the directory name fits in 255 bytes. The ID keeps two collections
|
that the whole name fits in 255 bytes; the ID keeps the directories apart.
|
||||||
of the same name apart.
|
|
||||||
|
|
||||||
`_collection.json` holds the fields a backup's collection JSON holds, except
|
`_collection.json` holds the fields a backup's collection JSON holds, except
|
||||||
`files`. Each `<fileID>.json` holds the fields a backup's JSON beside an
|
`files`. Each `<fileID>.json` holds the fields a backup's JSON beside an
|
||||||
original holds, with these differences:
|
original holds, with these differences:
|
||||||
|
|
||||||
- `imageMetadata` is written only with `--exif`. The original of every file,
|
- `imageMetadata` is written only with `--exif`. The original of every file is
|
||||||
videos included, is then downloaded and read for its EXIF, XMP and JPEG
|
then downloaded, videos included, and read as a backup reads it (see "The JSON
|
||||||
dimensions.
|
beside each original").
|
||||||
- A file whose original holds no image metadata gets no `imageMetadata` field.
|
- A file whose original holds no image metadata gets no `imageMetadata` field.
|
||||||
- An original that cannot be downloaded or read gives `imageMetadataError`
|
- An original that cannot be read gives `imageMetadataError` instead.
|
||||||
instead.
|
|
||||||
- A file in several collections has its original read once; its image metadata
|
- A file in several collections has its original read once; its image metadata
|
||||||
is written with the first of them.
|
is written with the first of them.
|
||||||
|
|
||||||
@@ -1047,24 +1006,21 @@ onto what the cache holds, empty or not. A refresh then starts only when
|
|||||||
- `contentSource`: a replacement for the client's own byte source. Mainly for
|
- `contentSource`: a replacement for the client's own byte source. Mainly for
|
||||||
tests.
|
tests.
|
||||||
|
|
||||||
In the content and thumbnail pools, your own requests take the next free slot
|
The two background downloads wait while your own requests run. A file already in
|
||||||
ahead of the two background downloads. A file already in the cache is not
|
the cache is not fetched again. When the originals cache grows past its limit,
|
||||||
fetched again. When the originals cache grows past its limit, the least recently
|
the least recently used originals are evicted, but never a pinned one, and never
|
||||||
used originals are evicted, but never a pinned one, and never one whose fetch is
|
one whose fetch is still in flight.
|
||||||
still in flight.
|
|
||||||
|
|
||||||
`onProgress` receives a `RefreshEvent`. Its `operation` names the work:
|
`onProgress` receives a `RefreshEvent`. Its `operation` names the work:
|
||||||
`"refresh"`, `"fetchMLData"` for a machine-learning fetch, and
|
`"refresh"`, `"fetchMLData"` for a machine-learning fetch, and
|
||||||
`"precacheThumbnails"` or `"precacheOriginals"` for the two background
|
`"precacheThumbnails"` or `"precacheOriginals"` for the two background
|
||||||
downloads. Its `status` is `"started"` as the work starts and `"done"` as it
|
downloads. Its `status` is `"started"` as the work starts and `"done"` as it
|
||||||
ends. A refresh or a machine-learning fetch that fails ends with `"failed"`
|
ends. A refresh or a machine-learning fetch that fails ends with `"failed"` and
|
||||||
instead, with the message in `error`. In between, a refresh sends `"progress"`
|
the error message instead. In between, a refresh sends `"progress"` after each
|
||||||
after each collection whose files it fetched: `fetched` is the number of
|
collection whose files it fetched: `fetched` is the number of collections
|
||||||
collections fetched so far, and `total` the number it will fetch. A refresh
|
fetched so far, and `total` the number it will fetch. A refresh fetches the
|
||||||
fetches the files of each collection that is new or changed since the cache's
|
files of each collection that is new or changed since the cache's copy, so on an
|
||||||
copy, so on an empty cache it fetches those of every collection. The `"done"`
|
empty cache it fetches those of every collection.
|
||||||
event of a machine-learning fetch, and of each background download, also carries
|
|
||||||
`fetched`: how many items it newly stored.
|
|
||||||
|
|
||||||
`lib.status()` returns a `LibraryStatus`. It holds the collection and file
|
`lib.status()` returns a `LibraryStatus`. It holds the collection and file
|
||||||
counts. `lastRefreshAt` is the time of the last refresh that succeeded, and
|
counts. `lastRefreshAt` is the time of the last refresh that succeeded, and
|
||||||
@@ -1079,10 +1035,10 @@ cached, so the next fetch asks for it again. The reasons are kept in memory
|
|||||||
only.
|
only.
|
||||||
|
|
||||||
`lib.close()` stops the background timer. It can be called more than once. A
|
`lib.close()` stops the background timer. It can be called more than once. A
|
||||||
refresh in flight is left to finish, with its write to the cache. `close()`
|
refresh in flight is left to finish, with its write to the cache. The promise
|
||||||
returns a promise. It resolves once that refresh, the machine-learning fetch and
|
`close()` returns resolves once that refresh, the machine-learning fetch and the
|
||||||
the background downloads already running have all finished. The cache directory
|
background downloads already running have all finished. The cache directory can
|
||||||
can then be removed. After `close()`, `fresh()`, `freshCollections()`,
|
then be removed. After `close()`, `fresh()`, `freshCollections()`,
|
||||||
`freshFiles()` and `lib.backup()` reject with `the library is closed`.
|
`freshFiles()` and `lib.backup()` reject with `the library is closed`.
|
||||||
|
|
||||||
### Default reads and fresh reads
|
### Default reads and fresh reads
|
||||||
@@ -1102,11 +1058,11 @@ running does it start one. When the refresh fails, `fresh()` rejects. Default
|
|||||||
reads stay silent and keep serving the last good copy.
|
reads stay silent and keep serving the last good copy.
|
||||||
|
|
||||||
`await lib.freshCollections()` is the fresh read of the collection list alone:
|
`await lib.freshCollections()` is the fresh read of the collection list alone:
|
||||||
one request, answered with every collection that is not deleted and decrypts, as
|
one request, answered with every live collection that decrypts, as `Collection`
|
||||||
`Collection` objects, in the server's order. It fetches no collection's files
|
objects, in the server's order. It fetches no collection's files and stores
|
||||||
and stores nothing, not even the list. It stores nothing because a refresh
|
nothing, not even the list. It stores nothing because a refresh fetches a
|
||||||
fetches a collection's files only when the collection is new or changed. A
|
collection's files only when the collection is new or changed. A collection
|
||||||
collection stored without its files would never get them.
|
stored without its files would never get them.
|
||||||
|
|
||||||
`await lib.freshFiles(collection)` is the fresh read of one collection's files,
|
`await lib.freshFiles(collection)` is the fresh read of one collection's files,
|
||||||
as `EnteFile` objects, in the server's order, each file once. `collection` is an
|
as `EnteFile` objects, in the server's order, each file once. `collection` is an
|
||||||
@@ -1133,9 +1089,8 @@ later refresh gets a copy that decrypts or learns that the server deleted it.
|
|||||||
- `lib.timeline.groups({ groupBy, filter? })` returns `TimelineGroup[]`, newest
|
- `lib.timeline.groups({ groupBy, filter? })` returns `TimelineGroup[]`, newest
|
||||||
group first. `groupBy` is `"day"`, `"week"` or `"month"`, and the group keys
|
group first. `groupBy` is `"day"`, `"week"` or `"month"`, and the group keys
|
||||||
are `YYYY-MM-DD`, `YYYY-Www` (the ISO week) and `YYYY-MM`. Each group holds
|
are `YYYY-MM-DD`, `YYYY-Www` (the ISO week) and `YYYY-MM`. Each group holds
|
||||||
`key`, `startsAt` and `fileIDs`. `key` is that group key. `startsAt` is the
|
`startsAt`, local time in milliseconds, and `fileIDs`, newest first. Weeks
|
||||||
start of the period, in local-time milliseconds. `fileIDs` lists its files,
|
start on Monday.
|
||||||
newest first. Weeks start on Monday.
|
|
||||||
|
|
||||||
A `PhotoFilter` narrows the timeline. Its fields combine with AND:
|
A `PhotoFilter` narrows the timeline. Its fields combine with AND:
|
||||||
|
|
||||||
@@ -1187,11 +1142,11 @@ progress of that one file.
|
|||||||
keyed by tag name, each as exifreader decodes it with its `id`, `value`,
|
keyed by tag name, each as exifreader decodes it with its `id`, `value`,
|
||||||
`description` and `computed` value. For example, `Make` is
|
`description` and `computed` value. For example, `Make` is
|
||||||
`{ id: 271, value: ["Example"], description: "Example", computed: "Example" }`.
|
`{ id: 271, value: ["Example"], description: "Example", computed: "Example" }`.
|
||||||
A tag that exifreader has no name for is keyed `undefined-<tag number>`. The
|
A tag exifreader has no name for is keyed `undefined-<tag number>`. The tags
|
||||||
tags of the thumbnail embedded in the file are under `Thumbnail`, so they
|
of the thumbnail embedded in the file are under `Thumbnail`, so they cannot
|
||||||
cannot hide the main image's tags of the same name. The thumbnail image itself
|
hide the main image's tags of the same name. The thumbnail image itself is
|
||||||
is left out. Any file exifreader cannot read gives `{}`, and a video gives
|
left out. Any file exifreader cannot read gives `{}`, and a video gives `{}`
|
||||||
`{}` without being downloaded.
|
without being downloaded.
|
||||||
- `await photo.make(opts?)` returns one common EXIF field, picked from the tags
|
- `await photo.make(opts?)` returns one common EXIF field, picked from the tags
|
||||||
`exif()` returns and typed as in `PhotoExif`, or `undefined` when the file
|
`exif()` returns and typed as in `PhotoExif`, or `undefined` when the file
|
||||||
lacks it. These methods do the same for their fields:
|
lacks it. These methods do the same for their fields:
|
||||||
@@ -1279,9 +1234,7 @@ symlinks. Once the lock is lost, it rejects with
|
|||||||
`BackupOptions`:
|
`BackupOptions`:
|
||||||
|
|
||||||
- `downloadDirectory`: where the tree lives. Default: the library's.
|
- `downloadDirectory`: where the tree lives. Default: the library's.
|
||||||
- `includeOriginals`: fetch and store originals. Default `true`. With `false`,
|
- `includeOriginals`: fetch and store originals. Default `true`.
|
||||||
the backup also fetches no machine-learning data, and writes no JSON beside
|
|
||||||
the originals and no symlinks.
|
|
||||||
- `includeThumbnails`: also store thumbnails as `thumbnails/<fileID>.jpg` beside
|
- `includeThumbnails`: also store thumbnails as `thumbnails/<fileID>.jpg` beside
|
||||||
`collections/`. Default `false`. `quak backup` does not set it.
|
`collections/`. Default `false`. `quak backup` does not set it.
|
||||||
- `onlyAlbumNames`: back up only the albums with these names.
|
- `onlyAlbumNames`: back up only the albums with these names.
|
||||||
@@ -1298,22 +1251,18 @@ releases it once the backup ends.
|
|||||||
|
|
||||||
### Request pools
|
### Request pools
|
||||||
|
|
||||||
`RequestPools` holds three independent pools, each with its own limit on how
|
`RequestPools` holds three independent bounded pools, because Ente meters these
|
||||||
many requests run at once, because Ente meters these three kinds of traffic
|
three kinds of traffic differently:
|
||||||
differently:
|
|
||||||
|
|
||||||
- metadata, with 10 slots: only the machine-learning data requests;
|
- metadata, with 10 slots;
|
||||||
- content, with 5: originals;
|
- content, with 5;
|
||||||
- thumbnails, with 25: thumbnails.
|
- thumbnails, with 25.
|
||||||
|
|
||||||
Only the requests named above go through the pools. A refresh,
|
Construct one with
|
||||||
`freshCollections()` and `freshFiles()` send theirs outside them. Construct one
|
|
||||||
with
|
|
||||||
`new RequestPools({ metadataConcurrency, contentConcurrency, thumbnailConcurrency })`
|
`new RequestPools({ metadataConcurrency, contentConcurrency, thumbnailConcurrency })`
|
||||||
and pass it as `pools`. Each pool serves on-demand work ahead of background
|
and pass it as `pools`. Each pool serves on-demand work ahead of background
|
||||||
work. Two tasks given to `BoundedPool.run()` with the same `key` run once while
|
work. A task submitted under a key that is still pending is not run a second
|
||||||
the first is pending, and both get its result. An idle pool never lends its
|
time. An idle pool never lends its slots to a busy one.
|
||||||
slots to a busy one.
|
|
||||||
|
|
||||||
`lib.backup()` works on as many originals at once as the content pool allows, 5
|
`lib.backup()` works on as many originals at once as the content pool allows, 5
|
||||||
by default. `quak backup` uses the default. The limit covers everything it does
|
by default. `quak backup` uses the default. The limit covers everything it does
|
||||||
@@ -1383,37 +1332,34 @@ They differ because a download's length depends on the file and the link. A
|
|||||||
whole-transfer deadline short enough to catch a hung connection would cancel a
|
whole-transfer deadline short enough to catch a hung connection would cancel a
|
||||||
large video on a slow link that is still making progress. The download deadline
|
large video on a slow link that is still making progress. The download deadline
|
||||||
restarts each time bytes arrive, so a slow download runs as long as it keeps
|
restarts each time bytes arrive, so a slow download runs as long as it keeps
|
||||||
moving, and one that stalls is aborted after 60 seconds of silence. The download
|
moving, and one that stalls is aborted after 60 seconds of silence. It covers
|
||||||
deadline covers the wait for the headers as well as the body. There is no limit
|
the wait for the headers as well as the body. There is no limit on the total
|
||||||
on the total length of a download.
|
length of a download.
|
||||||
|
|
||||||
A request that changes state is not blindly replayed. Every `POST` and `PUT` in
|
A request that changes state is not blindly replayed. Every `POST` and `PUT` in
|
||||||
the endpoint list under "HTTP API" goes through this rule. Verifying a second
|
the endpoint list under "HTTP API" goes through this rule. Verifying a second
|
||||||
factor, for one, uses up one of a small number of attempts. Such a request is
|
factor, for one, uses up one of a small number of attempts. Such a request is
|
||||||
retried only when every error code in its `cause` chain proves that no TCP
|
retried only when every error code in its `cause` chain proves that no TCP
|
||||||
connection to the server ever existed. Three codes prove that:
|
connection to the server ever existed. Three codes prove that: `ENOTFOUND` and
|
||||||
|
`EAI_AGAIN`, when name resolution produced no address, and `ECONNREFUSED`, when
|
||||||
- `ENOTFOUND`: name resolution found no address;
|
the peer refused the connection. A 5xx, a reset mid-flight and a deadline are
|
||||||
- `EAI_AGAIN`: name resolution failed for the moment;
|
all left to the caller, because each of them can happen after the server has
|
||||||
- `ECONNREFUSED`: the peer refused the connection.
|
already acted. These requests do not follow redirects either: a redirect means
|
||||||
|
the server already received the request, so it is reported as an error and not
|
||||||
A 5xx, a reset mid-flight and a deadline are all left to the caller, because
|
retried.
|
||||||
each of them can happen after the server has already acted. These requests do
|
|
||||||
not follow redirects either: a redirect means the server already received the
|
|
||||||
request, so it is reported as an error and not retried.
|
|
||||||
|
|
||||||
The routing errors `EHOSTUNREACH`, `ENETUNREACH` and `ENETDOWN` are left out of
|
The routing errors `EHOSTUNREACH`, `ENETUNREACH` and `ENETDOWN` are left out of
|
||||||
that rule although they look like connect-time failures. On Linux they are also
|
that rule although they look like connect-time failures. On Linux they are also
|
||||||
delivered on an established socket, after the request may have been written.
|
delivered on an established socket, after the request may have been written.
|
||||||
They stay retryable for requests that change nothing. Bun's `ConnectionRefused`
|
They stay retryable for requests that change nothing. Bun's `ConnectionRefused`
|
||||||
and `FailedToOpenSocket` are treated the same way: a request that changes
|
and `FailedToOpenSocket` are treated the same way: a request that changes
|
||||||
nothing is retried on them, and a `POST` or `PUT` is not replayed on them. Bun
|
nothing is retried on them, and a `POST` or `PUT` is not replayed on them. When
|
||||||
reuses a connection it kept open from an earlier request. When that connection
|
a connection bun kept open from an earlier request closes before any answer, bun
|
||||||
closes before any answer, bun sends the request again on a new one. If that one
|
sends the request again on a new connection. If that one fails, bun gives one of
|
||||||
fails, bun gives one of these codes although the server may already have the
|
these codes although the server may already have the request. The upload of a
|
||||||
request. The upload of a thumbnail to its presigned URL is exempt from the rule.
|
thumbnail to its presigned URL is exempt from the rule. It stores one whole
|
||||||
It stores one whole object at one key in one request, so replaying it has no
|
object at one key in one request, so replaying it has no partial state to
|
||||||
partial state to damage.
|
damage.
|
||||||
|
|
||||||
A download is retried as a whole: the request, the reading of the body and the
|
A download is retried as a whole: the request, the reading of the body and the
|
||||||
decryption. A socket reset after the response headers have arrived surfaces in
|
decryption. A socket reset after the response headers have arrived surfaces in
|
||||||
@@ -1427,20 +1373,19 @@ One imprecision is deliberate. When a body ends partway through an encrypted
|
|||||||
chunk, the authentication check fails and carries no sign of why, so a cut
|
chunk, the authentication check fails and carries no sign of why, so a cut
|
||||||
connection and corrupt bytes look the same. quak reports that as truncation,
|
connection and corrupt bytes look the same. quak reports that as truncation,
|
||||||
which means it is retried. quak can tell the two apart only in a body of more
|
which means it is retried. quak can tell the two apart only in a body of more
|
||||||
than one chunk. There, a chunk that fails while the stream carries on past it
|
than one chunk: a chunk that fails while the stream carries on past it stays an
|
||||||
stays an authentication failure and is not retried. Most thumbnails and every
|
authentication failure and is not retried. Most thumbnails and every small file
|
||||||
small file are one chunk. There a wrong key, corrupt bytes and a cut connection
|
are one chunk. There a wrong key, corrupt bytes and a cut connection look alike,
|
||||||
look alike, and all are retried. The attempt count limits the extra retries. In
|
and all are retried. The cost is bounded by the attempt count, and it buys never
|
||||||
return, quak never keeps a truncated file.
|
keeping a truncated file.
|
||||||
|
|
||||||
### The `Client`
|
### The `Client`
|
||||||
|
|
||||||
`Client.login({ email, password, totp?, emailOTP?, apiOptions? })` logs in and
|
`Client.login({ email, password, totp?, emailOTP?, apiOptions? })` logs in and
|
||||||
resolves to a `Client`. `totp` and `emailOTP` are callbacks that return the code
|
resolves to a `Client`. `totp` and `emailOTP` are callbacks that return the code
|
||||||
when the account asks for one. Without the callback the account needs, login
|
when the account asks for one. Without the callback the account needs, login
|
||||||
throws. A passkey challenge always throws. After an email code, login expects
|
throws. A passkey challenge always throws. `apiOptions` configures the HTTP
|
||||||
the key attributes and the token. When the server asks for a second factor
|
client (see below).
|
||||||
instead, login throws. `apiOptions` configures the HTTP client (see below).
|
|
||||||
|
|
||||||
The `Client` holds the session token, the master key, the secret key and the
|
The `Client` holds the session token, the master key, the secret key and the
|
||||||
public key in memory. There is no on-disk session store in the library; the
|
public key in memory. There is no on-disk session store in the library; the
|
||||||
@@ -1450,18 +1395,10 @@ write wherever it likes. `Client.fromJSON(snapshot, apiOptions?)` restores a
|
|||||||
working client from that snapshot without logging in again. It checks every
|
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.
|
field and each key's length first, and throws an error naming the bad field.
|
||||||
|
|
||||||
`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
|
|
||||||
`sodium` imported from `libsodium-wrappers-sumo`, which quak depends on. The
|
|
||||||
exported `beginLogin`, `unwrapAuth`, `decryptCollection`, `decryptFile`,
|
|
||||||
`downloadFile` and `downloadThumbnail` need the same.
|
|
||||||
|
|
||||||
`client.logout()` clears the token and zeroes the key buffers in place. After
|
`client.logout()` clears the token and zeroes the key buffers in place. After
|
||||||
it, every other method on that client throws. `logout()` does not contact the
|
it, every other method on that client throws. It does not contact the server, so
|
||||||
server, so the token stays valid there and in any saved snapshot. Call
|
the token stays valid there and in any saved snapshot;
|
||||||
`await client.logoutOnServer()` before `logout()` to end the session on the
|
`await client.logoutOnServer()` ends the session on the server first.
|
||||||
server.
|
|
||||||
|
|
||||||
`client.whoami()` returns `{ email, userID }`.
|
`client.whoami()` returns `{ email, userID }`.
|
||||||
|
|
||||||
@@ -1469,20 +1406,18 @@ server.
|
|||||||
`client.filesSince({ collectionID, collectionKey, sinceTime })` enumerate the
|
`client.filesSince({ collectionID, collectionKey, sinceTime })` enumerate the
|
||||||
changes since a microsecond timestamp, with `0` for everything. Each returns:
|
changes since a microsecond timestamp, with `0` for everything. Each returns:
|
||||||
|
|
||||||
- `collections` or `files`: the decrypted records that are not deleted;
|
- `collections` or `files`: the decrypted live records;
|
||||||
- `deleted`: the IDs of deleted records;
|
- `deleted`: the IDs of deleted records;
|
||||||
- `undecryptable`: each record that did not decrypt, as `{ id, error }`;
|
- `undecryptable`: each record that did not decrypt, as `{ id, error }`;
|
||||||
- `cursor`: the timestamp to pass next time.
|
- `cursor`: the timestamp to pass next time.
|
||||||
|
|
||||||
`client.listCollections()` and `client.listFiles(collectionID, collectionKey)`
|
`client.listCollections()` and `client.listFiles(collectionID, collectionKey)`
|
||||||
return only the decrypted records that are not deleted, from `sinceTime` `0`.
|
return only the live records, from `sinceTime` `0`.
|
||||||
`client.fetchMLData({ fileIDs, fileKeys })` fetches and decrypts the
|
`client.fetchMLData({ fileIDs, fileKeys })` fetches and decrypts the
|
||||||
machine-learning data of up to 200 files. `client.downloadFile(file, outPath?)`
|
machine-learning data of up to 200 files. `client.downloadFile(file, outPath?)`
|
||||||
and `client.downloadThumbnail(file, outPath?)` download and decrypt one file to
|
and `client.downloadThumbnail(file, outPath?)` download and decrypt one file to
|
||||||
`outPath`. For a live photo, `downloadFile` writes the image and the video, each
|
`outPath`. Without `outPath`, they write to the working directory under the
|
||||||
named `outPath` with its own extension. `quak get --out` instead puts the image
|
names `quak get` and `quak get-thumb` would use.
|
||||||
at the path itself. Without `outPath`, they write to the working directory under
|
|
||||||
the names `quak get` and `quak get-thumb` would use.
|
|
||||||
|
|
||||||
`ApiClientOptions`:
|
`ApiClientOptions`:
|
||||||
|
|
||||||
@@ -1676,9 +1611,8 @@ Login derives the keys in this order:
|
|||||||
token, or first asks for a second factor. quak answers a TOTP request with
|
token, or first asks for a second factor. quak answers a TOTP request with
|
||||||
the code (`POST /users/two-factor/verify`); the server then returns the key
|
the code (`POST /users/two-factor/verify`); the server then returns the key
|
||||||
attributes and the token. quak cannot answer a passkey request. When the
|
attributes and the token. quak cannot answer a passkey request. When the
|
||||||
account has email verification on (`isEmailMFAEnabled`), an email code
|
account has email MFA on (`isEmailMFAEnabled`), an email code replaces SRP,
|
||||||
replaces SRP. quak then expects the key attributes and the token in the
|
and the server returns the key attributes and the token after it.
|
||||||
answer, and fails the login when the server asks for a second factor instead.
|
|
||||||
5. `crypto_secretbox_open_easy` over the encrypted master key with the KEK
|
5. `crypto_secretbox_open_easy` over the encrypted master key with the KEK
|
||||||
yields the 32-byte master key.
|
yields the 32-byte master key.
|
||||||
6. `crypto_secretbox_open_easy` over the encrypted secret key with the master
|
6. `crypto_secretbox_open_easy` over the encrypted secret key with the master
|
||||||
@@ -1757,35 +1691,36 @@ This repository adheres to the
|
|||||||
standard. Normalized scripts in `script/` are the entrypoints for the
|
standard. Normalized scripts in `script/` are the entrypoints for the
|
||||||
development workflow, and most Makefile targets are thin shims that call them.
|
development workflow, and most Makefile targets are thin shims that call them.
|
||||||
The scripts are POSIX `sh`, not bash, so they run in minimal containers such as
|
The scripts are POSIX `sh`, not bash, so they run in minimal containers such as
|
||||||
Alpine. Of these, only `bootstrap`, `setup`, `test` and `cibuild` come from the
|
Alpine. The scripts marked "ours" are this repository's own additions to the
|
||||||
standard. The others are additions to it.
|
standard.
|
||||||
|
|
||||||
- `script/bootstrap`: install all dependencies. It installs `make`, `git`, Node
|
- `script/bootstrap`: install all dependencies. It installs `make`, `git`, Node
|
||||||
and `yarn` when they are missing, then runs `yarn install --frozen-lockfile`.
|
and `yarn` when they are missing, then runs `yarn install --frozen-lockfile`.
|
||||||
- `script/setup`: set up the repo for development after a fresh clone. It runs
|
- `script/setup`: set up the repo for development after a fresh clone. It runs
|
||||||
`script/bootstrap`, then `script/install-precommit`.
|
`script/bootstrap`, then `script/install-precommit`.
|
||||||
- `script/projectname`: print the project name. `script/docker` uses it for the
|
- `script/projectname` (ours): print the project name. `script/docker` uses it
|
||||||
image tag.
|
for the image tag.
|
||||||
- `script/build`: compile the TypeScript sources into `dist/` and stamp the
|
- `script/build` (ours): compile the TypeScript sources into `dist/` and stamp
|
||||||
version into `dist/package.json`. Then check that the entrypoints
|
the version into `dist/package.json`. Then check that the entrypoints
|
||||||
`package.json` declares (`main`, `types`, `bin`) were written, and make the
|
`package.json` declares (`main`, `types`, `bin`) were written, and make the
|
||||||
command line executable.
|
command line executable.
|
||||||
- `script/version`: print the version `script/build` stamps (see "Version").
|
- `script/version` (ours): print the version `script/build` stamps (see
|
||||||
|
"Version").
|
||||||
- `script/test`: run the test suite, by building the `test` phase of the
|
- `script/test`: run the test suite, by building the `test` phase of the
|
||||||
`Dockerfile`. Requires Docker.
|
`Dockerfile`. Requires Docker.
|
||||||
- `script/lint`: run eslint and a prettier check, by building the `lint` phase
|
- `script/lint`: run eslint and a prettier check, by building the `lint` phase
|
||||||
of the `Dockerfile`. Requires Docker.
|
of the `Dockerfile`. Requires Docker.
|
||||||
- `script/fmt`: format all files with prettier. It rewrites files in place.
|
- `script/fmt`: format all files with prettier. It rewrites files in place.
|
||||||
- `script/fmt-check`: check formatting on the host. It is read-only.
|
- `script/fmt-check`: check formatting on the host. It is read-only.
|
||||||
- `script/check`: run all checks: `test`, `lint`, `fmt-check`.
|
- `script/check` (ours): run all checks: `test`, `lint`, `fmt-check`.
|
||||||
- `script/docker`: build the image, tagged with `script/projectname`.
|
- `script/docker`: build the image, tagged with `script/projectname`.
|
||||||
- `script/cibuild`: what CI runs: `script/bootstrap`, then `script/check`, then
|
- `script/cibuild`: what CI runs: `script/bootstrap`, then `script/check`, then
|
||||||
the image build.
|
the image build.
|
||||||
- `script/precommit`: run by the git pre-commit hook. It runs `script/lint` and
|
- `script/precommit` (ours): run by the git pre-commit hook. It runs
|
||||||
`script/fmt-check`, and not the tests, so that a commit of failing tests can
|
`script/lint` and `script/fmt-check`, and not the tests, so that a commit of
|
||||||
land (see "Development workflow").
|
failing tests can land (see "Development workflow").
|
||||||
- `script/install-precommit`: install the git pre-commit hook. `make hooks`
|
- `script/install-precommit` (ours): install the git pre-commit hook.
|
||||||
calls it.
|
`make hooks` calls it.
|
||||||
|
|
||||||
The Makefile targets are:
|
The Makefile targets are:
|
||||||
|
|
||||||
@@ -1852,8 +1787,8 @@ empty, `dev` or `unknown`, because such a build could not be traced to its
|
|||||||
commit.
|
commit.
|
||||||
|
|
||||||
`.dockerignore` therefore does not leave out `.git`, so any `docker build .` of
|
`.dockerignore` therefore does not leave out `.git`, so any `docker build .` of
|
||||||
a clone stamps the commit it was built from. A clone of depth 1 stamps a tag
|
a clone stamps the commit it was built from. A shallow clone stamps a tag only
|
||||||
only when its commit carries one, and otherwise the short commit.
|
when the cloned commit itself carries one, and otherwise the short commit.
|
||||||
`.dockerignore` does leave out `.git/config`, which holds the clone's remote URL
|
`.dockerignore` does leave out `.git/config`, which holds the clone's remote URL
|
||||||
and any credential in it; `git describe` does not need that file.
|
and any credential in it; `git describe` does not need that file.
|
||||||
`script/docker` and `script/cibuild` work out the version on the host, with
|
`script/docker` and `script/cibuild` work out the version on the host, with
|
||||||
@@ -1903,18 +1838,12 @@ These apply to people and to coding agents alike.
|
|||||||
<https://git.eeqj.de/sneak/prompts> and covers repository structure, tooling,
|
<https://git.eeqj.de/sneak/prompts> and covers repository structure, tooling,
|
||||||
Makefile targets, Dockerfile conventions, dependency pinning and commit
|
Makefile targets, Dockerfile conventions, dependency pinning and commit
|
||||||
hygiene. Never `git add -A`. Never force-push `main`.
|
hygiene. Never `git add -A`. Never force-push `main`.
|
||||||
- Dependencies are pinned:
|
- Every dependency is pinned:
|
||||||
- npm packages by hash, in `yarn.lock`;
|
- npm packages by hash, in `yarn.lock`;
|
||||||
- container images by digest;
|
- container images by digest;
|
||||||
- the nvm archive `script/bootstrap` downloads, and the Python packages
|
- the nvm archive `script/bootstrap` downloads, and the Python packages
|
||||||
`bin/clip-keyword-vectors` installs, by SHA-256 hash;
|
`bin/clip-keyword-vectors` installs, by SHA-256 hash;
|
||||||
- Node and yarn, when `script/bootstrap` installs them, by version.
|
- Node and yarn by version, in `script/bootstrap`.
|
||||||
- Not pinned:
|
|
||||||
- bun, which `make build-bin` takes from `nix-shell -p bun`;
|
|
||||||
- `make` and `git`, which `script/bootstrap` installs from the system
|
|
||||||
package manager when they are missing, as every `Dockerfile` stage does;
|
|
||||||
- `bash` and `curl`, which `script/bootstrap` installs the same way when it
|
|
||||||
has to install Node.
|
|
||||||
- Follow the workflow above: branches off `next`, tests first and committed
|
- Follow the workflow above: branches off `next`, tests first and committed
|
||||||
failing, and `next` merged into `main` by sneak alone.
|
failing, and `next` merged into `main` by sneak alone.
|
||||||
- Before every commit, `make lint` and `make fmt-check` must pass. `make lint`
|
- Before every commit, `make lint` and `make fmt-check` must pass. `make lint`
|
||||||
|
|||||||
+7
-10
@@ -1,18 +1,15 @@
|
|||||||
// The CLI's log level, and the one function the commands write stderr lines
|
// The CLI's log level, and the one function the commands write stderr lines
|
||||||
// through.
|
// through.
|
||||||
//
|
//
|
||||||
// Every line quak logs goes to stderr, at one of six levels, from the
|
// Every line quak prints on stderr is a log line at one of six levels, from
|
||||||
// quietest: fatal, error, warning, info, debug, trace. A run prints the lines
|
// the quietest: fatal, error, warning, info, debug, trace. A run prints the
|
||||||
// at its own level and at every quieter one. Its level is info unless `-v` or
|
// lines at its own level and at every quieter one. Its level is info unless
|
||||||
// `-q` asks for another (see "What quak prints" in the README). A command that
|
// `-v` or `-q` asks for another (see "What quak prints" in the README). Every
|
||||||
// runs and exits non-zero logs a fatal line saying why, so even `-q`, which
|
// command that exits non-zero logs a fatal line saying why, so even `-q`, which
|
||||||
// prints only fatal lines, never leaves such an exit unexplained. An error
|
// prints only fatal lines, never leaves a non-zero exit unexplained. An error
|
||||||
// line reports something that failed, such as a file or an album, and a
|
// line reports something that failed, such as a file or an album, and a
|
||||||
// warning something that did not fail but needs the user's attention. stdout
|
// warning something that did not fail but needs the user's attention. stdout
|
||||||
// carries a command's output and the prompts of `login`, and is not a log.
|
// carries only a command's output and is not a log.
|
||||||
// Two things on stderr are not log lines: the `error:` message for a
|
|
||||||
// command line that quak cannot parse, and the usage printed when no command
|
|
||||||
// is given.
|
|
||||||
//
|
//
|
||||||
// The library itself writes no lines: its layers report through callbacks, and
|
// The library itself writes no lines: its layers report through callbacks, and
|
||||||
// the CLI logs what they report. `logRequests` gives the API client options
|
// the CLI logs what they report. `logRequests` gives the API client options
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ describe("what the library reports", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Each operation is named in plain words.
|
// Each operation is named in plain words, as the README names it.
|
||||||
it("logs a refresh's progress at info, and its start and end at debug", () => {
|
it("logs a refresh's progress at info, and its start and end at debug", () => {
|
||||||
const { log, lines } = collect();
|
const { log, lines } = collect();
|
||||||
const onProgress = logRefresh(log);
|
const onProgress = logRefresh(log);
|
||||||
|
|||||||
Reference in New Issue
Block a user