Read all five documents against the code and fix what the sixth review found
check / check (push) In progress
check / check (push) In progress
Fixes the four findings of the sixth review: `snapshot remove` with `--local-only`, the two comma-chained permission lists, six undefined terms, and the sentence about an unfinished snapshot in the local index. Then reads every sentence of `README.md`, `ARCHITECTURE.md`, `docs/DATAMODEL.md`, `docs/REPOSTRUCTURE.md` and the comments in `config.example.yml` against the code for four faults: a command's needs or behaviour stated without regard to a flag, a false absolute, three or more things chained with commas, and a technical word not defined on first use. Each sentence changed is listed on the pull request. The help string of `snapshot remove --dry-run` now says it needs no permission to delete. Model: fable-5-1
This commit is contained in:
1 parent
a133b8838e
commit
46b2a9fc78
6 files changed
+341
-193
No files matched your search
+36
-26
@@ -63,13 +63,13 @@ source files
|
||||
[`docs/DATAMODEL.md`](docs/DATAMODEL.md#code) says which type serves which
|
||||
table.
|
||||
- `internal/storage`: the `Storer` interface and its three implementations, for
|
||||
S3, a local directory and rclone.
|
||||
S3, a local directory and [rclone](https://rclone.org/).
|
||||
- `internal/s3`: the S3 client that the S3 `Storer` uses.
|
||||
- `internal/config`: loads and checks the config file.
|
||||
- `internal/ui`: formats the status lines, warnings and errors a user sees.
|
||||
- `internal/log`: the logger. It writes to stderr.
|
||||
- `internal/pidlock`: the lock that keeps two data-changing commands from
|
||||
running at once.
|
||||
- `internal/pidlock`: the lock that `RunWithApp` takes for a command that
|
||||
changes data, so that only one runs at a time.
|
||||
- `internal/globals`: the version, commit and commit date, set when the binary
|
||||
is built.
|
||||
- `internal/types`: distinct types for IDs, hashes and paths.
|
||||
@@ -114,8 +114,12 @@ A command runs through `cli.RunOperation`. It works like this:
|
||||
waits for the function to return, so that its temporary files are removed.
|
||||
- The wait ends after 30 seconds at most.
|
||||
|
||||
The `config` commands, `database delete`, `completion` and `version` do not
|
||||
build the application.
|
||||
These commands do not build the application:
|
||||
|
||||
- the `config` commands
|
||||
- `database delete`
|
||||
- `completion`
|
||||
- `version`
|
||||
|
||||
## Backup
|
||||
|
||||
@@ -163,7 +167,8 @@ A new `Scanner` is created for each snapshot, from `ScannerFactory`. It owns a
|
||||
- `repairInterruptedBlobs` deletes the records of blobs whose upload never
|
||||
finished. Their data is then stored again.
|
||||
- `loadDatabaseState` loads the known files and chunks into memory.
|
||||
- `scanPhase` walks the path. It sorts each entry into changed or unchanged.
|
||||
- `scanPhase` walks the path. It sorts each regular file and symlink into
|
||||
changed or unchanged.
|
||||
- `detectDeletedFiles` counts the files that the previous snapshot of this name
|
||||
had and that are now gone.
|
||||
- `batchAddFilesToSnapshot` records the unchanged files in the new snapshot.
|
||||
@@ -184,8 +189,8 @@ Data is stored once at two levels.
|
||||
|
||||
- **Files.** A file is not read again when its record in the index still matches
|
||||
it.
|
||||
- **Chunks.** A chunk is named by the SHA-256 hash of its content. A chunk whose
|
||||
hash is already in the index is not stored again.
|
||||
- **Chunks.** A chunk is named by the SHA-256 hash of its content. A chunk that
|
||||
an uploaded blob already holds, according to the index, is not stored again.
|
||||
|
||||
A file's record matches when all of these are unchanged:
|
||||
|
||||
@@ -208,8 +213,8 @@ on the content, so an insertion early in a file moves only nearby chunks.
|
||||
this size. Only a file's last chunk can be smaller than a quarter of it. With
|
||||
the default of 10 MB, those limits are 40 MB and 2.5 MB.
|
||||
|
||||
`ChunkReaderStreaming` passes each chunk to a callback as it is found. The whole
|
||||
file is never held in memory.
|
||||
`ChunkReaderStreaming` passes each chunk to a callback as it is found. The
|
||||
chunker buffers at most twice the largest chunk size.
|
||||
|
||||
### Blobs
|
||||
|
||||
@@ -217,8 +222,9 @@ file is never held in memory.
|
||||
|
||||
- It gives the blob a UUID and creates a temporary file.
|
||||
- It adds each chunk's bytes to a running SHA-256 hash.
|
||||
- In the same pass it compresses the bytes with zstd and encrypts them with
|
||||
age. The result goes to the temporary file.
|
||||
- In the same pass it compresses the bytes with
|
||||
[zstd](https://github.com/facebook/zstd) and encrypts them with
|
||||
[age](https://age-encryption.org/). The result goes to the temporary file.
|
||||
- It records each chunk's offset and length within the blob.
|
||||
- It closes the blob when the next chunk would pass `blob_size_limit`, and at
|
||||
the end of each path.
|
||||
@@ -250,8 +256,8 @@ chunks is uploaded.
|
||||
- `MarkSnapshotComplete` marks the snapshot complete.
|
||||
|
||||
Completion is recorded last on purpose. A backup that stops during the upload
|
||||
leaves the snapshot unfinished, and the next backup removes it and stores its
|
||||
data again.
|
||||
leaves the snapshot unfinished, and the next backup removes it and backs up its
|
||||
files again.
|
||||
|
||||
`ExportSnapshotMetadata` works on a copy of the index, never on the index
|
||||
itself:
|
||||
@@ -282,11 +288,13 @@ therefore marks a backup that did not finish.
|
||||
temporary directory.
|
||||
- `getFilesToRestore` selects the files, from the paths given.
|
||||
- `restoreAllFiles` downloads blobs and writes the files.
|
||||
- With `--verify`, `verifyRestoredFiles` reads each restored file and checks its
|
||||
chunk hashes.
|
||||
- With `--verify`, `verifyRestoredFiles` reads each restored regular file and
|
||||
checks its chunk hashes.
|
||||
|
||||
The restore does not read the local index or the manifest. It finds each file's
|
||||
chunks and blobs through the downloaded database.
|
||||
Given a snapshot ID, the restore does not read the local index. Given a remote
|
||||
key, it reads the local index only to check the destination marker. It does not
|
||||
read the manifest. It finds each file's chunks and blobs through the downloaded
|
||||
database.
|
||||
|
||||
### Checks on the downloaded database
|
||||
|
||||
@@ -313,12 +321,13 @@ database is honest. The restore therefore checks it:
|
||||
- `restoreSweeper` deletes a cached blob once no remaining file needs it.
|
||||
|
||||
A regular file is created with mode `0600` and gets its stored mode after its
|
||||
content is written. A directory gets its stored mode, owner and modification
|
||||
time after the whole restore.
|
||||
content is written. A directory gets its stored mode and modification time after
|
||||
the whole restore, and its owner when the command runs as root.
|
||||
|
||||
## Verify
|
||||
|
||||
`Vaultik.VerifySnapshotWithOptions` checks a snapshot without the private key:
|
||||
Without `--deep`, `Vaultik.VerifySnapshotWithOptions` checks a snapshot without
|
||||
the private key:
|
||||
|
||||
- It downloads the manifest.
|
||||
- It checks that the snapshot's encrypted database is present.
|
||||
@@ -337,7 +346,7 @@ time after the whole restore.
|
||||
|
||||
## Prune
|
||||
|
||||
`Vaultik.Prune` does three steps:
|
||||
`Vaultik.Prune` calls `EnsureStorageBinding` and then does three steps:
|
||||
|
||||
- `CleanupLocalSnapshots` drops from the local index each snapshot whose
|
||||
manifest is gone from the destination.
|
||||
@@ -382,17 +391,18 @@ With no `storage_url`, it builds the S3 implementation from the `s3` settings.
|
||||
Two checks keep the local index and the destination in step.
|
||||
|
||||
- `checkDestinationMarker` looks for `vaultik-destination.txt`. It fails when
|
||||
the file is reported missing while the local index holds snapshots.
|
||||
the file is reported missing while the local index holds snapshots. It skips
|
||||
the check when the local index belongs to another destination.
|
||||
- `EnsureStorageBinding` records the configured destination in the local index
|
||||
on first use. Later it fails when the config names a different destination.
|
||||
|
||||
Every command that changes data calls `EnsureStorageBinding` first.
|
||||
Every command that takes the lock calls `EnsureStorageBinding` first.
|
||||
|
||||
## Concurrency
|
||||
|
||||
- Each open database uses one connection, so its queries run one at a time.
|
||||
- The index runs in SQLite's WAL mode. A second process can therefore read it
|
||||
while a backup writes to it.
|
||||
- The index runs in SQLite's write-ahead log (WAL) mode. A second process can
|
||||
therefore read it while a backup writes to it.
|
||||
- `Packer` guards its state with a mutex.
|
||||
- `Scanner` holds `packerMu` while a chunk is added to the packer and while a
|
||||
blob is closed.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
`vaultik` is an MIT-licensed command-line tool for incremental, encrypted
|
||||
backups, written in Go by [@sneak](https://sneak.berlin). It encrypts your files
|
||||
with a public key and uploads them to an S3-compatible object store, a local
|
||||
directory or an rclone remote.
|
||||
directory or any storage service that [rclone](https://rclone.org/) supports.
|
||||
|
||||
The machine being backed up holds only the public key. It cannot decrypt its own
|
||||
backups. You keep the private key somewhere else. A backup never needs it.
|
||||
@@ -25,13 +25,14 @@ It was written to meet these requirements together:
|
||||
|
||||
## Features
|
||||
|
||||
- Encryption with [age](https://age-encryption.org/) (X25519 and
|
||||
ChaCha20-Poly1305), to one or more public keys.
|
||||
- Encryption with [age](https://age-encryption.org/), to one or more public
|
||||
keys. age uses X25519 keys and the ChaCha20-Poly1305 cipher.
|
||||
- Incremental backups. A file is read again only when it has changed, or when
|
||||
part of its content has to be stored again.
|
||||
- Deduplication. Identical content is stored once, across files and across
|
||||
snapshots.
|
||||
- zstd compression at a level you choose.
|
||||
- Compression with [zstd](https://github.com/facebook/zstd), at a level you
|
||||
choose.
|
||||
- Small files are packed together into large objects. This keeps the number of
|
||||
S3 requests low.
|
||||
- File contents, names, paths and metadata are encrypted at the destination.
|
||||
@@ -91,12 +92,13 @@ vaultik snapshot list
|
||||
|
||||
The default config is written for macOS. It backs up your home directory and
|
||||
`/Applications`, with a list of excludes. A configured path that does not exist
|
||||
stops the backup. On another system, run `vaultik config edit` first and change
|
||||
the `snapshots` section.
|
||||
stops the backup, unless you give `--skip-errors`. On another system, run
|
||||
`vaultik config edit` first and change the `snapshots` section.
|
||||
|
||||
On macOS, grant your terminal app Full Disk Access before the first backup. The
|
||||
setting is under System Settings, Privacy & Security, Full Disk Access. Without
|
||||
it the backup stops with a permission error on a protected directory.
|
||||
it the backup stops with a permission error on a protected directory, unless you
|
||||
give `--skip-errors`.
|
||||
|
||||
## Everyday use
|
||||
|
||||
@@ -155,7 +157,7 @@ Use that remote key in place of the snapshot ID:
|
||||
```sh
|
||||
export VAULTIK_AGE_SECRET_KEY="$(cat vaultik_backup_private_key.txt)"
|
||||
|
||||
# Restore everything, then check the chunk hashes of every restored file.
|
||||
# Restore everything, then check the chunk hashes of the restored files.
|
||||
vaultik snapshot restore --verify <remote-key> ~/vaultik-restore
|
||||
|
||||
# Optionally, also check every blob of the snapshot at the destination.
|
||||
@@ -194,10 +196,14 @@ vaultik version
|
||||
|
||||
### What every command needs
|
||||
|
||||
`config init`, `completion` and `version` need nothing.
|
||||
`completion` and `version` need nothing. `config init` needs write access to the
|
||||
config directory.
|
||||
|
||||
`config edit`, `config get` and `config set` need a config file that exists. It
|
||||
does not have to be complete.
|
||||
These commands need a config file that exists, even an incomplete one:
|
||||
|
||||
- `config edit`
|
||||
- `config get`
|
||||
- `config set`
|
||||
|
||||
Every other command needs a config file that loads. See
|
||||
[Configuration](#configuration) for the checks.
|
||||
@@ -219,24 +225,34 @@ sets file owners only when it runs as root.
|
||||
[Where the config file is](#where-the-config-file-is).
|
||||
- `--verbose`, `-v`: write informational log records to stderr.
|
||||
- `--debug`: write debug log records to stderr.
|
||||
- `--quiet`, `-q`: print only warnings, errors and the output a script reads.
|
||||
See [Output](#output).
|
||||
- `--quiet`, `-q`: leave out the banner, the status lines, the tables and the
|
||||
reports. See [Output](#output).
|
||||
- `--skip-errors`: skip a file that cannot be read during a backup, or cannot be
|
||||
restored during a restore. Without it the command stops at the first such
|
||||
file.
|
||||
|
||||
`--quiet` and `--cron` override `--verbose` and `--debug`.
|
||||
|
||||
`--skip-errors` applies to `snapshot create` and `snapshot restore` only. A
|
||||
command that skipped anything still finishes, and then exits non-zero.
|
||||
|
||||
### Durations
|
||||
|
||||
`--keep-newer-than`, `--older-than` and `prune_grace_period` take a duration.
|
||||
These take a duration:
|
||||
|
||||
- `--keep-newer-than`
|
||||
- `--older-than`
|
||||
- `prune_grace_period`
|
||||
|
||||
These units are accepted:
|
||||
|
||||
- `d`: days.
|
||||
- `w`: weeks.
|
||||
- `mo`: months of 30 days.
|
||||
- `y`: years of 365 days.
|
||||
- `h`, `m` and `s`: hours, minutes and seconds, as in `36h` or `90m`.
|
||||
- `h`: hours, as in `36h`.
|
||||
- `m`: minutes, as in `90m`.
|
||||
- `s`: seconds.
|
||||
|
||||
`m` means minutes and `mo` means months. Days, weeks, months and years take
|
||||
whole numbers and can be combined, as in `2w3d`.
|
||||
@@ -249,9 +265,9 @@ Writes a default config file, with a comment that explains each setting it
|
||||
lists.
|
||||
|
||||
- It writes to the path from `--config`, or else from `VAULTIK_CONFIG`.
|
||||
- Otherwise it writes to `config.yml` in the platform config directory. That is
|
||||
`~/.config/vaultik/` on Linux and `~/Library/Application Support/vaultik/` on
|
||||
macOS.
|
||||
- Otherwise it writes to `config.yml` in the platform config directory. By
|
||||
default that is `~/.config/vaultik/` on Linux and
|
||||
`~/Library/Application Support/vaultik/` on macOS.
|
||||
- As root it writes to `/etc/vaultik/config.yml` instead.
|
||||
- It creates the file with mode `0600`, because the file can hold credentials.
|
||||
- It refuses to overwrite a file that exists.
|
||||
@@ -275,7 +291,8 @@ Sets one value in the config file. The key is a dotted path, as in
|
||||
- A key that does not exist yet is created. A missing parent becomes a list when
|
||||
the next part of the path is a number, and a map otherwise.
|
||||
- In a list, the index one past the end appends an entry.
|
||||
- The command prints the key it set, and never the value.
|
||||
- The command prints the key it set, and not the value. A refusal for a value
|
||||
of the wrong type quotes the beginning of that value.
|
||||
- It sets the file's mode to `0600` when group or others could read it.
|
||||
|
||||
The command refuses a change that would stop the config from loading. An example
|
||||
@@ -301,7 +318,8 @@ It needs:
|
||||
|
||||
It does not need the private key.
|
||||
|
||||
A file is read again when any of these holds:
|
||||
A file is read when the local index does not know it, or when any of these
|
||||
holds:
|
||||
|
||||
- Its size has changed since the last backup.
|
||||
- Its modification time has changed.
|
||||
@@ -383,9 +401,9 @@ config.
|
||||
|
||||
### `snapshot list`
|
||||
|
||||
Lists the snapshots in the local index and the snapshots at the destination, as
|
||||
one table. Before the table, the command prints the snapshot names and paths in
|
||||
the config.
|
||||
Lists the finished snapshots in the local index and the snapshots at the
|
||||
destination, as one table. Before the table, the command prints the snapshot
|
||||
names and paths in the config.
|
||||
|
||||
It needs permission to list and read at the destination. It needs no private
|
||||
key.
|
||||
@@ -402,7 +420,8 @@ The table has these columns:
|
||||
A snapshot at the destination that is not in the local index is shown as
|
||||
`<remote only:...>`. The dots stand for the first 12 characters of its remote
|
||||
key. Its last two columns read `<remote only>`, because those sizes come from
|
||||
the local index. Its timestamp is the time its backup finished.
|
||||
the local index. Its timestamp is the time its manifest was written, at the end
|
||||
of its backup.
|
||||
|
||||
The command reports these below the table:
|
||||
|
||||
@@ -424,7 +443,8 @@ marker file although the local index holds snapshots.
|
||||
- `remote_key`: the full 64-character remote key.
|
||||
- `timestamp`
|
||||
- `compressed_size`
|
||||
- `uncompressed_size` and `new_chunk_size`: left out for a remote-only snapshot.
|
||||
- `uncompressed_size` and `new_chunk_size`: left out when zero, and for a
|
||||
remote-only snapshot.
|
||||
- `locally_tracked`: whether the snapshot is in the local index.
|
||||
- `remote_present`: whether the snapshot is at the destination. It is `null`
|
||||
when the destination could not be listed.
|
||||
@@ -469,7 +489,8 @@ variable wins when both are set. See [The private key](#the-private-key).
|
||||
- It downloads and decrypts every blob. It checks the hash of each chunk, and
|
||||
the hash that names the blob.
|
||||
|
||||
It stops at the first blob that fails.
|
||||
The presence check counts every missing or wrong-size blob. The download check
|
||||
stops at the first blob that fails.
|
||||
|
||||
`--deep` proves that the stored data is intact and decrypts with your key. It
|
||||
does not prove who wrote the data.
|
||||
@@ -510,20 +531,22 @@ cannot be listed or has no marker file.
|
||||
Removes one snapshot. The argument must be a snapshot ID. A remote key is not
|
||||
accepted.
|
||||
|
||||
It needs the [lock](#locking), and permission to list and delete at the
|
||||
destination. It needs no private key.
|
||||
It needs the [lock](#locking). It needs permission to list and delete at the
|
||||
destination, unless you give `--local-only`. It needs no private key.
|
||||
|
||||
Flags:
|
||||
|
||||
- `--force`, `-f`: do not ask for confirmation.
|
||||
- `--dry-run`: say what would be removed, and remove nothing.
|
||||
- `--dry-run`: say what would be removed, and remove nothing. It needs no
|
||||
permission to delete.
|
||||
- `--local-only`: remove the snapshot from the local index only. The destination
|
||||
is not changed.
|
||||
- `--json`: print the result as JSON. This also skips the confirmation.
|
||||
|
||||
The command removes the snapshot from the local index. It also deletes the
|
||||
snapshot's directory under `metadata/` at the destination. Its blobs stay. Run
|
||||
`vaultik prune` afterwards to delete the blobs that no snapshot uses any more.
|
||||
The command removes the snapshot from the local index. Without `--local-only`,
|
||||
it also deletes the snapshot's directory under `metadata/` at the destination.
|
||||
Its blobs stay. Run `vaultik prune` afterwards to delete the blobs that no
|
||||
snapshot uses any more.
|
||||
|
||||
An ID that is in neither the local index nor the destination is an error. With
|
||||
`--local-only`, the snapshot must be in the local index.
|
||||
@@ -586,8 +609,8 @@ Restore into a new directory that only you can write, not a shared one such as
|
||||
|
||||
Flags:
|
||||
|
||||
- `--verify`: after the restore, read every restored file and check its chunk
|
||||
hashes.
|
||||
- `--verify`: after the restore, read every restored regular file and check its
|
||||
chunk hashes.
|
||||
|
||||
With `--skip-errors`, a file that cannot be restored is reported and skipped,
|
||||
and the command exits non-zero. `--verify` does not check a skipped file.
|
||||
@@ -666,12 +689,13 @@ Reports what the destination holds:
|
||||
It needs permission to list and read at the destination. It needs no private
|
||||
key.
|
||||
|
||||
When a manifest cannot be read, the figures for blobs that no manifest lists are
|
||||
reported as unknown. That snapshot's own blob count and size are unknown too.
|
||||
|
||||
The command skips a name under `metadata/` that is not a remote key, with a
|
||||
warning. It does not count a file under `blobs/` whose name is not a blob hash.
|
||||
|
||||
When a manifest cannot be read, that snapshot's blob count and size are unknown.
|
||||
The figures for blobs that no manifest lists are unknown when a manifest cannot
|
||||
be read or sits under a skipped name.
|
||||
|
||||
`--json` prints the report as JSON. Unknown figures are `null`.
|
||||
`unreadable_manifests` lists the remote key of each manifest that could not be
|
||||
read. `skipped_manifest_count` counts the manifests under skipped names.
|
||||
@@ -715,7 +739,12 @@ Use this command after you point the config at a different destination. See
|
||||
|
||||
### `completion <shell>`
|
||||
|
||||
Prints a completion script for `bash`, `zsh`, `fish` or `powershell`.
|
||||
Prints a completion script for one of these shells:
|
||||
|
||||
- `bash`
|
||||
- `zsh`
|
||||
- `fish`
|
||||
- `powershell`
|
||||
|
||||
```sh
|
||||
# zsh, for the current session
|
||||
@@ -736,31 +765,51 @@ vaultik completion fish > ~/.config/fish/completions/vaultik.fish
|
||||
|
||||
### `version`
|
||||
|
||||
Prints the version, the commit, the commit date and the platform. It labels the
|
||||
commit date `build date`. It also says when the binary is a development build,
|
||||
not a release.
|
||||
Prints these:
|
||||
|
||||
- the version
|
||||
- the commit
|
||||
- the commit date, labelled `build date`
|
||||
- the Go version
|
||||
- the platform
|
||||
- the author
|
||||
- the homepage
|
||||
- the license
|
||||
|
||||
It also says when the binary is a development build, not a release.
|
||||
|
||||
## Configuration
|
||||
|
||||
The config file is YAML. `vaultik config init` writes one to start from, and
|
||||
[`config.example.yml`](config.example.yml) is a longer example.
|
||||
|
||||
A command refuses to start when the config does not load. The `config` commands,
|
||||
`completion` and `version` are the exceptions. The config loads when all of
|
||||
these hold:
|
||||
A command refuses to start when the config does not load. These commands are
|
||||
the exceptions:
|
||||
|
||||
- the `config` commands
|
||||
- `completion`
|
||||
- `version`
|
||||
|
||||
The config loads when it is valid YAML and all of these hold:
|
||||
|
||||
- Every entry in `age_recipients` is an age X25519 public key.
|
||||
- `snapshots` has at least one name, and every name has at least one path.
|
||||
- A destination is set, with credentials when it is S3.
|
||||
- A destination is set. Its `storage_url` has one of the three schemes under
|
||||
[Destinations](#destinations). An S3 destination has credentials.
|
||||
- Every `${ENV:NAME}` names an environment variable with a non-empty value.
|
||||
- Every other setting below is within its limits.
|
||||
|
||||
`vaultik` warns when group or others can read the config file.
|
||||
Every command that needs the local index warns when group or others can read the
|
||||
config file.
|
||||
|
||||
A value written as `${ENV:NAME}` is replaced by the environment variable `NAME`.
|
||||
This keeps a credential out of the file.
|
||||
|
||||
A leading `~` is expanded to your home directory in snapshot paths, in
|
||||
`index_path` and in a `file://~/...` destination.
|
||||
A leading `~` is expanded to your home directory in these places:
|
||||
|
||||
- snapshot paths
|
||||
- `index_path`
|
||||
- a `file://~/...` destination
|
||||
|
||||
### Where the config file is
|
||||
|
||||
@@ -768,11 +817,13 @@ A leading `~` is expanded to your home directory in snapshot paths, in
|
||||
|
||||
- the path given with `--config`
|
||||
- the path in the `VAULTIK_CONFIG` environment variable
|
||||
- `config.yml` in the platform config directory, which is `~/.config/vaultik/`
|
||||
on Linux and `~/Library/Application Support/vaultik/` on macOS
|
||||
- `config.yml` in the platform config directory, which by default is
|
||||
`~/.config/vaultik/` on Linux and `~/Library/Application Support/vaultik/` on
|
||||
macOS
|
||||
- `/etc/vaultik/config.yml`
|
||||
|
||||
A path from `--config` or `VAULTIK_CONFIG` must exist.
|
||||
A command that reads the config fails when the path from `--config` or
|
||||
`VAULTIK_CONFIG` does not exist. `config init` creates the file there.
|
||||
|
||||
### Settings
|
||||
|
||||
@@ -806,7 +857,8 @@ A path from `--config` or `VAULTIK_CONFIG` must exist.
|
||||
- `s3.region`
|
||||
- The S3 region. Default: `us-east-1`.
|
||||
- `s3.part_size`
|
||||
- The size of each part of a multipart upload. Default: `5MiB`.
|
||||
- A large blob is uploaded to S3 in parts. This is the size of each part.
|
||||
Default: `5MiB`.
|
||||
- It must be between `5MiB` and `5GiB`.
|
||||
- A blob too large for 10,000 parts of this size is uploaded in larger
|
||||
parts.
|
||||
@@ -826,8 +878,11 @@ A path from `--config` or `VAULTIK_CONFIG` must exist.
|
||||
- It must be at least four times `chunk_size`.
|
||||
- `compression_level`
|
||||
- The zstd compression level, from `1` to `19`. Default: `3`.
|
||||
- Levels `1` to `2`, `3` to `5`, `6` to `9` and `10` to `19` form four
|
||||
groups. Levels in one group compress the same way.
|
||||
- Levels in one of these groups compress the same way:
|
||||
- `1` to `2`
|
||||
- `3` to `5`
|
||||
- `6` to `9`
|
||||
- `10` to `19`
|
||||
- `prune_grace_period`
|
||||
- How old unused data must be before `prune` deletes it. Default: `24h`.
|
||||
- It applies to a blob that no manifest lists. It also applies to every
|
||||
@@ -839,8 +894,8 @@ A path from `--config` or `VAULTIK_CONFIG` must exist.
|
||||
- Only the part before the first `.` is used.
|
||||
- `index_path`
|
||||
- The path of the local index.
|
||||
- Default: `index.sqlite` in the platform data directory. That is
|
||||
`~/.local/share/vaultik/` on Linux and
|
||||
- Default: `index.sqlite` in the platform data directory. By default that
|
||||
is `~/.local/share/vaultik/` on Linux and
|
||||
`~/Library/Application Support/vaultik/` on macOS.
|
||||
|
||||
Sizes take a number of bytes or a unit. `MB` and `GB` are powers of 1000. `MiB`
|
||||
@@ -856,7 +911,9 @@ and `GiB` are powers of 1024.
|
||||
destination.
|
||||
- `EDITOR`: the editor that `config edit` starts.
|
||||
- `TMPDIR`: the directory for temporary files.
|
||||
- `NO_COLOR`: when set, status lines have no color.
|
||||
- `NO_COLOR`: a non-empty value turns off color in status lines.
|
||||
- `XDG_CONFIG_HOME` and `XDG_DATA_HOME`: move the platform config and data
|
||||
directories.
|
||||
- `VAULTIK_CPUPROFILE`: write a CPU profile to this path. For debugging.
|
||||
- `VAULTIK_MEMPROFILE`: write a heap profile to this path on exit. For
|
||||
debugging.
|
||||
@@ -966,15 +1023,26 @@ A leftover `.partial` file is ignored. You can delete it.
|
||||
|
||||
### Destination permissions
|
||||
|
||||
Each command needs different permissions at the destination.
|
||||
Commands need these permissions at the destination:
|
||||
|
||||
- `snapshot create` needs only permission to write objects. A machine you back
|
||||
up can therefore hold credentials that can neither read nor delete.
|
||||
- `snapshot list`, `snapshot verify`, `snapshot restore` and `remote info` list
|
||||
and read.
|
||||
- `snapshot purge`, `snapshot remove` and `remote nuke` list and delete.
|
||||
- `snapshot create` without `--prune` needs only permission to write objects. A
|
||||
machine you back up can therefore hold credentials that can neither read nor
|
||||
delete.
|
||||
- These commands list and read:
|
||||
- `snapshot list`
|
||||
- `snapshot verify`
|
||||
- `snapshot restore`
|
||||
- `remote info`
|
||||
- These commands list and delete:
|
||||
- `snapshot purge`
|
||||
- `snapshot remove`
|
||||
- `remote nuke`
|
||||
- `prune` and `snapshot create --prune` list, read and delete.
|
||||
|
||||
`snapshot verify` and `snapshot restore` list only when you give a remote key.
|
||||
`snapshot remove --local-only` does not reach the destination.
|
||||
`snapshot remove --dry-run` deletes nothing.
|
||||
|
||||
A machine that runs `snapshot create --prune` can delete its own backups. To
|
||||
keep it to write-only credentials, run `prune` from a separate, trusted machine.
|
||||
|
||||
@@ -991,12 +1059,15 @@ unplugged volume leaves an empty directory at its mount point. Without the
|
||||
marker, that directory would look like a destination whose snapshots are all
|
||||
gone.
|
||||
|
||||
The check applies while the local index holds snapshots. When the destination
|
||||
then answers that the marker does not exist, commands treat the destination as
|
||||
not present:
|
||||
The check applies while the local index holds snapshots and belongs to the
|
||||
configured destination. When the destination then answers that the marker does
|
||||
not exist, commands treat the destination as not present:
|
||||
|
||||
- `snapshot create` refuses to write to it.
|
||||
- `prune`, `snapshot purge` and `remote nuke` fail.
|
||||
- These commands fail:
|
||||
- `prune`
|
||||
- `snapshot purge`
|
||||
- `remote nuke`
|
||||
- `snapshot list` shows the local index alone.
|
||||
- `snapshot remove` deletes nothing at the destination.
|
||||
|
||||
@@ -1025,7 +1096,11 @@ different destination:
|
||||
- `remote nuke`
|
||||
|
||||
The destination that is compared is `storage_url`. When `storage_url` is not
|
||||
set, it is `s3.endpoint`, `s3.bucket` and `s3.prefix`.
|
||||
set, these three settings are compared instead:
|
||||
|
||||
- `s3.endpoint`
|
||||
- `s3.bucket`
|
||||
- `s3.prefix`
|
||||
|
||||
There are two ways out:
|
||||
|
||||
@@ -1057,8 +1132,7 @@ The new snapshot cannot be restored afterwards.
|
||||
|
||||
## Locking
|
||||
|
||||
Commands that change data take a lock, so that only one of them runs at a time.
|
||||
These commands take it:
|
||||
Five commands take a lock, so that only one of them runs at a time:
|
||||
|
||||
- `snapshot create`
|
||||
- `snapshot purge`
|
||||
@@ -1068,9 +1142,9 @@ These commands take it:
|
||||
|
||||
A second one does not wait. It exits at once with an "already running" error.
|
||||
|
||||
The lock is the file `vaultik.pid` in the platform data directory. That is
|
||||
`~/.local/share/vaultik/` on Linux and `~/Library/Application Support/vaultik/`
|
||||
on macOS.
|
||||
The lock is the file `vaultik.pid` in the platform data directory. By default
|
||||
that is `~/.local/share/vaultik/` on Linux and
|
||||
`~/Library/Application Support/vaultik/` on macOS.
|
||||
|
||||
The lock covers one user on one machine. It does not stop another user or
|
||||
another machine from writing to the same destination. See
|
||||
@@ -1111,7 +1185,8 @@ banner, log records and a final error stay on your terminal.
|
||||
Log records are colored lines when stderr is a terminal. They are JSON, one
|
||||
object per line, when stderr is redirected.
|
||||
|
||||
Status lines are colored only on a terminal, and never when `NO_COLOR` is set.
|
||||
Status lines are colored only on a terminal, and never when `NO_COLOR` has a
|
||||
non-empty value.
|
||||
|
||||
### `--json`
|
||||
|
||||
@@ -1183,8 +1258,8 @@ destination, what it protects and the risks it accepts.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **No extended attributes.** ACLs, macOS Finder metadata, quarantine flags and
|
||||
SELinux labels are not backed up.
|
||||
- **No extended attributes.** Access control lists, macOS Finder metadata,
|
||||
quarantine flags and SELinux labels are not backed up.
|
||||
- **No hard links.** Two hard links to one file are backed up as two files. The
|
||||
content is stored once, but a restore creates two separate files.
|
||||
- **No sparse files.** A sparse file is backed up at its full size, zeros
|
||||
@@ -1260,25 +1335,33 @@ call them.
|
||||
- `script/test`: runs the whole test suite, by building the `test` stage of the
|
||||
`Dockerfile`.
|
||||
- `script/lint`: runs the linter, by building the `lint` stage of the
|
||||
`Dockerfile`. The linter never runs on your machine.
|
||||
`Dockerfile`. The linter runs only inside Docker.
|
||||
- `script/lint-fix`: applies the linter's automatic fixes to your files. It
|
||||
needs a local Docker daemon. Run `script/lint` afterwards.
|
||||
- `script/fmt`: formats the Go code.
|
||||
- `script/fmt-check`: checks the formatting and changes nothing.
|
||||
- `script/check`: runs `script/test`, `script/lint` and `script/fmt-check`.
|
||||
- `script/precommit`: runs `go mod tidy`, `go fmt` and `script/check`. The git
|
||||
pre-commit hook calls it.
|
||||
- `script/check`: runs these three scripts:
|
||||
- `script/test`
|
||||
- `script/lint`
|
||||
- `script/fmt-check`
|
||||
- `script/precommit`: the git pre-commit hook calls it. It runs these three:
|
||||
- `go mod tidy`
|
||||
- `go fmt`
|
||||
- `script/check`
|
||||
- `script/install-precommit`: installs that hook.
|
||||
- `script/docker`: builds the Docker image. The build runs the linter and the
|
||||
tests too.
|
||||
- `script/cibuild`: what CI runs on every push. It runs `script/bootstrap`,
|
||||
`script/check` and the image build.
|
||||
- `script/cibuild`: what CI runs on every push. It runs these three:
|
||||
- `script/bootstrap`
|
||||
- `script/check`
|
||||
- the image build
|
||||
- `script/version`: prints the version to build into the binary.
|
||||
- `script/projectname`: prints the project name, which tags the Docker image.
|
||||
- `script/install-go`: installs the Go version that `go.mod` names into
|
||||
`.tool/go`. It checks the download against a checksum in the script.
|
||||
- `script/install-goreleaser`: installs the pinned `goreleaser` into
|
||||
`.tool/bin`, checked the same way.
|
||||
- `script/install-goreleaser`: installs the pinned
|
||||
[`goreleaser`](https://goreleaser.com/) into `.tool/bin`, checked the same
|
||||
way.
|
||||
- `script/release`: builds and publishes the release files.
|
||||
- `script/release-snapshot`: builds the release files into `./dist` and
|
||||
publishes nothing.
|
||||
@@ -1289,15 +1372,17 @@ skip the tests or the linter and still report success.
|
||||
The test suite has no separate integration target. Every test runs every time.
|
||||
Each test binary is stopped after 90 seconds.
|
||||
|
||||
To change the Go version, change these together: `go.mod`, the version and
|
||||
checksums in `script/install-go`, and the two `golang` image digests in the
|
||||
`Dockerfile`.
|
||||
To change the Go version, change these together:
|
||||
|
||||
- `go.mod`
|
||||
- the version and checksums in `script/install-go`
|
||||
- the two `golang` image digests in the `Dockerfile`
|
||||
|
||||
### Messages to the user
|
||||
|
||||
Status lines, warnings and errors go through `internal/ui`. The data a command
|
||||
exists to print is written plain, with no marker and no color. That covers
|
||||
tables, reports, JSON documents and `config get` values.
|
||||
Write status lines, warnings and errors through `internal/ui`. The data a
|
||||
command exists to print is written plain, with no marker and no color. That
|
||||
covers tables, reports, JSON documents and `config get` values.
|
||||
|
||||
`internal/ui` has these kinds of message:
|
||||
|
||||
@@ -1383,8 +1468,14 @@ version.
|
||||
`make release-snapshot` builds the same files into `./dist` and publishes
|
||||
nothing.
|
||||
|
||||
Each archive holds the binary, `LICENSE` and `README.md`. The release files are
|
||||
not signed, have no SBOM and are not built reproducibly.
|
||||
Each archive holds three files:
|
||||
|
||||
- the binary
|
||||
- `LICENSE`
|
||||
- `README.md`
|
||||
|
||||
The release files are not signed, have no software bill of materials and are
|
||||
not built reproducibly.
|
||||
|
||||
## license
|
||||
|
||||
|
||||
+21
-15
@@ -3,7 +3,7 @@
|
||||
# defaults; where an option has a default, its comment gives it.
|
||||
# Copy this file and change the values you need
|
||||
|
||||
# Age recipient public keys for encryption
|
||||
# Age recipient public keys for encryption (age: https://age-encryption.org/)
|
||||
# Backups are encrypted to these public keys. snapshot create needs at least
|
||||
# one; listing, verifying and restoring do not
|
||||
# Generate a key pair, and show its public key, with:
|
||||
@@ -11,6 +11,7 @@
|
||||
# grep 'public key' vaultik_backup_private_key.txt
|
||||
# Keep the private key file: restoring needs it. Do not pipe age-keygen
|
||||
# straight into grep, which would discard the private key
|
||||
# Default: none
|
||||
age_recipients:
|
||||
- age1cj2k2addawy294f6k2gr2mf9gps9r3syplryxca3nvxj3daqm96qfp84tz
|
||||
|
||||
@@ -268,6 +269,7 @@ snapshots:
|
||||
- "**/vendor"
|
||||
|
||||
# Global patterns to exclude from all backups
|
||||
# Default: none
|
||||
exclude:
|
||||
- "*.tmp"
|
||||
|
||||
@@ -276,8 +278,8 @@ exclude:
|
||||
# An s3:// URL also needs s3.access_key_id and s3.secret_access_key below
|
||||
# A file:// path must be absolute, and the directory must already exist:
|
||||
# vaultik never creates it
|
||||
# An rclone:// URL needs the remote to be defined in rclone's own config
|
||||
# (rclone config)
|
||||
# An rclone:// URL names a remote of rclone (https://rclone.org/); the remote
|
||||
# must be defined in rclone's own config (rclone config)
|
||||
storage_url: "rclone://myremote/path/to/backups"
|
||||
|
||||
# S3-compatible storage configuration
|
||||
@@ -310,7 +312,7 @@ storage_url: "rclone://myremote/path/to/backups"
|
||||
#
|
||||
# # Part size for multipart uploads
|
||||
# # Minimum 5MiB, maximum 5GiB; affects memory usage during upload
|
||||
# # A blob too large for 10,000 parts of this size gets larger parts
|
||||
# # An object too large for 10,000 parts of this size gets larger parts
|
||||
# # Supports: 10MB, 16MiB, 100MiB, etc. (5MB is below the minimum)
|
||||
# # Default: 5MiB
|
||||
# #part_size: 5MiB
|
||||
@@ -323,8 +325,9 @@ storage_url: "rclone://myremote/path/to/backups"
|
||||
# Linux: ~/.local/share/vaultik/index.sqlite
|
||||
#index_path: /path/to/index.sqlite
|
||||
|
||||
# Average chunk size for content-defined chunking
|
||||
# Smaller chunks = better deduplication but more metadata
|
||||
# Average size of a chunk, a piece of a file's content. Chunk boundaries
|
||||
# depend on the file's content, not on fixed offsets
|
||||
# Smaller chunks find more duplicate content and make the index larger
|
||||
# Minimum 1MB
|
||||
# Supports: 10MB, 5M, 1GB, 64MiB, etc.
|
||||
# Default: 10MB
|
||||
@@ -334,9 +337,10 @@ storage_url: "rclone://myremote/path/to/backups"
|
||||
# Multiple chunks are packed into blobs up to this size
|
||||
# Must be at least four times chunk_size (the largest chunk the chunker can
|
||||
# emit); a smaller limit would let a single-chunk blob exceed it.
|
||||
# Chunking uses no secret (the FastCDC parameters are fixed and public). At a
|
||||
# large limit a blob holds hundreds of chunks, so individual chunk lengths are
|
||||
# not visible in its size; lowering the limit toward chunk_size exposes them.
|
||||
# Chunking uses no secret (the parameters of the FastCDC chunking algorithm
|
||||
# are fixed and public). At a large limit a blob holds hundreds of chunks, so
|
||||
# individual chunk lengths are not visible in its size; lowering the limit
|
||||
# toward chunk_size exposes them.
|
||||
# A backup needs free temporary space, because each blob is written whole to
|
||||
# a temporary file before it is uploaded (up to about blob_size_limit; an
|
||||
# rclone destination that cannot stream uploads needs about twice that) and
|
||||
@@ -352,12 +356,14 @@ storage_url: "rclone://myremote/path/to/backups"
|
||||
# Default: 3
|
||||
compression_level: 5
|
||||
|
||||
# How old a blob that no snapshot lists must be before prune deletes it, and
|
||||
# how old every object in a directory under metadata/ with no manifest must
|
||||
# be before prune deletes that directory
|
||||
# A backup uploads its blobs and its db.zst.age before its manifest, so on a
|
||||
# destination shared between hosts or users this must be longer than the
|
||||
# longest backup run
|
||||
# How old a blob that no snapshot lists must be before vaultik prune deletes
|
||||
# it. Each snapshot has a directory under metadata/ at the destination, and
|
||||
# its manifest there lists the blobs it uses. This is also how old every
|
||||
# object in a directory with no manifest must be before vaultik prune deletes
|
||||
# that directory
|
||||
# A backup uploads its blobs and its encrypted database (db.zst.age) before
|
||||
# its manifest, so on a destination shared between hosts or users this must
|
||||
# be longer than the longest backup run
|
||||
# It does not protect a blob a backup reuses from an earlier run that stopped
|
||||
# before its manifest: that blob keeps the time of its first upload, so a
|
||||
# prune can delete it
|
||||
|
||||
+51
-28
@@ -1,7 +1,8 @@
|
||||
# vaultik data model
|
||||
|
||||
`vaultik` keeps a SQLite database on the backed-up machine, called the local
|
||||
index. This document describes its tables and how each command uses them.
|
||||
index. This document describes its tables, and how a backup, a restore and
|
||||
`prune` use them.
|
||||
|
||||
The local index has two jobs:
|
||||
|
||||
@@ -27,10 +28,11 @@ with the snapshot.
|
||||
|
||||
## Where the index is
|
||||
|
||||
The index is the file `index.sqlite` in the platform data directory. That is
|
||||
`~/.local/share/vaultik/` on Linux and `~/Library/Application Support/vaultik/`
|
||||
on macOS. The `index_path` setting and the `VAULTIK_INDEX_PATH` environment
|
||||
variable change the path.
|
||||
The index is the file `index.sqlite` in the platform data directory. By default
|
||||
that is `~/.local/share/vaultik/` on Linux and
|
||||
`~/Library/Application Support/vaultik/` on macOS. The `index_path` setting and
|
||||
the `VAULTIK_INDEX_PATH` environment variable change the path. So does
|
||||
`XDG_DATA_HOME`.
|
||||
|
||||
The file is readable only by its owner, because it lists every backed-up path.
|
||||
`vaultik` sets that mode each time it opens the index.
|
||||
@@ -74,15 +76,15 @@ One row for each backed-up path: a regular file, a symlink or a directory.
|
||||
|
||||
- `id`: a UUID. Primary key.
|
||||
- `path`: the absolute path. Unique.
|
||||
- `source_path`: the configured path that the scan found this entry under. A
|
||||
restore does not read it.
|
||||
- `source_path`: the configured path under which the scan found this entry,
|
||||
made absolute with symlinks resolved. A restore does not read it.
|
||||
- `mtime`: the modification time.
|
||||
- `mtime_nsec`: the nanoseconds within that second.
|
||||
- `size`: the size in bytes.
|
||||
- `mode`: the permission bits and the file type.
|
||||
- `uid`: the owner's user ID.
|
||||
- `gid`: the owner's group ID.
|
||||
- `link_target`: the target of a symlink. It is `NULL` for other entries.
|
||||
- `link_target`: the target of a symlink. It is empty for other entries.
|
||||
|
||||
`idx_files_path` indexes `path`.
|
||||
|
||||
@@ -161,7 +163,8 @@ A restore finds the blob for each chunk through this table.
|
||||
One row for each snapshot.
|
||||
|
||||
- `id`: the snapshot ID. Primary key.
|
||||
- `hostname`: the hostname of the machine that made the backup.
|
||||
- `hostname`: the `hostname` setting of the config. When that setting is empty,
|
||||
it is the machine's hostname.
|
||||
- `vaultik_version`: the `vaultik` version that made it.
|
||||
- `vaultik_git_revision`: the git commit of that version.
|
||||
- `started_at`: when the backup started.
|
||||
@@ -189,8 +192,8 @@ example is `server1_home_2025-06-01T12:00:00Z`. It has three parts:
|
||||
- the snapshot name
|
||||
- the start time, in UTC
|
||||
|
||||
`snapshot list` shows `uncompressed_chunk_size` as `UNCOMPRESSED SIZE` and
|
||||
`new_chunk_size` as `NEW CHUNK SIZE`.
|
||||
The `snapshot list` table shows `uncompressed_chunk_size` as
|
||||
`UNCOMPRESSED SIZE` and `new_chunk_size` as `NEW CHUNK SIZE`.
|
||||
|
||||
A snapshot with `completed_at` set to `NULL` is unfinished. `snapshot list`
|
||||
does not show it. The next backup deletes it, and so does `prune`.
|
||||
@@ -233,10 +236,10 @@ A directory is recorded here when all of these hold:
|
||||
- The backup found an entry in it that is not excluded.
|
||||
- It is on a different filesystem from its parent directory.
|
||||
|
||||
The device number is read after the directory has been listed. Listing an
|
||||
automount point mounts its volume, so the number is that of the volume. An
|
||||
automount point whose volume cannot be mounted is not recorded, because the
|
||||
backup finds no entry in it.
|
||||
The device number is read after the directory has been listed. An automount
|
||||
point is a directory whose volume is mounted when it is first listed, so the
|
||||
number is that of the volume. An automount point whose volume cannot be mounted
|
||||
is not recorded, because the backup finds no entry in it.
|
||||
|
||||
`snapshot create --prune` reads this table. It skips the prune when an earlier
|
||||
snapshot of the same name recorded a mount point that the new backup did not
|
||||
@@ -266,8 +269,14 @@ Settings that belong to this copy of the index.
|
||||
- `value`: its value.
|
||||
|
||||
The only key is `storage_url`. It holds the destination that the index was
|
||||
first used with. A command that changes data refuses to run when the config
|
||||
names a different destination.
|
||||
first used with. These commands refuse to run when the config names a different
|
||||
destination:
|
||||
|
||||
- `snapshot create`
|
||||
- `snapshot purge`
|
||||
- `snapshot remove`
|
||||
- `prune`
|
||||
- `remote nuke`
|
||||
|
||||
### `schema_migrations`
|
||||
|
||||
@@ -282,13 +291,16 @@ The migrations that have run.
|
||||
|
||||
A backup works on the tables in this order:
|
||||
|
||||
- It saves the destination in `local_meta` when the index has none yet.
|
||||
- It deletes unfinished snapshots, and the rows that no snapshot uses.
|
||||
- It creates a row in `snapshots`, with `completed_at` set to `NULL`.
|
||||
- It deletes the `blobs` rows that were never uploaded.
|
||||
- It walks each path and compares every entry with its row in `files`.
|
||||
- It walks each path. It compares each regular file and symlink with its row in
|
||||
`files`.
|
||||
- It links each unchanged file to the snapshot in `snapshot_files`.
|
||||
- It records each mount point in `snapshot_mount_points`.
|
||||
- It reads each changed file and stores it. See below.
|
||||
- It reads each changed regular file and stores it. See below. A changed symlink
|
||||
is recorded without being read.
|
||||
- It fills `snapshot_blobs` with every blob the snapshot uses.
|
||||
- It stores the snapshot's totals in `snapshots`.
|
||||
- It uploads the snapshot's metadata.
|
||||
@@ -303,7 +315,8 @@ in any of these:
|
||||
- owner or group
|
||||
- link target
|
||||
|
||||
A file is also read again when it lists a chunk that no uploaded blob holds.
|
||||
A file is also read again when it lists a chunk that no uploaded blob holds. A
|
||||
file with no row in `files` is read too.
|
||||
|
||||
Storing a changed file writes these rows:
|
||||
|
||||
@@ -316,8 +329,10 @@ Storing a changed file writes these rows:
|
||||
- `uploaded_ts` and `compressed_size` in `blobs`
|
||||
- a row in `snapshot_blobs`
|
||||
- a row in `uploads`, unless the blob was already at the destination
|
||||
- The row in `files`, with its `file_chunks`, `chunk_files` and
|
||||
`snapshot_files` rows.
|
||||
- The row in `files`, and its rows in these tables:
|
||||
- `file_chunks`
|
||||
- `chunk_files`
|
||||
- `snapshot_files`
|
||||
|
||||
The row in `files` is written last. It is written only after every blob that
|
||||
holds the file's new chunks is stored.
|
||||
@@ -334,31 +349,38 @@ The metadata of a snapshot is made from a copy of the index:
|
||||
uploaded database therefore names the destination, and lists the migrations
|
||||
that have run.
|
||||
- `VACUUM` rewrites the copy, so that no deleted row is left in the file.
|
||||
- The copy is compressed with zstd and encrypted with age.
|
||||
- The copy is compressed with [zstd](https://github.com/facebook/zstd) and
|
||||
encrypted with [age](https://age-encryption.org/).
|
||||
- The manifest is built from the copy's `snapshot_blobs` table.
|
||||
- The copy is uploaded as `metadata/<remote-key>/db.zst.age`.
|
||||
- The manifest is uploaded as `metadata/<remote-key>/manifest.json.zst`.
|
||||
|
||||
The copy is made through SQLite, not by copying the file. The index runs in WAL
|
||||
mode, so rows that are already committed can still be in the `-wal` file beside
|
||||
it.
|
||||
The copy is made through SQLite, not by copying the file. The index runs in
|
||||
SQLite's write-ahead log (WAL) mode, so rows that are already committed can
|
||||
still be in the `-wal` file beside it.
|
||||
|
||||
### Restore
|
||||
|
||||
A restore does not use the local index.
|
||||
A restore reads no file, chunk or blob rows from the local index. Given a
|
||||
remote key, it reads `snapshots` and `local_meta` there to check the destination
|
||||
marker.
|
||||
|
||||
- It downloads and decrypts `db.zst.age` into a temporary directory.
|
||||
- It opens that file read-only. It applies no migration to it.
|
||||
- It reads the snapshot's row in `snapshots`, to check that this is the right
|
||||
database.
|
||||
- It reads the files to restore from `files`.
|
||||
- It finds each file's chunks in `file_chunks`.
|
||||
- It finds the blob and offset of each chunk in `blob_chunks`.
|
||||
- It finds the name of each blob in `blobs`.
|
||||
- With `--verify`, it reads each chunk's size from `chunks`.
|
||||
|
||||
It does not read the manifest.
|
||||
|
||||
### Prune
|
||||
|
||||
`prune` changes the local index in two ways:
|
||||
`prune` first saves the destination in `local_meta` when the index has none yet.
|
||||
It then changes the local index in two ways:
|
||||
|
||||
- It deletes each snapshot whose manifest is gone from the destination.
|
||||
- It deletes unfinished snapshots, and the rows that no snapshot uses.
|
||||
@@ -389,6 +411,7 @@ Each repository type in `internal/database` serves one or more tables:
|
||||
- `snapshot_files`
|
||||
- `snapshot_blobs`
|
||||
- `snapshot_mount_points`
|
||||
- `uploads`, where it deletes a snapshot's rows
|
||||
- `UploadRepository`: `uploads`
|
||||
- `LocalMetaRepository`: `local_meta`
|
||||
|
||||
|
||||
+48
-31
@@ -4,8 +4,8 @@ This document describes what `vaultik` stores at the destination. It covers the
|
||||
layout, what the layout protects, and the risks it accepts.
|
||||
|
||||
The destination is the place the backups are stored. It is an S3 bucket and
|
||||
prefix, a local directory or an rclone remote. The layout is the same on all
|
||||
three.
|
||||
prefix, a local directory or a remote of [rclone](https://rclone.org/). The
|
||||
layout is the same on all three.
|
||||
|
||||
## Terms
|
||||
|
||||
@@ -16,7 +16,8 @@ three.
|
||||
content.
|
||||
- **Blob**: one object under `blobs/`. It holds many chunks.
|
||||
- **Local index**: the SQLite database on the backed-up machine.
|
||||
- **Recipient**: an age public key that backups are encrypted to.
|
||||
- **Recipient**: an [age](https://age-encryption.org/) public key that backups
|
||||
are encrypted to.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -49,12 +50,15 @@ unplugged volume leaves an empty directory at its mount point. Without the
|
||||
marker, that directory would look like a destination whose snapshots are all
|
||||
gone.
|
||||
|
||||
The check applies while the local index holds snapshots. When the destination
|
||||
then answers that the marker does not exist, commands treat the destination as
|
||||
not present:
|
||||
The check applies while the local index holds snapshots and belongs to the
|
||||
configured destination. When the destination then answers that the marker does
|
||||
not exist, commands treat the destination as not present:
|
||||
|
||||
- `snapshot create` refuses to write to it.
|
||||
- `prune`, `snapshot purge` and `remote nuke` fail.
|
||||
- These commands fail:
|
||||
- `prune`
|
||||
- `snapshot purge`
|
||||
- `remote nuke`
|
||||
- `snapshot list` shows the local index alone.
|
||||
- `snapshot remove` deletes nothing at the destination.
|
||||
|
||||
@@ -73,8 +77,10 @@ A blob holds the content of many chunks, one after another.
|
||||
- **Path**: `blobs/<first 2 characters>/<next 2 characters>/<full hash>`.
|
||||
- **Example**:
|
||||
`blobs/ca/fe/cafebabe1234567890abcdef1234567890abcdef1234567890abcdef12345678`.
|
||||
- **Format**: the chunk data, compressed with zstd and then encrypted with age.
|
||||
- **Recipients**: the public keys in `age_recipients` when the blob was written.
|
||||
- **Format**: the chunk data, compressed with
|
||||
[zstd](https://github.com/facebook/zstd) and then encrypted with age.
|
||||
- **Recipients**: the public keys in the `age_recipients` setting when the blob
|
||||
was written.
|
||||
|
||||
The two directory levels keep any one directory from holding too many objects.
|
||||
|
||||
@@ -257,9 +263,9 @@ therefore does not stop overwrites.
|
||||
An object's size follows exactly from the size of its compressed contents.
|
||||
|
||||
All the new chunks in one blob share one zstd stream. The stream looks back 8
|
||||
MiB, or 4 MiB at compression levels 1 and 2. A blob is closed at
|
||||
`blob_size_limit` and at the end of each configured path. A chunk that is
|
||||
already stored is never packed again.
|
||||
MiB, or 4 MiB at compression levels 1 and 2. A blob is closed at the
|
||||
`blob_size_limit` setting and at the end of each configured path. A chunk that
|
||||
the local index records as uploaded is never packed again.
|
||||
|
||||
Data that an outsider writes compresses smaller when it matches a secret near it
|
||||
in the same blob, so the blob gets smaller. The blob's size therefore shows the
|
||||
@@ -277,14 +283,15 @@ Back up a directory that outsiders can write to as a snapshot of its own.
|
||||
|
||||
### Chunk boundaries use no secret
|
||||
|
||||
The FastCDC parameters are fixed and public. `chunk_size` sets the average chunk
|
||||
size. No chunk is larger than four times this size. Only a file's last chunk can
|
||||
be smaller than a quarter of it. With the default of 10 MB, those limits are 40
|
||||
MB and 2.5 MB. A file of 2.5 MB or less is one chunk.
|
||||
`vaultik` finds chunk boundaries with the FastCDC algorithm. Its parameters are
|
||||
fixed and public. `chunk_size` sets the average chunk size. No chunk is larger
|
||||
than four times this size. Only a file's last chunk can be smaller than a
|
||||
quarter of it. With the default of 10 MB, those limits are 40 MB and 2.5 MB. A
|
||||
file of 2.5 MB or less is one chunk. An empty file has no chunk.
|
||||
|
||||
At the default `blob_size_limit` of 10 GB, a blob holds hundreds of chunks. The
|
||||
size of the blob then does not show the size of any one chunk. A limit close to
|
||||
`chunk_size` begins to show chunk sizes.
|
||||
At the default `blob_size_limit` of 10 GB, a full blob holds hundreds of
|
||||
chunks. The size of the blob then does not show the size of any one chunk. A
|
||||
limit close to `chunk_size` begins to show chunk sizes.
|
||||
|
||||
### Decrypted data on the local disk
|
||||
|
||||
@@ -307,14 +314,24 @@ Trust the temporary directory as much as the directory you restore into.
|
||||
|
||||
### Commands need different permissions
|
||||
|
||||
`snapshot create` needs only permission to write objects. Other commands need
|
||||
more:
|
||||
`snapshot create` without `--prune` needs only permission to write objects.
|
||||
Other commands need more:
|
||||
|
||||
- `snapshot list`, `snapshot verify`, `snapshot restore` and `remote info` list
|
||||
and read.
|
||||
- `snapshot purge`, `snapshot remove` and `remote nuke` list and delete.
|
||||
- These commands list and read:
|
||||
- `snapshot list`
|
||||
- `snapshot verify`
|
||||
- `snapshot restore`
|
||||
- `remote info`
|
||||
- These commands list and delete:
|
||||
- `snapshot purge`
|
||||
- `snapshot remove`
|
||||
- `remote nuke`
|
||||
- `prune` lists, reads and deletes.
|
||||
|
||||
`snapshot verify` and `snapshot restore` list only when they are given a remote
|
||||
key. `snapshot remove --local-only` does not reach the destination.
|
||||
`snapshot remove --dry-run` deletes nothing.
|
||||
|
||||
`snapshot create --prune` runs a prune on the backed-up machine. That machine
|
||||
then holds credentials that can delete its own backups. To keep it to
|
||||
write-only credentials, run `prune` from a separate, trusted machine.
|
||||
@@ -322,8 +339,8 @@ write-only credentials, run `prune` from a separate, trusted machine.
|
||||
### New recipients cannot read old data
|
||||
|
||||
Changing `age_recipients` does not re-encrypt what is already stored. A chunk
|
||||
that is already stored is not written again, so it stays encrypted to the
|
||||
earlier recipients.
|
||||
that the local index records as uploaded is not written again, so it stays
|
||||
encrypted to the earlier recipients.
|
||||
|
||||
A new snapshot that uses such chunks cannot be restored with a new recipient's
|
||||
key alone. To make everything readable by a new key, run
|
||||
@@ -335,8 +352,8 @@ key alone. To make everything readable by a new key, run
|
||||
recipients.
|
||||
|
||||
The encrypted data is held by a third party for a long time. There is no second
|
||||
kind of recipient to fall back on if X25519 is broken, and no post-quantum
|
||||
option.
|
||||
kind of recipient to fall back on if X25519 is broken, and no kind that resists
|
||||
a quantum computer.
|
||||
|
||||
## Order of writes
|
||||
|
||||
@@ -353,12 +370,12 @@ therefore belongs to a backup that did not finish.
|
||||
|
||||
The local index marks a snapshot complete only after both metadata files are
|
||||
uploaded. A backup that stops during that upload leaves the snapshot
|
||||
unfinished. The next backup removes it from the local index and stores its data
|
||||
again.
|
||||
unfinished. The next backup removes it from the local index and backs up its
|
||||
files again.
|
||||
|
||||
One short gap remains. A backup can stop after the upload and before the
|
||||
snapshot is marked complete. The destination then holds a snapshot that can be
|
||||
restored, and the local index does not list it. `snapshot list` shows it as
|
||||
restored, and the local index marks it unfinished. `snapshot list` shows it as
|
||||
remote only.
|
||||
|
||||
An upload that is cut off leaves no partial object under the final name on S3
|
||||
|
||||
@@ -361,7 +361,8 @@ nuke --force' — it is the single supported entry point for that.`,
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Skip confirmation prompt")
|
||||
cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false,
|
||||
"Show what would be removed without removing")
|
||||
"Show what would be removed without removing; needs no permission "+
|
||||
"to delete")
|
||||
cmd.Flags().BoolVar(&opts.JSON, "json", false,
|
||||
"Output result as JSON; skips the confirmation prompt, as --force does")
|
||||
cmd.Flags().BoolVar(&opts.LocalOnly, "local-only", false,
|
||||
|
||||
Reference in new issue
Block a user