diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2f8aa2e..fe07de2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -255,7 +255,9 @@ not one this daemon sealed itself (`own_sealed_rev` table), the queued bytes are conflict copy (`Core::revision_conflict`) next to the remote version. A name taken on Drive by a node this machine has not seen lands under a conflict name too. Both go to the activity log. -Conflict copies are named `{stem} (sync-conflict {unix_secs}).{ext}`. +Conflict copies are named `{stem} (sync-conflict {host} {unix_secs}).{ext}`, where `{host}` is the +hostname cut down to one token (`conflict_label`). `conflict_base_name` also reads the older form +without a host. ### Failures and sync issues @@ -342,6 +344,22 @@ Safety rules: - **Ignore rules.** `.pdfsignore` (or `.protonignore`) at the folder root plus `ignore_patterns` from `config.json`, in gitignore syntax. Ignoring never deletes. +Another client may write the same folder, such as a second computer syncing into the device. The +baseline therefore also records the active revision id each file was synced at (schema 44): + +- **A new revision is a remote change** even when the link's modification time and the size come + out the same. +- **An upload checks first.** `upload_revision` reads the node and leaves a file whose revision + moved since the walk for the next pass, which classifies it as a conflict. A revision committed + between that check and the SDK's own revision draft is not caught. +- **The baseline records what was transferred.** A download records the revision the pass + planned to fetch, not a read-back that may already show a newer one. An upload records the + read-back only when its SHA-1 is the one sent. +- **A first sync compares content.** A path present on both sides without a baseline is adopted + only when the local SHA-1 matches Drive's digest; otherwise it is a conflict. + +Remote changes still reach a mirror only through the 120 s poll. + ### Moves between locations My files and each online-only folder are separate FUSE sessions, and a mirrored folder has none, @@ -369,12 +387,18 @@ named like the hostname, else a new one. An adopted uid that no longer exists is replaced silently. The local database does not survive a lost machine, so the daemon backs up its arrangement to -`/.proton-drive-linux/profile.json` (`pdfs_core::profile`): each synced folder's +`/.proton-drive-linux/profile-.json` (`pdfs_core::profile`): each synced folder's remote uid, local path and mode, the pins, and the ignore patterns. It is uploaded like any other file, so it is end-to-end encrypted. `Core::touch_profile` coalesces bursts of changes into one upload. `PROFILE_VERSION` guards the format: a profile from a newer client is refused, not applied in part. +The machine id is random and lives in `machine-id` next to `config.json`. Each machine has its +own file because several computers may sync into one device: `pdfs devices join` and **Sync With +This Computer…** adopt another computer's device without taking it over. Restore reads every +profile there, the legacy shared `profile.json` included, and prefers the one with this hostname, +then the newest (`profile::saved_folder`). + Restore lists the device's folders, proposes a local path from the profile, and on confirmation adds a synced folder bound to the remote uid. The download is an ordinary pass against an empty baseline. The user side is in [RECOVERY.md](RECOVERY.md). diff --git a/docs/BUGS.md b/docs/BUGS.md index f37c358..6e2f8c1 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -100,6 +100,11 @@ a live account since. | Entry | Title | |---|---| +| [B252](#b252--computers-sharing-a-device-overwrite-each-others-profile) | Computers sharing a device overwrite each other's profile | +| [B251](#b251--two-computers-can-make-the-same-conflict-copy-name) | Two computers can make the same conflict copy name | +| [B250](#b250--a-revision-made-during-a-mirror-download-never-arrives) | A revision made during a mirror download never arrives | +| [B249](#b249--a-mirror-edit-replaces-a-revision-another-computer-made-since) | A mirror edit replaces a revision another computer made since | +| [B248](#b248--a-joining-mirror-takes-a-same-size-file-for-the-same-file) | A joining mirror takes a same-size file for the same file | | [B247](#b247--tray-status-lines-are-not-translated) | Tray status lines are not translated | | [B246](#b246--signing-in-with-a-locked-keyring-freezes-the-app-window) | Signing in with a locked keyring freezes the app window | | [B245](#b245--any-child-of-the-daemon-can-answer-the-watchdog-for-it) | Any child of the daemon can answer the watchdog for it | @@ -350,6 +355,126 @@ a live account since. Newest first. +### B252 — Computers sharing a device overwrite each other's profile + +**Status:** Fixed, not verified live +**Found:** 2026-10-16, design review for syncing several computers into one device. +**Where:** `save_profile` and `load_profiles` in `crates/pdfs-fuse/src/profile.rs` + +**Repro:** Adopt one device on two computers and sync a different folder on each. The +device's `profile.json` lists only the folders of whichever computer saved last. + +**Cause.** The profile was one file per device, written as a new revision by every computer +that synced into it, each with only its own folders. + +**Fix.** Each machine writes `profile-.json`. The id is random, made on first use +and kept in `machine-id` next to `config.json`, so nothing that identifies the host goes to +Drive. Restore reads every profile in the folder, including the shared `profile.json` of older +clients. A folder's proposal comes from this computer's own profile, otherwise from the newest +one that has the folder. The legacy file is left alone. + +**Test:** `the_saved_folder_comes_from_this_computer_then_the_newest_profile` and +`a_profile_name_is_the_legacy_one_or_one_per_machine` in `pdfs-core`; +`each_machine_saves_its_own_profile` in `pdfs-fuse`. + +--- + +### B251 — Two computers can make the same conflict copy name + +**Status:** Fixed, not verified live +**Found:** 2026-10-16, design review for syncing several computers into one device. +**Where:** `conflict_path_with_suffix` in `crates/pdfs-fuse/src/sync/planner.rs`, `conflict_name` +in `crates/pdfs-fuse/src/lib.rs` + +**Repro:** Two computers sync one folder and each keeps a conflict copy of the same file in the +same second. Both name it `notes (sync-conflict 1700000000).txt`, so the second upload meets +the first on Drive, and neither copy says which computer it came from. + +**Cause.** The name held only the time. The `-` suffix is made unique on the local disk, not +on Drive. + +**Fix.** Conflict copies carry the hostname, cut down to one token of letters, digits, `-`, `_` +and `.`: `notes (sync-conflict laptop 1700000000).txt`. `conflict_base_name` reads both forms, +so the sweep and **Resolve** still find copies made before. + +**Test:** `a_hostname_becomes_one_safe_token`, `conflict_name_keeps_the_extension`, +`conflict_path_keeps_extension_and_directory` and `parses_conflict_names_and_leaves_others_alone`. + +--- + +### B250 — A revision made during a mirror download never arrives + +**Status:** Fixed, not verified live +**Found:** 2026-10-16, design review for syncing several computers into one device. +**Where:** `download_file` and `record_file_baseline` in `crates/pdfs-fuse/src/sync.rs` + +**Repro:** Another computer saves a file in a mirrored folder while this computer downloads the +previous version. This computer keeps the previous version, and later passes do not fetch the +newer one. + +**Cause.** After a download the baseline read the node again and recorded what Drive held then. +A revision committed while the download ran was recorded as synced, although the disk held the +older one. + +**Fix.** The baseline records the revision the pass planned to download. If the download already +fetched the newer one, the next pass fetches it once more, which costs a transfer and loses +nothing. After an upload the read-back is still used, but only when Drive's digest is the one +just sent; otherwise the baseline is left as it was and the next pass keeps a conflict copy. + +**Test:** `a_revision_made_during_a_mirror_download_still_arrives` (simulation, FUSE). It fails +with the old read-back. + +--- + +### B249 — A mirror edit replaces a revision another computer made since + +**Status:** Fixed, not verified live +**Found:** 2026-10-16, design review for syncing several computers into one device. +**Where:** `upload_revision` in `crates/pdfs-fuse/src/sync.rs` + +**Repro:** Edit a file in a mirrored folder while another computer saves its own edit of the +same file, after this computer's pass looked at Drive and before it uploads. This computer's +revision becomes the current one. The other edit survives only in the version history, and no +conflict copy is made. + +**Cause.** The upload made a new revision of whatever Drive held at that moment, from a plan +that could be minutes old. Nothing compared Drive with the baseline before uploading. Change +detection used only the link's modification time and the size. + +**Fix.** The baseline records the revision id it was synced at (schema 44). The upload reads +the node first and leaves a file whose revision moved for the next pass, which keeps the local +edit as a conflict copy. A new revision id counts as a remote change even at the same +modification time and size. A revision committed between the check and the upload's own draft +is not caught, because the SDK reads the current revision itself; the read-back after the upload +catches one committed after it. + +**Test:** `a_mirror_edit_does_not_replace_a_revision_another_computer_made_since` (simulation, +FUSE), `a_new_remote_revision_is_a_change_even_at_the_same_mtime_and_size` and +`migration_v44_adds_the_synced_revision_without_disturbing_a_v43_baseline`. + +--- + +### B248 — A joining mirror takes a same-size file for the same file + +**Status:** Fixed, not verified live +**Found:** 2026-10-16, design review for syncing several computers into one device. +**Where:** `plan_file` in `crates/pdfs-fuse/src/sync/planner.rs` + +**Repro:** Sync a local folder into a folder on Drive that already holds a file of the same name +and size but different content, for example on a second computer joining a folder. The first +pass records both as in step, and the two computers go on holding different files. + +**Cause.** The first sync of a path present on both sides adopted the baseline whenever the sizes +matched, to avoid a conflict copy of every pre-existing file. + +**Fix.** The adoption needs Drive's content digest, and the local file is hashed first. A match +is adopted; a different file, or one without a digest on Drive, is kept as a conflict copy. + +**Test:** `a_joining_mirror_keeps_a_same_size_different_file_as_a_conflict` (simulation, FUSE) +and `first_sync_without_a_remote_digest_is_a_conflict`. + +--- + ### B247 — Tray status lines are not translated **Status:** Fixed, not verified live diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 88d6e29..274c92e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,10 +13,28 @@ scratch (user data in `staging/` and `recovery/` is never touched by this). ## [3.5.0] — 2026-10-16 -Schema: **43**. Queued changes get an index by kind and name. Nothing is rewritten, and the -upgrade takes a moment. +Schema: **44**. Queued changes get an index by kind and name, and each synced file remembers the +version on Proton Drive it was synced at. Nothing is rewritten, and the upgrade takes a moment. + +### Added +- **Several computers can sync the same folders.** On the Computers page, **Sync With This + Computer…** on another computer, or `pdfs devices join`, makes this computer sync into that + computer's backup alongside it, then offers its folders for syncing here. Both computers keep + them in step, deletions included. Each computer now keeps its own settings backup in the + shared backup, so they no longer overwrite each other (B252). ### Fixed +- **An edit in a synced folder could replace one another computer had just saved.** The newer + version from the other computer was left only in the version history. The edit is now kept as + a conflict copy (B249). +- **A version saved on another computer while this one downloaded the previous version never + arrived.** (B250) +- **Syncing a folder into one that already held a file of the same name and size took the two + for the same file**, even when their content differed. The content is now compared, and a + different file is kept as a conflict copy (B248). +- **Conflict copies now name the computer that made them**, for example + `notes (sync-conflict laptop 1700000000).txt`, so two computers no longer make the same name + (B251). - **A folder deleted on another device took files with it that had never reached Drive.** A new file, an edit or an ignored `.git` in a synced folder was deleted along with the folder. Such a folder is now kept and created on Drive again (B211). diff --git a/docs/CLI.md b/docs/CLI.md index 312da01..a0e2045 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -102,6 +102,7 @@ commands are for scripts and for cases where the mount is not available. | `pdfs devices list` | List computers registered to the account. | | `pdfs devices rename UID NAME` | Rename a computer. | | `pdfs devices rm UID` | Remove a computer and its backup from the account. | +| `pdfs devices join UID [--yes]` | Sync the same folders as another computer. This machine syncs into that computer's backup alongside it, then offers its folders like `pdfs sync restore`. Both computers keep them in step. `--yes` skips the confirmation and accepts every proposed path. | | `pdfs devices adopt UID` | Make this machine continue another computer's backup, for example after a reinstall. `--clear` returns to matching by hostname. | | `pdfs devices migrate UID [--yes]` | Move another computer's folders into this computer's backup, then restore them here like `pdfs sync restore`. The other computer stays registered, without those folders. `--yes` skips the confirmation and accepts every proposed path. See [RECOVERY.md](RECOVERY.md). | diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 7b74336..f6aee91 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -14,6 +14,7 @@ and where the client keeps its data. | Path | Contents | |---|---| | `~/.config/proton-drive-linux/config.json` | Settings (this page) | +| `~/.config/proton-drive-linux/machine-id` | A random id for this machine. It names this machine's profile in a computer's backup that several computers may share | | `~/.local/state/proton-drive-linux/cache.db` | Metadata database: names, folder tree, sync state, upload queue, photo index | | `~/.local/state/proton-drive-linux/sdk_cache.db` | Encrypted cache of Proton Drive metadata. Safe to delete while the service is stopped | | `~/.local/state/proton-drive-linux/control.sock` | Socket the app, tray, prompt and CLI use to reach the service | @@ -78,7 +79,7 @@ fix it. | `upload_limit` | bytes/s | none | General → Network | Shared cap for all uploads. `0` or `null` means none | | `download_limit` | bytes/s | none | General → Network | Shared cap for all downloads, reads from the mount included | | `ignore_patterns` | list | see below | — | Global ignore rules for synced folders | -| `device_uid` | string | none | Computers → Continue This Backup Here… | The computer this machine continues. Unset means "match by hostname" | +| `device_uid` | string | none | Computers → Continue This Backup Here… or Sync With This Computer… | The computer this machine syncs into. Unset means "match by hostname" | | `conflict_sweep` | `off` / `report` / `enforce` | `report` | — | See [`conflict_sweep`](#conflict_sweep) | | `local_first` | bool | `true` | — | See [`local_first`](#local_first) | | `language` | gettext code | system | General → Appearance | Language of the app, tray and prompt, such as `de` or `pt_BR` | diff --git a/docs/RECOVERY.md b/docs/RECOVERY.md index 8d0b47c..b6a58f4 100644 --- a/docs/RECOVERY.md +++ b/docs/RECOVERY.md @@ -17,7 +17,7 @@ your synced folders back on the replacement machine. | Every file that finished uploading | Proton Drive | | 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 | +| 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, one per computer that syncs into it, end-to-end encrypted like any other file. Clients before 3.5.0 wrote `profile.json` | | Your account, keys and shares | Proton | The service rewrites the profile a short while after you add, remove or switch a synced folder, @@ -166,18 +166,21 @@ This section is for the curious and for anyone debugging a restore. 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 +- **Proposed paths.** Several computers may sync into one backup, each with its own profile. The + profile written by a computer with this machine's hostname is used first, then the newest one + that has the folder. When it names a local path whose parent directory exists on this machine, that path is proposed. Otherwise the proposal is `~/`. 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. + no baseline are compared by content: identical files are taken as they are, and different + ones 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 +- **The profile format.** Each profile carries a `version` field. A client refuses a profile written by a newer, incompatible client instead of applying part of it. ## 4. Known gaps diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 1981ce6..c8292d8 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -188,15 +188,16 @@ Before you shut down a computer for good, check that nothing is waiting: see When the same file changed on this computer and somewhere else since the last sync, neither side is thrown away. The local version is kept as a copy named like -`report (sync-conflict 1759068180).odt`, where the number is the time of the conflict in seconds -since 1970, and the remote version takes the original name. +`report (sync-conflict laptop 1759068180).odt`, where `laptop` is the name of the computer that +made the copy and the number is the time of the conflict in seconds since 1970. The remote +version takes the original name. Copies made before 3.5.0 have no computer name. Conflicts appear under **Sync → Overview → Needs attention** and in `pdfs conflicts`. For each one, keep the original, keep the copy, or keep both under a new name: ```bash pdfs conflicts -pdfs conflicts resolve "Documents/report (sync-conflict 1759068180).odt" --keep copy +pdfs conflicts resolve "Documents/report (sync-conflict laptop 1759068180).odt" --keep copy ``` The version you do not keep goes to the Trash, so a wrong choice can be undone. @@ -284,12 +285,31 @@ Proton Drive keeps earlier versions of every file. **Details → Versions…** i back folders that are backed up under it but not synced here yet. - **Another computer** can be browsed read-only in the same list or grid as My files, and **Restore to This Computer…** downloads its folders here. +- **Sync With This Computer…** makes this computer sync the same folders as another one, for + example a desktop and a laptop. Both keep the folders in step: a change or a deletion on one + reaches the other. See [Syncing several computers](#syncing-several-computers). - **Continue This Backup Here…** makes this computer take over another computer's backup, for example after reinstalling or replacing a machine. See [RECOVERY.md](RECOVERY.md). - **Move to This Computer…** moves another computer's folders into this computer's backup, for example when you switch from Windows to Linux, and then offers them for syncing here. The other computer stays in the list, without those folders. +### Syncing several computers + +Two or more computers can sync the same folders through one backup in Proton Drive. + +1. Sync the folders on the first computer as usual. +2. On the second computer, open **Computers**, open the menu of the first computer and choose + **Sync With This Computer…**. From a terminal: `pdfs devices join UID`, with the UID from + `pdfs devices list`. +3. Choose which of its folders to sync here and where, as in a restore. + +From then on, both computers sync into that backup. A folder you add later on either computer +lands there too, and a folder with the same name joins the one already there. An edit on one +computer reaches the other within about two minutes. When both computers change the same file in +that time, each keeps the other's version under the original name and its own as a conflict copy +named after it. Folders that were synced here before joining stay in this computer's own backup. + ## Search launcher `pdfs-prompt` searches Proton Drive and your home folder together, in one ranked list. It