docs: restructure and update documentation for 2.5.0

Split the README into a short front page and task-oriented pages under
docs/: installation, user guide, CLI reference, configuration and
troubleshooting, with an index in docs/README.md. Rewrite RECOVERY.md
around Continue This Backup Here and Restore Folders, and DEVELOPMENT.md
around running, debugging and releasing from source.

Bring ARCHITECTURE.md up to date with the read path, push events, the
thread map, conflict copies and the machine profile, and move the feature
design notes there. Collect planned work in ROADMAP.md. Link the threat
model from SECURITY.md and the translation rules from CONTRIBUTING.md.

Add scripts/publish-wiki.sh, which mirrors the user pages to the GitHub
wiki with rewritten links and only pushes with --push.
This commit is contained in:
nvrl committed 2026-09-28 20:04:22 +02:00
1 parent 4b90abf3b5
commit af869944cf
16 files changed
+1960 -873

No files matched your search

+119 -213
View File
@@ -1,262 +1,168 @@
# Recovery: Getting a Replacement Machine Back onto Your Device
# Recovering on a new computer
You lose the laptop. You install `pdfs` on a new one. What does it take to get the
synced folders back?
Your laptop is lost, stolen or reinstalled. This page explains what survives, and how to get
your synced folders back on the replacement machine.
This document is two things: **§2 is the runbook that works with the client as it
stands today**, gotchas included, and **§3 is the plan to reduce that runbook to
one command.** §1 is the part neither can fix — what actually died with the
machine.
- [1. What survives](#1-what-survives)
- [2. Restore your synced folders](#2-restore-your-synced-folders)
- [3. How the restore works](#3-how-the-restore-works)
- [4. Known gaps](#4-known-gaps)
---
## 1. What survives
## 1. What survives, and what does not
### On Proton Drive
### Survives (it is on Drive, or derivable)
| Thing | Where it lives |
| What | Where |
|---|---|
| Every file that finished uploading | Proton Drive |
| The Device registration itself | Proton Drive (`enumerate_devices`) |
| The device's folder tree — one folder per synced folder | Proton Drive, under the device root |
| Your account, keys, sharing | Proton |
| The computer's registration and its backup | **Computers** in the app and the web |
| One folder per synced folder, under that computer | Proton Drive |
| The machine profile: which folders were synced, to which local path, in which mode; pins; ignore patterns | `.proton-drive-linux/profile.json` under the computer's backup, end-to-end encrypted like any other file |
| Your account, keys and shares | Proton |
### Does not survive
The service rewrites the profile a short while after you add, remove or switch a synced folder,
or change a pin.
| Thing | Why | Recoverable? |
### Lost with the machine
| What | Why | Recoverable? |
|---|---|---|
| Undrained `staging/` + `recovery/` blobs | Writes the kernel accepted but that never reached Drive. They existed only on that disk. | **No.** |
| Local-only files in a mirror folder that had not uploaded yet | Same. | **No.** |
| `sync_folder` rows — which local path maps to which remote folder, and in which mode | Local SQLite (`cache.db`) | Rebuildable by hand (§2), not automatically (§3) |
| `sync_entry` baseline | Local SQLite | Not needed. An empty baseline reconciles correctly — see §2.4. |
| Pins | Local SQLite | Rebuildable by hand |
| `config.json` — mountpoint, cache budget | Local config dir | Rebuildable by hand |
| `cache.db` node/FTS/photo caches | Local SQLite | Pure cache; rebuilds itself |
| Changes still in `staging/` or `recovery/` | Writes that were accepted locally but never reached Proton Drive | **No** |
| New files in a synced folder that had not uploaded yet | Same | **No** |
| `config.json`: mountpoint, cache limit, bandwidth limits, appearance | Local config directory | Set again in **Preferences** |
| The local database and content cache | Local disk | Not needed; rebuilt from Proton Drive |
The first two rows are the only genuinely unrecoverable ones, and today nothing
warns you they are non-empty before you walk away from a machine. That is tracked
separately from this document as a pending-writes visibility gap.
Only the first two rows are real losses. Before you retire a working machine, check that
`pdfs sync queue` prints "Nothing queued" (or that **Sync → Overview** shows nothing waiting).
### Before anything else: revoke
### Before anything else: revoke the old session
The lost machine holds a decrypted content cache, a plaintext index of every node
name in your Drive (`cache.db`), and a live session in its keyring. Whether that
matters depends on whether its disk was encrypted — but the session does not.
The lost machine holds a decrypted content cache, a plain-text index of your file names and a
live session in its keyring. Whether its disk was encrypted decides how much of that is exposed,
but the session is valid either way.
**Revoke the session from your Proton account settings before restoring.** Nothing
in this client can do that for you; the daemon on the lost machine holds
credentials it will happily keep refreshing.
**Sign the lost machine out from your Proton account's security settings before you restore.**
The client cannot do that for you.
If you have also lost your password, Proton's recovery phrase is what determines
whether the data is reachable at all. That is upstream of this client entirely: if
zero-knowledge encryption worked as advertised, nobody at Proton can help.
If you have also lost your password, account recovery is Proton's matter, not this client's.
Proton Drive is end-to-end encrypted: without the password or a recovery method, the data cannot
be decrypted.
---
## 2. Restore your synced folders
## 2. The runbook that works today
### Step 1: Install and sign in
### 2.1 Install, log in
Install the client as described in [INSTALL.md](INSTALL.md) and sign in with `pdfs-app`. Signing
in from the app also starts the service.
```bash
pdfs login # stores the session in the keyring
systemctl --user start proton-drive # or let autostart do it
```
### Step 2: Continue the old computer's backup
### 2.2 Re-adopt the device — check this before anything else
Tell this machine which computer it replaces, so new changes go into the existing backup instead
of a second, empty one.
`Core::ensure_device` recovers an existing device by **matching a Linux device
whose name equals this machine's hostname**, read from
`/proc/sys/kernel/hostname`. So:
- **In the app:** open **Computers**, open the menu of the old computer and choose
**Continue This Backup Here…**.
- **From a terminal:**
```bash
hostnamectl hostname # what this machine will register as
pdfs devices list # what the account already has
```
```bash
pdfs devices list # find the old computer's UID
pdfs devices adopt UID
```
- **Hostnames match** → the new machine silently re-adopts the old device and its
root folder. This is the happy path, and it is why the rest of the runbook works.
- **Hostnames differ** → `pdfs` registers a **second device**. Nothing errors.
Your old device and all its folders are still there, but this machine is not
attached to them, and every step below will quietly create empty folders under
the new device instead.
Adoption takes effect at once and is stored in `config.json` as `device_uid`. It survives a
change of hostname. `pdfs devices adopt --clear` returns to the default, which is to match a
computer whose name equals this machine's hostname.
> **The single most important step in this document:** if the hostnames differ,
> set the new machine's hostname to match the old device's name *before* adding
> any sync folders.
> ```bash
> sudo hostnamectl set-hostname OLD-DEVICE-NAME
> ```
> There is no supported way to adopt a device by uid yet — see §3.1.
> [!NOTE]
> Without adoption, the client only finds the old computer if this machine has the same
> hostname. Otherwise it registers a new computer, and nothing points at the old backup.
### 2.3 Find out which folders the device had
### Step 3: Restore the folders
```bash
pdfs devices list # confirm the device and note its name
```
- **In the app:** open **Computers**, open the menu of **This computer** and choose
**Restore Folders…**. Confirm or change the local path of each folder.
- **From a terminal:**
The device's synced folders are the folders directly under its root in Drive.
Browse them in the GUI's Computers page, or in the Proton Drive web app under
that device. **Write down their exact names** — you need them in the next step.
```console
$ pdfs sync restore
Documents [mirror] -> /home/you/Documents (Enter to accept, 'n' to skip, or a path)
Projects [ondemand] -> /home/you/Projects (Enter to accept, 'n' to skip, or a path)
```
### 2.4 Re-attach each folder
Press Enter to accept a path, type `n` to skip the folder, or type another path. `--yes`
accepts every proposal without asking.
For each folder name you noted, create the local directory and add it:
For each folder you accept, the service creates the local directory, attaches it to the folder in
the backup, restores its mode (**Mirror** or **Online only**) and downloads it. Follow the
progress on **Sync → Overview** or with `pdfs sync list`.
```bash
mkdir -p ~/Documents
pdfs sync add ~/Documents
```
> [!TIP]
> Restore into empty or missing directories. If a directory already holds files, for example
> from another backup, the client cannot tell which side is newer. It keeps both: your local file
> is renamed to a `(sync-conflict …)` copy and the version from Proton Drive is downloaded. See
> [Conflicts](USER_GUIDE.md#conflicts).
`add_sync_folder` looks for a folder under the device root named after the local
directory's **basename**, and reuses it if found rather than creating a second
one. So `~/Documents` re-attaches to the existing `Documents`. With an empty
baseline and an empty local directory, the first reconcile classifies every remote
path as `(None, Some(remote))` with no baseline → **download**, and the folder
refills from Drive.
### Step 4: Restore the rest by hand
Three things to be careful about:
- **Available offline files.** The profile records them, but the restore does not apply them yet.
Mark them again in the app, or with `pdfs pin PATH`.
- **Preferences.** Set the cache limit, bandwidth limits, mountpoint and appearance again.
- **The basename must match exactly.** Restoring to `~/docs` when the device
folder is `Documents` does not re-attach — it creates a *new*, empty `docs`
folder in Drive and syncs nothing. The failure is silent; you just see an empty
directory. Check with `pdfs sync list` and the Computers page.
- **Add the folder empty, or accept conflict copies.** If the local directory
already has files (restored from a backup, say), there is no baseline to say
which side is newer, so both sides read as changed and the reconcile takes the
conflict arm: your local file is renamed to a `(sync-conflict <ts>)` copy and
the remote version is downloaded. Nothing is lost, but it is noisy. Restoring
into an empty directory avoids it.
- **A wholly empty local tree is now safe.** Before the audit fixes, adding a
folder whose local side was empty against a populated remote could, on a later
pass, be read as "the user deleted everything" and trash the lot. `sync.rs`'s
`guard_local_wipe` now refuses any pass where every baseline path has vanished
locally, which is exactly the restore shape.
### Step 5: Clean up
### 2.5 Restore modes, pins, and settings by hand
```bash
pdfs sync list # note the ids
pdfs sync mode <id> ondemand # for folders that were on-demand
```
Nothing records which folders were `ondemand` before, so this is from memory. Pins
and the cache budget/mountpoint likewise: set them again from the Settings page.
### 2.6 Clean up
If a duplicate device got created before you noticed the hostname mismatch:
If this machine registered a second computer before you adopted the old one, remove it:
```bash
pdfs devices list
pdfs devices rm <uid-of-the-stray-device>
pdfs devices rm UID
```
Check what is under it in the web app first. Deleting a device you actually
attached folders to will take its folders with it.
> [!WARNING]
> Removing a computer deletes its backup from Proton Drive. Check what is under it in
> **Computers → Browse Files** first.
---
### Restoring another computer's folders
## 3. The plan: make this one command
To bring folders from a computer that still exists, for example a desktop whose documents you want
on a laptop, use **Restore to This Computer…** on that computer's row, or:
The pieces all exist — device enumeration, folder-name reuse, an empty-baseline
reconcile that downloads correctly. What is missing is that the restore is
**implicit, name-based, and undiscoverable**. Each phase below removes one of
those properties. They are independent and land in this order by value.
```bash
pdfs sync restore --device UID
```
### P1 — Adopt a device explicitly, by uid
The folders stay part of the other computer's backup and sync in both directions. This machine
does not take over that computer's backup.
**Problem:** device identity is inferred from the hostname. A new laptop with a
new name silently forks a second device, and the failure looks like "my files
didn't come back" rather than an error.
## 3. How the restore works
- Add `Request::AdoptDevice { uid }`: validate the uid against
`enumerate_devices`, write the `StoredDevice` row, and refuse if any
`sync_folder` rows already reference a different device.
- `pdfs devices adopt <uid>`, and an "Adopt this device" action on rows in the
GUI's Computers page that are not the current machine.
- Make the fork *visible* rather than silent: when `ensure_device` is about to
create a device and the account already has other Linux devices, log a warning
naming them, and surface it in `pdfs status`.
This section is for the curious and for anyone debugging a restore.
Keep hostname matching as the default. It is right for the common case; it just
must not be the *only* way.
- **Which computer the machine is.** At start, the service uses `device_uid` from `config.json`
when it is set. When it is not, it looks for a Linux computer in the account whose name equals
the hostname, and registers a new one when there is none. An adopted UID that no longer exists
in the account is reported in the log instead of being replaced silently.
- **What can be restored.** `pdfs sync restore` lists the folders directly under the computer's
backup, except `.proton-drive-linux`. Folders already synced on this machine are skipped.
- **Proposed paths.** When the profile names a local path whose parent directory exists on this
machine, that path is proposed. Otherwise the proposal is `~/<folder name>`. The service never
writes to a path you have not confirmed.
- **Attaching by UID.** The restore binds each local path to the remote folder's UID, not to its
name. Restoring `Documents` into `~/docs` works.
- **Downloading.** A restored folder starts with an empty sync baseline. Against an empty local
directory, every remote file counts as new and is downloaded. Files present on both sides with
no baseline become conflict copies, as described above.
- **The wipe guard.** A sync pass in which every previously synced file has vanished locally is
refused instead of being mirrored to Proton Drive as a mass deletion. A folder that was emptied
by accident therefore does not empty the backup.
- **The profile format.** `profile.json` carries a `version` field. A client refuses a profile
written by a newer, incompatible client instead of applying part of it.
### P2 — Discover and re-attach the device's folders
## 4. Known gaps
**Problem:** §2.3 and §2.4 are "remember what you had, then retype it exactly."
- Add `Request::ListDeviceFolders { device_uid }` — enumerate the device root's
child folders and report name, node uid, child count, and total size. This is
the "what did this machine sync?" answer that today requires the web app.
- Add `Request::AttachSyncFolder { remote_uid, local_path, mode }` — the
re-attach counterpart to `AddSyncFolder`. It binds a local path to a **specific
remote uid** rather than resolving by basename, which removes the silent
`~/docs` vs `Documents` failure entirely.
- CLI: `pdfs sync restore [--device <uid>] [--into <dir>]` — list the device's
folders, propose `<dir>/<name>` for each (default `$HOME`), and attach the ones
the user confirms.
- GUI: a "Restore folders from this device" flow on the Computers page, with a
per-folder local-path picker defaulting to `~/<name>`.
Refuse to attach into a non-empty directory without an explicit
`--allow-nonempty`, so the conflict-copy noise of §2.4 is opt-in rather than a
surprise.
### P3 — Back the profile up to Drive
**Problem:** modes, pins, mountpoint, and cache budget exist only on the dead
machine, so even a perfect P2 restores folders in the wrong mode with no pins.
Write a small JSON document into a hidden folder in the user's own Drive — say
`.proton-drive-linux/profile.json` — containing, per device:
- sync folders: remote uid, the local path last used, and mode
- pinned node uids
- mountpoint and cache budget
It rides on Drive's own encryption, so this introduces no new crypto and no new
place for plaintext to sit. Rewrite it whenever any of those change, debounced.
On a fresh install, after login: *"Found settings from `<device>`, last updated
`<date>`. Restore?"* Node uids are stable across machines, so pins restore
exactly. Local paths need remapping when the username or home differs — show the
old path, default the new one to `$HOME/<basename>`.
Store the last-used local path only as a *hint*. Restoring a profile must never
write to a path the user has not confirmed in this session.
### P4 — Make the unrecoverable visible
Nothing above helps with §1's undrained writes, because by then it is too late.
The mitigation is to make the pending queue impossible to miss *before* the
machine is lost:
- pending count and bytes in `pdfs status`, using the existing `pending` map and
`TransferRegistry`
- a tray icon state and a GUI banner while the queue is non-empty
- a warning on logout or shutdown while it is non-empty
### P5 — Verification
None of the above is trustworthy without exercising the actual shape of the
disaster. The end-to-end test is: register a device, sync a folder, upload
content, **delete the local state directory and cache entirely** (simulating the
lost machine), restart, restore, and assert the tree comes back byte-identical.
That is a two-account or at least two-state-dir integration test against a live
account, not a unit test. Until the mock-backend problem described in
`audit-plan.md` is solved, it stays manual — but it should be run, and its result
recorded here, before P1–P3 are called done.
---
## 4. Current status
| Phase | State |
| Gap | Effect |
|---|---|
| Manual runbook (§2) | Works, with the hostname and basename caveats |
| P1 explicit adoption | Not started |
| P2 discover + attach | Not started |
| P3 profile on Drive | Not started |
| P4 pending visibility | Not started |
| P5 restore drill | Never run |
| Pins are backed up but not restored | Mark files available offline again by hand |
| No warning at shutdown or sign-out while changes are queued | Check `pdfs sync queue` before you retire a machine |
| The full disaster drill (delete all local state, restore, compare byte for byte) has not been run against a live account | The individual steps are tested; the whole sequence is not |
Planned work is tracked in [ROADMAP.md](ROADMAP.md).