From 9460c93b505a906384e85b5f76c9ca9c124d9c6e Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 31 Oct 2026 09:47:54 +0100 Subject: [PATCH 01/12] docs(bugs): record the audit of issues #53, #46, #40 and #38 B258 to B273: why a stuck sync, a failing mount and a silent restore are invisible, plus the sync, mount and restore bugs behind them. B165, B202, B205 and B210 point at the entries that finish them. --- docs/BUGS.md | 377 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 377 insertions(+) diff --git a/docs/BUGS.md b/docs/BUGS.md index e701f5a..319b4ca 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -68,6 +68,22 @@ original wording. | Entry | Title | What is left | |---|---|---| +| [B273](#b273--a-restore-proposes-another-computers-paths-when-the-two-share-a-hostname) | A restore proposes another computer's paths when the two share a hostname | Not fixed | +| [B272](#b272--after-a-folder-is-removed-and-added-again-its-file-events-go-to-the-old-id) | After a folder is removed and added again, its file events go to the old id | Not fixed | +| [B271](#b271--one-failed-listing-makes-a-large-first-sync-start-over-from-nothing) | One failed listing makes a large first sync start over from nothing | Not fixed | +| [B270](#b270--one-file-left-in-a-mirror-folder-clears-the-wipe-guard-for-all-the-others) | One file left in a mirror folder clears the wipe guard for all the others | Not fixed | +| [B269](#b269--a-restore-that-skipped-every-folder-reports-success) | A restore that skipped every folder reports success | Not fixed | +| [B268](#b268--pdfs-diagnose-says-no-problems-found-while-sync-is-stuck) | `pdfs diagnose` says "No problems found" while sync is stuck | Not fixed | +| [B267](#b267--a-key-failure-at-a-start-from-a-warm-cache-shows-as-offline-for-good) | A key failure at a start from a warm cache shows as offline for good | Not fixed | +| [B266](#b266--pin-unpin-search-and-node-loads-report-a-database-error-as-offline) | Pin, unpin, search and node loads report a database error as "offline" | Not fixed | +| [B265](#b265--address-keys-whose-token-is-for-an-inactive-account-key-never-open) | Address keys whose token is for an inactive account key never open | Needs an SDK change; not reproduced | +| [B264](#b264--a-mount-that-fails-after-starting-its-workers-leaves-them-running) | A mount that fails after starting its workers leaves them running | Not fixed | +| [B263](#b263--a-mount-that-can-never-succeed-retries-every-5-s-and-tells-only-the-journal) | A mount that can never succeed retries every 5 s and tells only the journal | Not fixed | +| [B262](#b262--watching-a-large-synced-folder-holds-up-the-sync-engine-and-a-failed-watch-is-never-retried) | Watching a large synced folder holds up the sync engine, and a failed watch is never retried | Not fixed | +| [B261](#b261--a-folder-that-has-never-synced-reads-up-to-date) | A folder that has never synced reads "up to date" | Not fixed | +| [B260](#b260--the-poll-pushes-a-new-folders-first-sync-behind-every-other-folder) | The poll pushes a new folder's first sync behind every other folder | Not fixed | +| [B259](#b259--the-sync-engine-can-die-or-hang-and-nothing-shows-it-until-a-restart) | The sync engine can die or hang, and nothing shows it until a restart | Not fixed; the trigger in #53 is not known | +| [B258](#b258--journalctl--p-warning-shows-nothing-whatever-the-daemon-logged) | `journalctl -p warning` shows nothing, whatever the daemon logged | Not fixed | | [B134](#b134--a-file-deleted-online-while-the-drain-checks-its-write-is-kept-as-a-conflict-copy) | A file deleted online while the drain checks its write is kept as a conflict copy | Only with `local_first` off | | [B91](#b91--the-daemon-hung-overnight-and-nothing-could-say-what-it-was-waiting-on) | The daemon hung overnight, and nothing could say what it was waiting on | The cause is not diagnosed; the daemon now reports where it is stuck and is restarted | | [B67](#b67--fresh-state-restore-can-silently-select-a-new-device-high-22) | Fresh-state restore can silently select a new device (HIGH-22) | An ambiguous device after a loss of local state is not asked about | @@ -359,6 +375,361 @@ a live account since. Newest first. +### B273 — A restore proposes another computer's paths when the two share a hostname + +**Status:** Open +**Found:** 2026-10-31, code audit after issue #46. +**Where:** `saved_folder` in `crates/pdfs-core/src/profile.rs` + +**Repro:** Two computers with the same hostname, for example both left at a distribution's +default `archlinux`, sync the same device. Restore a folder on one of them. The proposed local +path and mode come from whichever of the two profiles is found first, not from this computer's. + +**Cause.** `saved_folder` prefers the profile whose `hostname` matches. The machine id that +B252 added tells computers apart, but this lookup does not use it. + +**Fix.** Not done. Match on the machine id first, then fall back to the hostname for profiles +written before it existed. + +--- + +### B272 — After a folder is removed and added again, its file events go to the old id + +**Status:** Open +**Found:** 2026-10-31, code audit after issue #53. +**Where:** `rewatch` in `crates/pdfs-fuse/src/sync/engine.rs` + +**Repro:** Remove a synced folder and add the same path again before the engine handles the +first `Rewatch`, so both land in one burst. Edits in the folder are not synced until the +two-minute poll, and the same goes for every later edit until the daemon restarts. + +**Cause.** `rewatch` matches the watches it has against the ones it wants by path alone. The +path is still wanted, so the old entry stays with the removed folder's id. Its events become +`Reconcile(old id)`, `sync_folder_get` finds no row, and the request is dropped. + +**Fix.** Not done. Compare `(path, id)` pairs, and update the id of a path that is still +watched. + +--- + +### B271 — One failed listing makes a large first sync start over from nothing + +**Status:** Open +**Found:** 2026-10-31, code audit after issue #46: a folder of about 19,900 items restored on a +second computer sat at `scanning…` with nothing downloaded, and the journal showed +`RequestTimeout (http 408)`. +**Where:** `walk_remote` in `crates/pdfs-fuse/src/sync.rs` + +**Repro:** Restore or add a mirror folder with tens of thousands of items on Drive, on a link +that drops a request now and then. The pass walks the whole remote tree before it downloads +anything. When one folder's listing fails after the SDK's retries, the pass fails, and the +next one walks the whole tree again. On a slow enough link it never gets past the walk. + +**Cause.** Each `enumerate_folder_children_node_uids`, `enumerate_nodes_light` and +`enumerate_nodes` call in the walk ends the whole walk through `?`. Nothing is applied until +the walk is done. Files that did download keep their baseline rows, so only the walk is lost, +but on a first sync that is all of it. Progress is also counted only after a folder's full +listing, so a large folder shows `scanning…` with 0 checked for a long time. + +**Fix.** Not done. Record a failed subtree as a gap, as undecryptable nodes already are: no +deletions under it, and the rest of the pass still applies. + +--- + +### B270 — One file left in a mirror folder clears the wipe guard for all the others + +**Status:** Open +**Found:** 2026-10-31, code audit after issue #46, whose reporter emptied a synced folder +several times. +**Where:** `guard_local_wipe` and `guard_remote_wipe` in `crates/pdfs-fuse/src/sync/planner.rs` + +**Repro:** Sync a mirror folder with many files. Make all of them but one unavailable here: a +half-mounted disk, a restored backup that brought back one file, or a script that emptied the +folder and wrote one file back. The next pass trashes every other file on Drive. On a device +that two computers sync, the other computer then deletes its copies too. + +**Cause.** The guard refuses a pass only when every baseline path is missing. One surviving +path is enough to clear it, so it guards against an empty folder and nothing short of that. +The remote guard has the same shape. + +**Fix.** Not done. Also refuse when most of a large baseline is missing at once, and leave the +choice to the user: delete on the other side, or sync from scratch. + +--- + +### B269 — A restore that skipped every folder reports success + +**Status:** Open +**Found:** 2026-10-31, code audit after issue #46: the journal showed `restore complete +restored=0 skipped=1` several times, and the user saw nothing. +**Where:** the `RestoreSyncFolders` and `RestoreDeviceFolders` handler in +`crates/pdfs-fuse/src/control.rs`; `restore_sync_folders` in `crates/pdfs-fuse/src/profile.rs` + +**Repro:** Restore a folder from another computer to a path that is already a sync root here, +for example a folder left over from an earlier attempt. `pdfs devices join` prints `restoring +folders`, and the app shows the same. Nothing downloads, and no error appears anywhere. + +**Cause.** The daemon acks before it works, because a restore outlasts the socket timeout. The +result, `restored 0 folder(s); skipped: …`, is then logged as a successful activity, so the +reason is only in the activity text and the journal. When the database insert fails partway, +the `?` also skips the `Rewatch` for the folders already restored. + +**Fix.** Not done. Log a restore that skipped folders as a failure, name the reason in the app +and the CLI, and offer to replace or reset the folder in the way. Send the `Rewatch` on every +exit path. + +--- + +### B268 — `pdfs diagnose` says "No problems found" while sync is stuck + +**Status:** Open +**Found:** 2026-10-31, issue #53: a folder that never synced, and `diagnose` found nothing. +**Where:** `diagnose` in `crates/pdfs-cli/src/main.rs` + +**Repro:** Leave a synced folder in `error`, or have queued writes that keep failing, or pause +sync. `pdfs diagnose` reports every check `[ok]` and exits 0. + +**Cause.** The `Status` reply is matched with `..`, which drops `failing_ops`, `failing_error`, +`parked_uploads`, `staged_oldest_secs` and `paused`. `diagnose` never asks for the synced +folders or for `Diagnostics`, so a folder in error and a stalled worker are not seen. A +control socket file with no daemon behind it is also reported `[ok]`; only the next line says +the daemon does not answer. + +**Fix.** Not done. Warn on failing or parked writes, a pause, old staged data and folders in +error, and report the sync engine's state once B259 exposes it. + +--- + +### B267 — A key failure at a start from a warm cache shows as offline for good + +**Status:** Open +**Found:** 2026-10-31, code audit after issue #40. B202 fixed the same path for a refused +session only. +**Where:** `fetch_or_recall_root` in `crates/pdfs-fuse/src/mount.rs`; `run_online_probe` in +`crates/pdfs-fuse/src/link.rs` + +**Repro:** Sign in once so the cache holds the My Files root. Then start the daemon in a state +where the root cannot be decrypted, for example the address-key failure of B265. The mount +comes up "offline", the app says you are offline, and it stays so while the network is fine. + +**Cause.** Any error from `get_my_files_folder` other than a revoked session becomes +`Start::Offline` when a cached root exists. The online probe then retries the same call and +logs its failures at debug level, so the journal at the default level says nothing. + +**Fix.** Not done. Use `Start::Offline` only for an error `is_network_error` accepts, and +report the others as their own state through `Status`. + +--- + +### B266 — Pin, unpin, search and node loads report a database error as "offline" + +**Status:** Open +**Found:** 2026-10-31, code audit after issue #38. B205 fixed the photo upload path only. +**Where:** `CoreError::from_api` in `crates/pdfs-core/src/error.rs`; its callers in +`crates/pdfs-fuse/src/lib.rs` (`pin`, `unpin`, the searches, `load node`) + +**Repro:** Pin a folder while the cache database is locked or full. The app says "You're +offline" and drops the reason. + +**Cause.** `from_api` classifies any error that is not a `ProtonError` as `Offline`, because +it was written for SDK calls, where that means a transport error. These callers pass it +`pdfs_core::Error` values from the cache and the database. A `ProtonError` wrapped in +`pdfs_core::Error::Proton` also misses the downcast, since the source chain is not walked. + +**Fix.** Not done. Use `CoreError::internal` at the database callers, and walk the source +chain in `from_api` before falling back. + +--- + +### B265 — Address keys whose token is for an inactive account key never open + +**Status:** Open +**Found:** 2026-10-31, code audit after issue #40: a Duo account whose mount failed on every +start with `fetch My Files root: cryptography error: failed to decrypt: missing key`, after three +`failed to decrypt address key token` warnings. The web app worked. B210's fix does not apply, +because the account has an active volume. +**Where:** `user_keys` and `decrypt_address` in `proton-sdk` (`crates/proton-sdk/src/account.rs` +in `proton-sdk-rs`) + +**Repro:** Not reproduced. An account whose address-key tokens are encrypted to an account key +that is no longer active cannot open those address keys, and so cannot open the My Files share. + +**Cause.** `user_keys` skips every inactive account key, even one the password opens, and the +address-key tokens are decrypted only with what it returns. `keycheck.rs` in this repository +already reports "token is for inactive account key", so the case is known. Separately, +`addresses()` fails as a whole through `?` when one address has no usable primary key, which +takes the working addresses down with it. + +**Fix.** Not done, and it belongs in the SDK. Use every account key that opens for decrypting +tokens, and only the active ones for signing. Skip and log an address that cannot be opened. + +--- + +### B264 — A mount that fails after starting its workers leaves them running + +**Status:** Open +**Found:** 2026-10-31, code audit after issue #40. +**Where:** `run_mount` in `crates/pdfs-fuse/src/mount.rs` + +**Repro:** Make the mount fail after the workers start: a busy mountpoint, or a control socket +whose permissions cannot be set. The daemon retries every 5 s, and the drain, sync, sweep, +probe and index threads of the failed attempt keep running beside the next one. + +**Cause.** The early returns after the workers are spawned (the `?` on the socket bind, the +permission check, a `spawn_session` error, and a `?` on a later thread spawn) skip +`stop_workers`. The threads hold a `Core`, and the `Core` holds the database. If that keeps +the `flock` held, every later attempt fails with "another daemon is using cache.db" instead +of the real error. That part is inferred from the code and not observed. + +**Fix.** Not done. Bind the socket and check the mountpoint before any worker starts, and stop +the workers on every error path. + +--- + +### B263 — A mount that can never succeed retries every 5 s and tells only the journal + +**Status:** Open +**Found:** 2026-10-31, issue #40: the app showed "Connecting… waiting for the Proton Drive +service" for days, `pdfs diagnose` only reported the control socket missing, and the reporter +uninstalled. +**Where:** the mount loop in `crates/pdfs-cli/src/main.rs`; `run_mount` in +`crates/pdfs-fuse/src/mount.rs` + +**Repro:** Start the daemon with a session whose keys cannot open My Files (B265). The journal +shows `mount failed; retrying in 5s` forever. The app and the tray wait for the service, and +nothing says why. + +**Cause.** Every mount error gets the same 5 s retry, whether it is a network error or one that +no retry fixes, such as a key or a 4xx answer. Each attempt builds a new client and sends about +four requests, so a broken account sends tens of thousands a day, which may also feed +Proton's sign-in blocks (#39). The control socket is bound only after the mount succeeds, and +the error is written nowhere a front end can read it. + +**Fix.** Not done. Back off exponentially, treat key, `KeysLocked` and 4xx errors as permanent +with a long back-off, and write the last error where the app, the tray and `diagnose` can +show it (a state file, and `STATUS=` to systemd). + +--- + +### B262 — Watching a large synced folder holds up the sync engine, and a failed watch is never retried + +**Status:** Open +**Found:** 2026-10-31, code audit after issue #53. +**Where:** `rewatch` and `engine_loop` in `crates/pdfs-fuse/src/sync/engine.rs` + +**Repro:** Add a synced folder with many subfolders, or one that holds a symlink to a large +tree, on a system near `fs.inotify.max_user_watches`. The new folder's first pass waits until +the recursive watch is set up. When the watch fails, the journal has one `sync: watch failed` +line, the folder is never watched, and later changes are found only by the two-minute poll. + +**Cause.** +- `rewatch` runs on the engine thread before the reconcile, and notify's recursive watch walks + the whole tree, at startup for every folder. +- notify follows symlinks by default, and the local walk does not. +- When `watch` fails partway, the watches added before the failure stay registered, but the + folder is not recorded as watched. Their events match no folder and become `ReconcileAll`, + and they use up watches the next folder needs. +- Errors the watcher reports later are dropped in its callback. + +**Fix.** Not done. Reconcile first, unwatch after a partial failure and retry with a back-off, +turn off symlink following, and show a watch failure in the folder's state. + +--- + +### B261 — A folder that has never synced reads "up to date" + +**Status:** Open +**Found:** 2026-10-31, issue #53: "In Computers I add the folder. It appears as synced." +**Where:** `sync_folder` in `crates/pdfs-core/src/db/migrations.rs`; `sync_state_label` in +`crates/pdfs-gui/src/app/pages/devices.rs`; the location rows in +`crates/pdfs-gui/src/app/pages/locations.rs` + +**Repro:** Add a synced folder. Until its first pass starts, the app shows it as up to date. +The same happens after a switch from on-demand to mirror, after **Sync From Scratch…**, and for +every folder while sync is paused for the whole account. + +**Cause.** The `state` column defaults to `idle`, and every state the label does not know reads +"up to date". `last_sync` is 0 for a folder that has never synced, but the rows do not look at +it, and a test pins the label for that case. The rows only show a folder's own pause, not the +account-wide one. + +**Fix.** Not done. Show "waiting for first sync" when `last_sync` is 0, and show the +account-wide pause on the folder rows. + +--- + +### B260 — The poll pushes a new folder's first sync behind every other folder + +**Status:** Open +**Found:** 2026-10-31, code audit after issue #53. +**Where:** `engine_loop` in `crates/pdfs-fuse/src/sync/engine.rs` + +**Repro:** Sync a few large mirror folders whose passes take longer than two minutes, then add +another folder. Its first pass waits until every other folder has been walked again, each +time. + +**Cause.** The poll sends `ReconcileAll` every two minutes whether or not a pass is still +running. When a burst holds both a `ReconcileAll` and a folder's `Reconcile(id)`, the loop runs +only `reconcile_all`, which goes in id order. A new folder has the highest id, so it always +goes last. + +**Fix.** Not done. Run the folders asked for by id before the rest, and don't queue a poll +while a pass runs. + +--- + +### B259 — The sync engine can die or hang, and nothing shows it until a restart + +**Status:** Open +**Found:** 2026-10-31, issue #53: a newly added folder did not sync until the reporter restarted +the app. All FUSE workers were idle, nothing was queued, and the warning-level journal was +empty (B258). +**Where:** `spawn` and `engine_loop` in `crates/pdfs-fuse/src/sync/engine.rs`; +`reconcile_pass` in `crates/pdfs-fuse/src/sync.rs`; `crates/pdfs-fuse/src/diagnostics.rs` + +**Repro:** Not reproduced. Make one pass panic, or block in a call that does not return. Every +later add, `pdfs sync now`, file event and poll is dropped without a word. The folder that was +syncing stays `syncing` with its last progress. A restart runs a pass over every folder, which +is why it helps. + +**Cause.** +- The engine is one thread, and nothing catches a panic in a pass. When it unwinds, the + receiver is gone and every `let _ = sync_tx.send(..)` fails silently. The poll thread stops + at its first failed send. +- The thread's `JoinHandle` is checked only at shutdown. +- A pass has no overall deadline. Each request is bounded, but a pass is not; B165 dealt with + the stop only. +- Nothing logs a pass starting, or ending without changes. `Diagnostics` covers the FUSE + workers and the request queue, not this thread, so "workers idle, nothing queued" says + nothing about sync. +- A database error when the engine reads a folder drops that folder's request without a log. + +**Fix.** Not done. Catch a panic per pass, end its progress and set the folder to `error`. Have +the supervisor restart a finished engine thread. Log passes at info, and report the folder, +the pass age and the queue in `Diagnostics`. + +--- + +### B258 — `journalctl -p warning` shows nothing, whatever the daemon logged + +**Status:** Open +**Found:** 2026-10-31, issue #53: the reporter ran `journalctl --user -u proton-drive.service -b +-p warning`, as `docs/TROUBLESHOOTING.md` tells them to, and got `-- No entries --`. +**Where:** `main` in `crates/pdfs-cli/src/main.rs`; `packaging/proton-drive.service` + +**Repro:** Make the daemon log a warning, then run the command above. It prints no entries. +Without `-p` the warning is there. + +**Cause.** The daemon logs with plain `tracing_subscriber::fmt()` to standard output. journald +stores every line of a service's output at the unit's default priority, info, unless the line +starts with a `` prefix. So every `WARN` and `ERROR` line is stored as info, and the +troubleshooting guide's filter hides all of them. + +**Fix.** Not done. When `JOURNAL_STREAM` says the output goes to journald, start each line with +the `` prefix for its level. + +--- + ### B256 — `pdfs copy` right after a save copies the old version **Status:** Fixed, not verified live @@ -1523,6 +1894,8 @@ what it holds. What was synced in it is still deleted. ### B210 — After a password reset, the mount retries "missing key" forever **Status:** Fixed, not verified live +**See also:** B265. The account in issue #40 has an active volume, so this fix does not run for +it; its "missing key" comes from address keys that do not open. **Found:** 2026-10-15, issue #40: on a fresh 3.4.0 install the sign-in worked, but the daemon logged `mount failed: fetch My Files root: cryptography error: failed to decrypt: missing key` every 5 seconds. The app stayed on "Connecting…" because the daemon never got far enough to @@ -1668,6 +2041,7 @@ front-end is the stale one, and restarting the service would not help. ### B205 — A photo upload the SDK refuses says "You're offline" **Status:** Fixed, not verified live +**See also:** B266, the same misclassification on the pin, unpin, search and node-load paths. **Found:** 2026-10-11, issue #38: uploading an image from the Photos page showed "You're offline" while other uploads went through. Each `upload_photo` failed about 5 µs after it started, with no request sent, and the journal gave no reason. @@ -1752,6 +2126,7 @@ taken as this session's, so the daemon keeps writing its later rotations. ### B202 — A refused session shows as offline forever, and nothing says to sign in again **Status:** Fixed, not verified live +**See also:** B267. Other errors at a start from a warm cache still mount as offline. **Found:** 2026-10-09, issue #35: the tray and `pdfs diagnose` said "offline, 3 queued", and diagnose also said "keyring session: ok", while every call in the log failed with `InvalidRefreshToken (http 400)`. `pdfs sync now` and restarting the service changed nothing. @@ -2693,6 +3068,8 @@ fix). ### B165 — A stop waits on a stuck thread until systemd kills the daemon **Status:** Fixed (unverified). +**See also:** B259. A pass still has no deadline while the daemon runs, and a stuck or dead +sync engine is not reported. **Found:** 2026-10-04, by the account run on 2.10.0 (cleanup after on-demand pairing 2). The daemon was stopped at 15:53:04 and logged "control socket closing", but never "sync engine stopping" or "daemon stopping". systemd killed it 15 s later (`TimeoutStopSec`). The run's From 8d4a005aa21376efe9a821b727682f8cbd194fb6 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 31 Oct 2026 09:54:07 +0100 Subject: [PATCH 02/12] fix(cli): give journald each log line's priority journald stored every daemon line at info, so journalctl -p warning, which TROUBLESHOOTING.md asks reporters for, was always empty (B258). When fd 1 is the journal stream, prefix lines with and drop colours. --- crates/pdfs-cli/src/journal.rs | 122 +++++++++++++++++++++++++++++++++ crates/pdfs-cli/src/main.rs | 27 +++++--- docs/BUGS.md | 15 ++-- 3 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 crates/pdfs-cli/src/journal.rs diff --git a/crates/pdfs-cli/src/journal.rs b/crates/pdfs-cli/src/journal.rs new file mode 100644 index 0000000..a689a09 --- /dev/null +++ b/crates/pdfs-cli/src/journal.rs @@ -0,0 +1,122 @@ +//! Log priorities for journald. +//! +//! journald files each line a service writes at the unit's default priority, +//! info, unless the line opens with a syslog `` prefix. Without one every +//! WARN and ERROR the daemon logs is stored as info, and the +//! `journalctl -p warning` that the troubleshooting guide asks for shows +//! nothing (`docs/BUGS.md` B258). + +use std::fmt; +use std::os::unix::fs::MetadataExt; + +use tracing::{Event, Level, Subscriber}; +use tracing_subscriber::fmt::format::Writer; +use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields}; +use tracing_subscriber::registry::LookupSpan; + +/// Whether standard output is the journal stream systemd connected this +/// process to. +/// +/// `JOURNAL_STREAM` alone does not say so: a terminal started from a user unit +/// inherits it, and every `pdfs` run in that terminal would print the +/// prefixes. systemd sets it to the stream's `device:inode`, so it is compared +/// against what fd 1 actually is. +pub(crate) fn stdout_is_journal() -> bool { + let Some(stream) = std::env::var_os("JOURNAL_STREAM") else { + return false; + }; + let Ok(stdout) = std::fs::metadata("/proc/self/fd/1") else { + return false; + }; + stream + .to_str() + .is_some_and(|value| names_stream(value, stdout.dev(), stdout.ino())) +} + +/// Whether a `JOURNAL_STREAM` value names the file `dev`:`ino`. +fn names_stream(value: &str, dev: u64, ino: u64) -> bool { + value + .split_once(':') + .is_some_and(|(d, i)| d.parse() == Ok(dev) && i.parse() == Ok(ino)) +} + +/// The syslog priority journald should file a `level` line at. +fn priority(level: &Level) -> u8 { + match *level { + Level::ERROR => 3, + Level::WARN => 4, + Level::INFO => 6, + Level::DEBUG | Level::TRACE => 7, + } +} + +/// An event format that opens each line with its `` priority, then writes +/// the line as `F` would. +pub(crate) struct Prioritized(pub(crate) F); + +impl FormatEvent for Prioritized +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, + F: FormatEvent, +{ + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &Event<'_>, + ) -> fmt::Result { + write!(writer, "<{}>", priority(event.metadata().level()))?; + self.0.format_event(ctx, writer, event) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io; + use std::sync::{Arc, Mutex}; + + #[derive(Clone)] + struct Buf(Arc>>); + + impl io::Write for Buf { + fn write(&mut self, bytes: &[u8]) -> io::Result { + self.0.lock().unwrap().extend_from_slice(bytes); + Ok(bytes.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + #[test] + fn each_line_opens_with_the_priority_of_its_level() { + let buf = Buf(Arc::new(Mutex::new(Vec::new()))); + let writer = buf.clone(); + let subscriber = tracing_subscriber::fmt() + .with_max_level(Level::DEBUG) + .with_ansi(false) + .with_writer(move || writer.clone()) + .event_format(Prioritized(tracing_subscriber::fmt::format())) + .finish(); + tracing::subscriber::with_default(subscriber, || { + tracing::error!("e"); + tracing::warn!("w"); + tracing::info!("i"); + tracing::debug!("d"); + }); + let out = String::from_utf8(buf.0.lock().unwrap().clone()).unwrap(); + let prefixes: Vec<&str> = out.lines().map(|line| &line[..3]).collect(); + assert_eq!(prefixes, ["<3>", "<4>", "<6>", "<7>"]); + } + + #[test] + fn journal_stream_must_name_the_same_file() { + assert!(names_stream("8:12345", 8, 12345)); + assert!(!names_stream("8:12345", 8, 54321)); + assert!(!names_stream("9:12345", 8, 12345)); + assert!(!names_stream("8", 8, 12345)); + assert!(!names_stream("", 8, 12345)); + } +} diff --git a/crates/pdfs-cli/src/main.rs b/crates/pdfs-cli/src/main.rs index 75f6e3b..fa9b400 100644 --- a/crates/pdfs-cli/src/main.rs +++ b/crates/pdfs-cli/src/main.rs @@ -20,6 +20,8 @@ use pdfs_core::control::{ use pdfs_core::db::Db; use pdfs_core::service; +mod journal; + /// `println!` for this binary, shadowing std's, which panics when stdout is /// closed: `pdfs ls | head` ended in a "Broken pipe" panic once `head` had its /// lines. This one hands the failure to [`stdout_written`] instead. @@ -879,15 +881,22 @@ fn cli_error(kind: ErrorKind, message: &str) -> String { } fn main() -> Result<()> { - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - // `pgp` logs a benign WARN when it re-serializes a Proton secret-key - // packet with a longer (but equivalent) length encoding than the - // original; drop it to error so normal runs stay quiet. - .unwrap_or_else(|_| "info,pgp=error".into()), - ) - .init(); + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + // `pgp` logs a benign WARN when it re-serializes a Proton secret-key + // packet with a longer (but equivalent) length encoding than the + // original; drop it to error so normal runs stay quiet. + .unwrap_or_else(|_| "info,pgp=error".into()); + if journal::stdout_is_journal() { + // journalctl colours lines by the priority the prefix gives them, so + // the escape codes would only be noise in the stored text. + tracing_subscriber::fmt() + .with_env_filter(filter) + .with_ansi(false) + .event_format(journal::Prioritized(tracing_subscriber::fmt::format())) + .init(); + } else { + tracing_subscriber::fmt().with_env_filter(filter).init(); + } let cli = Cli::parse(); JSON_OUTPUT.store(cli.json, std::sync::atomic::Ordering::Relaxed); diff --git a/docs/BUGS.md b/docs/BUGS.md index 319b4ca..55b86bd 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -83,7 +83,6 @@ original wording. | [B261](#b261--a-folder-that-has-never-synced-reads-up-to-date) | A folder that has never synced reads "up to date" | Not fixed | | [B260](#b260--the-poll-pushes-a-new-folders-first-sync-behind-every-other-folder) | The poll pushes a new folder's first sync behind every other folder | Not fixed | | [B259](#b259--the-sync-engine-can-die-or-hang-and-nothing-shows-it-until-a-restart) | The sync engine can die or hang, and nothing shows it until a restart | Not fixed; the trigger in #53 is not known | -| [B258](#b258--journalctl--p-warning-shows-nothing-whatever-the-daemon-logged) | `journalctl -p warning` shows nothing, whatever the daemon logged | Not fixed | | [B134](#b134--a-file-deleted-online-while-the-drain-checks-its-write-is-kept-as-a-conflict-copy) | A file deleted online while the drain checks its write is kept as a conflict copy | Only with `local_first` off | | [B91](#b91--the-daemon-hung-overnight-and-nothing-could-say-what-it-was-waiting-on) | The daemon hung overnight, and nothing could say what it was waiting on | The cause is not diagnosed; the daemon now reports where it is stuck and is restarted | | [B67](#b67--fresh-state-restore-can-silently-select-a-new-device-high-22) | Fresh-state restore can silently select a new device (HIGH-22) | An ambiguous device after a loss of local state is not asked about | @@ -116,6 +115,7 @@ a live account since. | Entry | Title | |---|---| +| [B258](#b258--journalctl--p-warning-shows-nothing-whatever-the-daemon-logged) | `journalctl -p warning` shows nothing, whatever the daemon logged | | [B256](#b256--pdfs-copy-right-after-a-save-copies-the-old-version) | `pdfs copy` right after a save copies the old version | | [B255](#b255--a-synced-folder-whose-files-are-all-gone-here-is-stuck-for-good) | A synced folder whose files are all gone here is stuck for good | | [B254](#b254--an-edit-in-the-same-second-as-a-new-upload-never-arrives) | An edit in the same second as a new upload never arrives | @@ -712,7 +712,7 @@ the pass age and the queue in `Diagnostics`. ### B258 — `journalctl -p warning` shows nothing, whatever the daemon logged -**Status:** Open +**Status:** Fixed, not verified live **Found:** 2026-10-31, issue #53: the reporter ran `journalctl --user -u proton-drive.service -b -p warning`, as `docs/TROUBLESHOOTING.md` tells them to, and got `-- No entries --`. **Where:** `main` in `crates/pdfs-cli/src/main.rs`; `packaging/proton-drive.service` @@ -725,8 +725,15 @@ stores every line of a service's output at the unit's default priority, info, un starts with a `` prefix. So every `WARN` and `ERROR` line is stored as info, and the troubleshooting guide's filter hides all of them. -**Fix.** Not done. When `JOURNAL_STREAM` says the output goes to journald, start each line with -the `` prefix for its level. +**Fix.** When standard output is the journal stream (`JOURNAL_STREAM` names the same +device and inode as fd 1), each line starts with the `` prefix for its level, and ANSI colours +are off. A terminal that merely inherited `JOURNAL_STREAM` keeps the plain format +(`crates/pdfs-cli/src/journal.rs`). + +**Test:** `each_line_opens_with_the_priority_of_its_level` and +`journal_stream_must_name_the_same_file` in `pdfs-cli`. On 2026-10-31, `pdfs diagnose` run with +`RUST_LOG=debug` under `systemd-run --user -p StandardOutput=journal` stored its debug lines at +`PRIORITY=7` and its report at 6. --- From c64bb23e3c8e55a5a3a51dd29ac825f964c062fb Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 31 Oct 2026 10:14:23 +0100 Subject: [PATCH 03/12] fix(fuse,core,cli): keep a panicking sync pass from ending the engine One thread runs every synced folder's passes. A panic in a pass ended it, and every later add, sync-now, file event and poll went nowhere without a word until a restart, which fits issue #53 (B259). Each pass now runs under catch_unwind; a panic sets the folder to error and the next poll tries again. Diagnostics reports the engine's state, the supervisor warns when it has ended or a pass has gone quiet, and first passes log at info. --- crates/pdfs-cli/src/main.rs | 21 +++++ crates/pdfs-core/src/control.rs | 23 +++++ crates/pdfs-fuse/src/diagnostics.rs | 1 + crates/pdfs-fuse/src/lib.rs | 3 + crates/pdfs-fuse/src/mount.rs | 1 + crates/pdfs-fuse/src/supervisor.rs | 68 +++++++++++++- crates/pdfs-fuse/src/sync.rs | 73 ++++++++++++++- crates/pdfs-fuse/src/sync/engine.rs | 132 ++++++++++++++++++++++++++-- docs/BUGS.md | 26 ++++-- 9 files changed, 334 insertions(+), 14 deletions(-) diff --git a/crates/pdfs-cli/src/main.rs b/crates/pdfs-cli/src/main.rs index 75f6e3b..b0b7e49 100644 --- a/crates/pdfs-cli/src/main.rs +++ b/crates/pdfs-cli/src/main.rs @@ -3096,6 +3096,24 @@ fn human_secs(secs: u64) -> String { } } +/// The sync engine's line in `pdfs diagnostics`: whether it runs, and which +/// folder's pass it is on. +fn sync_engine_line(sync: &pdfs_core::control::SyncEngineState) -> String { + let mut line = match (sync.running, sync.pass_folder) { + (false, _) => "STOPPED; synced folders do not sync until the daemon restarts".to_string(), + (true, None) => "idle".to_string(), + (true, Some(folder)) => format!( + "pass on folder {folder} for {}, last progress {} ago", + human_secs(sync.pass_age_secs), + human_secs(sync.pass_idle_secs), + ), + }; + if sync.panics > 0 { + line.push_str(&format!("; {} pass(es) panicked", sync.panics)); + } + line +} + fn cmd_diagnostics() -> Result<()> { let response = control_request(CtlRequest::Diagnostics)?; if emit_json(&response)? { @@ -3131,6 +3149,9 @@ fn cmd_diagnostics() -> Result<()> { human_secs(worker.age_secs) ); } + if let Some(sync) = &d.sync { + println!("sync engine: {}", sync_engine_line(sync)); + } if d.inflight.is_empty() { println!("in-flight requests: none"); } else { diff --git a/crates/pdfs-core/src/control.rs b/crates/pdfs-core/src/control.rs index c111146..3b7c590 100644 --- a/crates/pdfs-core/src/control.rs +++ b/crates/pdfs-core/src/control.rs @@ -1980,6 +1980,29 @@ pub struct Diagnostics { pub control_handler_limit: usize, /// Queued mutations not yet drained. pub pending_ops: u64, + /// The folder-sync engine. `None` from a daemon that predates the field. + #[serde(default)] + pub sync: Option, +} + +/// What the folder-sync engine is doing, for [`Diagnostics`]. +/// +/// The engine is one thread that runs every synced folder's passes in turn. +/// Dead or stuck, it looked exactly like an idle one: workers idle, nothing +/// queued, nothing in the journal (`docs/BUGS.md` B259). +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct SyncEngineState { + /// Whether the engine thread is running. Once it has ended, nothing syncs + /// until the daemon restarts. + pub running: bool, + /// The synced folder whose pass is running, if one is. + pub pass_folder: Option, + /// How long that pass has been running. + pub pass_age_secs: u64, + /// How long since that pass last reported progress. + pub pass_idle_secs: u64, + /// Passes that ended in a panic since the daemon started. + pub panics: u64, } /// The daemon's reply to a [`Request`]. diff --git a/crates/pdfs-fuse/src/diagnostics.rs b/crates/pdfs-fuse/src/diagnostics.rs index 5cde5df..f775043 100644 --- a/crates/pdfs-fuse/src/diagnostics.rs +++ b/crates/pdfs-fuse/src/diagnostics.rs @@ -163,6 +163,7 @@ impl Core { control_handlers: super::control::active_handlers(), control_handler_limit: super::control::handler_limit(), pending_ops: self.pending_ops_quickly(), + sync: Some(self.sync_engine.report()), } } diff --git a/crates/pdfs-fuse/src/lib.rs b/crates/pdfs-fuse/src/lib.rs index d9bbb0e..bb7f004 100644 --- a/crates/pdfs-fuse/src/lib.rs +++ b/crates/pdfs-fuse/src/lib.rs @@ -728,6 +728,8 @@ struct Core { /// can say what a pass is doing rather than just "syncing". An entry exists /// only while that folder's reconcile pass is running. sync_progress: Arc>>, + /// Whether the folder-sync engine is alive, and which pass it is on. + sync_engine: Arc, /// Channel to the folder-sync engine: nudges it to /// reconcile a folder, reconcile everything, or re-scan its watch set. sync_tx: std::sync::mpsc::Sender, @@ -5989,6 +5991,7 @@ impl Core { if let Some(p) = self.sync_progress.lock().get_mut(&folder_id) { f(p); } + self.sync_engine.moved(); } /// Set the number of items the scan expects to check, from the size of the diff --git a/crates/pdfs-fuse/src/mount.rs b/crates/pdfs-fuse/src/mount.rs index 1aeccdc..f60a1e3 100644 --- a/crates/pdfs-fuse/src/mount.rs +++ b/crates/pdfs-fuse/src/mount.rs @@ -507,6 +507,7 @@ pub(crate) fn mount_with( events: Arc::new(Default::default()), indexing: Arc::new(AtomicBool::new(false)), sync_progress: Arc::new(Mutex::new(HashMap::new())), + sync_engine: Arc::default(), sync_tx, mounts: Arc::new(Mutex::new(SecondaryMountRegistry::default())), sync_locks: Arc::new(Mutex::new(HashMap::new())), diff --git a/crates/pdfs-fuse/src/supervisor.rs b/crates/pdfs-fuse/src/supervisor.rs index c7cfdf3..6e54f87 100644 --- a/crates/pdfs-fuse/src/supervisor.rs +++ b/crates/pdfs-fuse/src/supervisor.rs @@ -36,6 +36,11 @@ const TICK: Duration = Duration::from_secs(15); /// would train everyone to ignore the real one. const STALL_AFTER: Duration = Duration::from_secs(120); +/// How long a sync pass may go without reporting progress before it is called +/// out. Much longer than [`STALL_AFTER`]: one large file in a pass reports +/// nothing until its whole transfer is done. +const SYNC_STALL_AFTER: Duration = Duration::from_secs(900); + /// How often resident size goes into the log. const RSS_EVERY: Duration = Duration::from_secs(300); @@ -137,6 +142,26 @@ fn report_stalls(report: &pdfs_core::control::Diagnostics, last_completed: (u64, "the transfer lane has not completed a job since the last check" ); } + // The engine is one thread for every synced folder, so a dead or stuck one + // stops all of them while the worker pool looks idle (B259). + if let Some(sync) = &report.sync { + if !sync.running { + stalled = true; + warn!( + "the sync engine is not running; synced folders do not sync until the daemon restarts" + ); + } else if let Some(folder) = sync.pass_folder + && sync.pass_idle_secs >= SYNC_STALL_AFTER.as_secs() + { + stalled = true; + warn!( + folder, + age_secs = sync.pass_age_secs, + idle_secs = sync.pass_idle_secs, + "a sync pass has not reported progress for a long time" + ); + } + } stalled } @@ -169,7 +194,7 @@ fn probe(control_socket: &Path) -> bool { #[cfg(test)] mod tests { use super::*; - use pdfs_core::control::{Diagnostics, InflightRequest, WorkerState}; + use pdfs_core::control::{Diagnostics, InflightRequest, SyncEngineState, WorkerState}; fn busy_worker(age_secs: u64) -> WorkerState { WorkerState { @@ -226,4 +251,45 @@ mod tests { assert!(!report_stalls(&report, (0, 30))); assert!(report_stalls(&report, (0, 40))); } + + fn with_sync(sync: SyncEngineState) -> Diagnostics { + Diagnostics { + sync: Some(sync), + ..Default::default() + } + } + + /// A sync engine that has ended stops every synced folder while the + /// workers look idle, which is what issue #53 looked like (B259). + #[test] + fn a_stopped_sync_engine_is_a_stall() { + assert!(report_stalls( + &with_sync(SyncEngineState::default()), + (0, 0) + )); + } + + /// A pass that keeps reporting progress is working, however long it runs; + /// one that has gone quiet past the threshold is not. + #[test] + fn only_a_quiet_sync_pass_is_a_stall() { + let busy = SyncEngineState { + running: true, + pass_folder: Some(3), + pass_age_secs: SYNC_STALL_AFTER.as_secs() * 4, + pass_idle_secs: 5, + panics: 0, + }; + assert!(!report_stalls(&with_sync(busy.clone()), (0, 0))); + let quiet = SyncEngineState { + pass_idle_secs: SYNC_STALL_AFTER.as_secs(), + ..busy + }; + assert!(report_stalls(&with_sync(quiet), (0, 0))); + let idle = SyncEngineState { + running: true, + ..Default::default() + }; + assert!(!report_stalls(&with_sync(idle), (0, 0))); + } } diff --git a/crates/pdfs-fuse/src/sync.rs b/crates/pdfs-fuse/src/sync.rs index 864e522..c698021 100644 --- a/crates/pdfs-fuse/src/sync.rs +++ b/crates/pdfs-fuse/src/sync.rs @@ -48,8 +48,10 @@ mod engine; mod local_scan; mod model; mod planner; +#[cfg(test)] +use engine::Running; pub(crate) use engine::SYNC_CONCURRENCY; -pub(crate) use engine::{SyncMsg, spawn}; +pub(crate) use engine::{EngineHealth, SyncMsg, spawn}; #[cfg(test)] use engine::{classify, event_target, settle_with}; #[cfg(test)] @@ -67,6 +69,11 @@ use planner::{ plan_file, rel_to_path, remote_sig, remote_subtree_changed, revision_moved, tracked_baseline, unchanged_remote_size, }; + +/// A pass that took at least this long is logged at info even when it changed +/// nothing, so the journal says where a slow engine spends its time. +const SLOW_PASS: Duration = Duration::from_secs(30); + /// What a [`Pending`] op did, so the reconcile can update shared state on the /// engine thread (never inside a task). enum Applied { @@ -154,6 +161,14 @@ impl Core { // The cost is one small file read against a pass that is about to walk the // whole tree. let rules = self.ignore_rules(&local_root); + // A first pass is the one a user waits on after adding or restoring a + // folder, so it is said at info; the poll's passes stay at debug. + if folder.last_sync == 0 { + info!(id = folder.id, path = %folder.local_path, "sync: first pass starting"); + } else { + debug!(id = folder.id, "sync: pass starting"); + } + let started = Instant::now(); self.progress_begin(folder.id); let result = if push_only { self.push_pass(folder.id, &local_root, &remote_root, &rules) @@ -195,6 +210,18 @@ impl Core { "idle" }; let _ = self.db.sync_folder_set_state(folder.id, state, now_secs()); + let elapsed = started.elapsed(); + if folder.last_sync == 0 || !outcome.is_empty() || elapsed >= SLOW_PASS { + info!( + id = folder.id, + state, + elapsed_ms = elapsed.as_millis() as u64, + summary = %outcome.summary(), + "sync: pass finished" + ); + } else { + debug!(id = folder.id, state, "sync: pass finished"); + } self.report_unscanned(folder.id, name, &outcome.unscanned); // Summarise a pass that actually moved something. A poll over an // unchanged folder does nothing and says nothing — otherwise the @@ -2438,6 +2465,50 @@ fn system_mtime_ns(meta: &std::fs::Metadata) -> Option { mod tests { use super::*; + /// A panic in a pass ends that pass and is counted; the engine is not + /// left believing the pass still runs (`docs/BUGS.md` B259). + #[test] + fn a_panicking_pass_is_contained_and_counted() { + let health = EngineHealth::default(); + assert!(!health.contain(7, || panic!("a pass that panics"))); + let report = health.report(); + assert_eq!(report.panics, 1); + assert_eq!(report.pass_folder, None); + assert!(health.contain(7, || {})); + assert_eq!(health.report().panics, 1); + } + + /// While a pass runs, the report names its folder, which is what tells a + /// long pass apart from an idle engine. + #[test] + fn the_running_pass_is_reported() { + let health = EngineHealth::default(); + let mut seen = None; + health.contain(7, || seen = health.report().pass_folder); + assert_eq!(seen, Some(7)); + } + + /// The engine reads as running only while its thread lives, including + /// when the thread ends in a panic. + #[test] + fn the_engine_reads_as_stopped_once_its_thread_ends() { + let health = Arc::new(EngineHealth::default()); + let thread_health = health.clone(); + let (started_tx, started_rx) = std::sync::mpsc::channel(); + let (end_tx, end_rx) = std::sync::mpsc::channel::<()>(); + let thread = std::thread::spawn(move || { + let _running = Running::start(&thread_health); + started_tx.send(()).unwrap(); + let _ = end_rx.recv(); + panic!("the engine thread panics"); + }); + started_rx.recv().unwrap(); + assert!(health.report().running); + end_tx.send(()).unwrap(); + assert!(thread.join().is_err()); + assert!(!health.report().running); + } + /// A removal of something already gone has produced the state the sync /// engine wanted, so its baseline is dropped rather than retried forever. /// Anything else stays an error and keeps the baseline (`docs/BUGS.md` B42). diff --git a/crates/pdfs-fuse/src/sync/engine.rs b/crates/pdfs-fuse/src/sync/engine.rs index 5664392..7020cb4 100644 --- a/crates/pdfs-fuse/src/sync/engine.rs +++ b/crates/pdfs-fuse/src/sync/engine.rs @@ -1,5 +1,11 @@ //! Filesystem watcher, polling, and debounce control plane. +use std::panic::AssertUnwindSafe; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + +use pdfs_core::control::SyncEngineState; +use tracing::error; + use super::*; /// How long the watcher must go quiet before a burst counts as finished. @@ -53,8 +59,124 @@ pub(crate) fn spawn(core: Core, rx: Receiver) -> Option>, + panics: AtomicU64, +} + +#[derive(Clone, Copy)] +struct PassClock { + folder: i64, + started: Instant, + moved: Instant, +} + +impl EngineHealth { + /// Note that the running pass reported progress. + pub(crate) fn moved(&self) { + if let Some(pass) = self.pass.lock().as_mut() { + pass.moved = Instant::now(); + } + } + + /// The engine's state now. `try_lock`, like the rest of the diagnostics: + /// a report must not wait on the thing it reports about. + pub(crate) fn report(&self) -> SyncEngineState { + let pass = self.pass.try_lock().and_then(|pass| *pass); + SyncEngineState { + running: self.running.load(Ordering::Relaxed), + pass_folder: pass.map(|p| p.folder), + pass_age_secs: pass.map_or(0, |p| p.started.elapsed().as_secs()), + pass_idle_secs: pass.map_or(0, |p| p.moved.elapsed().as_secs()), + panics: self.panics.load(Ordering::Relaxed), + } + } + + /// Run `pass` over `folder` as the current pass. Returns `false` when it + /// panicked: the panic ends that pass, not the engine. + pub(super) fn contain(&self, folder: i64, pass: impl FnOnce()) -> bool { + let now = Instant::now(); + *self.pass.lock() = Some(PassClock { + folder, + started: now, + moved: now, + }); + let finished = std::panic::catch_unwind(AssertUnwindSafe(pass)).is_ok(); + *self.pass.lock() = None; + if !finished { + self.panics.fetch_add(1, Ordering::Relaxed); + } + finished + } +} + +/// Marks the engine running for as long as it lives. +pub(super) struct Running<'a>(&'a EngineHealth); + +impl<'a> Running<'a> { + pub(super) fn start(health: &'a EngineHealth) -> Self { + health.running.store(true, Ordering::Relaxed); + Self(health) + } +} + +impl Drop for Running<'_> { + fn drop(&mut self) { + self.0.running.store(false, Ordering::Relaxed); + if std::thread::panicking() { + error!("sync engine stopped by a panic; nothing syncs until the daemon restarts"); + } + } +} + +/// One folder's pass, contained: a panic in it sets the folder to `error` and +/// leaves the engine running for the next one. +fn run_pass(core: &Core, folder: &StoredSyncFolder) { + if core + .sync_engine + .contain(folder.id, || core.reconcile_folder(folder)) + { + return; + } + core.progress_end(folder.id); + let _ = core + .db + .sync_folder_set_state(folder.id, "error", folder.last_sync); + error!( + id = folder.id, + path = %folder.local_path, + "sync: pass panicked; the folder is set to error and tried again on the next poll" + ); + core.log_activity( + ActivityKind::Sync, + format!("sync failed for {}", base_name(&folder.local_path)), + "an internal error stopped the pass; it is tried again on the next poll", + false, + ); +} + +/// The pass a `Reconcile(id)` asked for. A folder removed since, or one the +/// database cannot read, is said so rather than dropped in silence. +fn run_pass_by_id(core: &Core, id: i64) { + match core.db.sync_folder_get(id) { + Ok(Some(folder)) => run_pass(core, &folder), + Ok(None) => debug!(id, "sync: folder removed before its pass; skipping"), + Err(e) => warn!(id, error = ?e, "sync: cannot read folder; skipping its pass"), + } +} + /// The engine control loop. Single-threaded so reconcile passes never overlap. fn engine_loop(core: Core, rx: Receiver) { + let health = core.sync_engine.clone(); + let _running = Running::start(&health); // Paths the watcher currently covers, mapped to their folder id, so an event // path can be resolved back to the folder it belongs to. let watched: Arc>> = Arc::new(Mutex::new(Vec::new())); @@ -120,9 +242,7 @@ fn engine_loop(core: Core, rx: Receiver) { if core.sync_paused() { break; } - if let Ok(Some(folder)) = core.db.sync_folder_get(id) { - core.reconcile_folder(&folder); - } + run_pass_by_id(&core, id); } } } @@ -159,9 +279,7 @@ fn poll_only_loop(core: &Core, rx: Receiver) { if core.sync_paused() { break; } - if let Ok(Some(folder)) = core.db.sync_folder_get(id) { - core.reconcile_folder(&folder); - } + run_pass_by_id(core, id); } } } @@ -254,7 +372,7 @@ fn reconcile_all(core: &Core) { } }; for folder in folders { - core.reconcile_folder(&folder); + run_pass(core, &folder); } } diff --git a/docs/BUGS.md b/docs/BUGS.md index 319b4ca..2103de3 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -82,7 +82,6 @@ original wording. | [B262](#b262--watching-a-large-synced-folder-holds-up-the-sync-engine-and-a-failed-watch-is-never-retried) | Watching a large synced folder holds up the sync engine, and a failed watch is never retried | Not fixed | | [B261](#b261--a-folder-that-has-never-synced-reads-up-to-date) | A folder that has never synced reads "up to date" | Not fixed | | [B260](#b260--the-poll-pushes-a-new-folders-first-sync-behind-every-other-folder) | The poll pushes a new folder's first sync behind every other folder | Not fixed | -| [B259](#b259--the-sync-engine-can-die-or-hang-and-nothing-shows-it-until-a-restart) | The sync engine can die or hang, and nothing shows it until a restart | Not fixed; the trigger in #53 is not known | | [B258](#b258--journalctl--p-warning-shows-nothing-whatever-the-daemon-logged) | `journalctl -p warning` shows nothing, whatever the daemon logged | Not fixed | | [B134](#b134--a-file-deleted-online-while-the-drain-checks-its-write-is-kept-as-a-conflict-copy) | A file deleted online while the drain checks its write is kept as a conflict copy | Only with `local_first` off | | [B91](#b91--the-daemon-hung-overnight-and-nothing-could-say-what-it-was-waiting-on) | The daemon hung overnight, and nothing could say what it was waiting on | The cause is not diagnosed; the daemon now reports where it is stuck and is restarted | @@ -96,6 +95,7 @@ original wording. | Entry | Title | What is left | |---|---|---| +| [B259](#b259--the-sync-engine-can-die-or-hang-and-nothing-shows-it-until-a-restart) | The sync engine can die or hang, and nothing shows it until a restart | No pass deadline; an ended engine is reported, not restarted; the trigger in #53 is not known | | [B199](#b199--a-stopped-share-stays-on-the-shared-by-me-page-for-minutes) | A stopped share stays on the Shared by me page for minutes | The share itself is not deleted; the SDK has no call for it | | [B88](#b88--pdfs-pin-rename-move-delete-and-sharing-still-cannot-name-a-path-under-a-secondary-mount) | `pdfs pin`, `rename`, `move`, `delete` and sharing still cannot name a path under a secondary mount | `pin`, `unpin`, opening a file, thumbnails, uploads, sharing and versions | | [B72](#b72--pdfs-trash-never-returns) | `pdfs trash` never returns | The slowness of the first refresh is unmeasured | @@ -680,7 +680,8 @@ while a pass runs. ### B259 — The sync engine can die or hang, and nothing shows it until a restart -**Status:** Open +**Status:** Partly fixed. A hung pass is reported but not ended, and a panic outside a pass +still stops the engine; it is reported, not restarted. **Found:** 2026-10-31, issue #53: a newly added folder did not sync until the reporter restarted the app. All FUSE workers were idle, nothing was queued, and the warning-level journal was empty (B258). @@ -704,9 +705,24 @@ is why it helps. nothing about sync. - A database error when the engine reads a folder drops that folder's request without a log. -**Fix.** Not done. Catch a panic per pass, end its progress and set the folder to `error`. Have -the supervisor restart a finished engine thread. Log passes at info, and report the folder, -the pass age and the queue in `Diagnostics`. +**Fix.** +- Each pass runs under `catch_unwind` (`EngineHealth::contain` in + `crates/pdfs-fuse/src/sync/engine.rs`). A panic ends that pass only: its progress is cleared, + the folder is set to `error`, the activity feed says so, and the next poll tries again. +- `Diagnostics` carries the engine's state: running or not, the folder of the running pass, how + long it has run and how long since it last reported progress, and how many passes panicked. + `pdfs diagnostics` prints it as `sync engine: …`. +- The supervisor warns when the engine thread has ended, and when a pass has reported no + progress for 15 minutes. +- A first pass logs its start and end at info, as does any pass that changed something or + took 30 s or more. A request for a folder the database cannot read is logged. +- Not done: a deadline for a pass, and restarting an engine thread that has ended. The + channel's receiver ends with the thread, so a restart needs a new channel in every `Core` + clone. + +**Test:** `a_panicking_pass_is_contained_and_counted`, `the_running_pass_is_reported` and +`the_engine_reads_as_stopped_once_its_thread_ends` in `pdfs-fuse`; +`a_stopped_sync_engine_is_a_stall` and `only_a_quiet_sync_pass_is_a_stall` in the supervisor. --- From b40ca797d9900cf2999199c6a62bbb17c3ca2cf4 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 31 Oct 2026 10:24:32 +0100 Subject: [PATCH 04/12] fix(fuse): run a burst's named folders first and retry failed watches A poll in the same burst as a new folder's Reconcile(id) took its place, so the new folder waited behind a pass over every other one (B260). A recursive watch that failed partway left its watches registered, was never retried, and its events ran passes over every folder (B262). It is now unwatched and retried every 10 minutes, symlinks are not followed, and watcher errors are logged. A folder removed and added again in one burst keeps its watch under the new id (B272). --- crates/pdfs-fuse/src/sync.rs | 26 +++- crates/pdfs-fuse/src/sync/engine.rs | 199 ++++++++++++++++++---------- docs/BUGS.md | 38 ++++-- 3 files changed, 181 insertions(+), 82 deletions(-) diff --git a/crates/pdfs-fuse/src/sync.rs b/crates/pdfs-fuse/src/sync.rs index c698021..a5c5f09 100644 --- a/crates/pdfs-fuse/src/sync.rs +++ b/crates/pdfs-fuse/src/sync.rs @@ -48,11 +48,11 @@ mod engine; mod local_scan; mod model; mod planner; -#[cfg(test)] -use engine::Running; pub(crate) use engine::SYNC_CONCURRENCY; pub(crate) use engine::{EngineHealth, SyncMsg, spawn}; #[cfg(test)] +use engine::{Running, burst_order, keep_wanted}; +#[cfg(test)] use engine::{classify, event_target, settle_with}; #[cfg(test)] use local_scan::is_write_mode; @@ -2478,6 +2478,28 @@ mod tests { assert_eq!(health.report().panics, 1); } + /// Folders named by id run before the rest of a `ReconcileAll` in the + /// same burst, so a folder just added does not wait behind a pass over + /// every other folder (`docs/BUGS.md` B260). + #[test] + fn named_folders_run_before_the_rest_of_a_burst() { + let ids: HashSet = [9, 4].into(); + assert_eq!(burst_order(&ids, &[1, 4, 6, 9]), [4, 9, 1, 6]); + assert_eq!(burst_order(&HashSet::new(), &[1, 4]), [1, 4]); + // A folder the list no longer has still gets the pass it asked for. + assert_eq!(burst_order(&[12].into(), &[1]), [12, 1]); + } + + /// A folder removed and added again keeps its watch under the new id; + /// a folder that is gone loses its watch (`docs/BUGS.md` B272). + #[test] + fn a_re_added_folder_keeps_its_watch_under_the_new_id() { + let mut have = vec![(PathBuf::from("/a"), 1), (PathBuf::from("/b"), 2)]; + let dropped = keep_wanted(&mut have, &[(PathBuf::from("/a"), 7)]); + assert_eq!(have, [(PathBuf::from("/a"), 7)]); + assert_eq!(dropped, [PathBuf::from("/b")]); + } + /// While a pass runs, the report names its folder, which is what tells a /// long pass apart from an idle engine. #[test] diff --git a/crates/pdfs-fuse/src/sync/engine.rs b/crates/pdfs-fuse/src/sync/engine.rs index 7020cb4..8d6fb00 100644 --- a/crates/pdfs-fuse/src/sync/engine.rs +++ b/crates/pdfs-fuse/src/sync/engine.rs @@ -29,6 +29,9 @@ const POLL_INTERVAL: Duration = Duration::from_secs(120); /// unusable done one-at-a-time; this bounds concurrency like the bulk-upload /// engine does. pub(crate) const SYNC_CONCURRENCY: usize = 8; +/// How long after a failed watch the engine tries the watches again. Each try +/// walks the folder's whole tree, so not on every burst. +const REWATCH_RETRY: Duration = Duration::from_secs(600); /// A message to the sync engine's control thread. pub(crate) enum SyncMsg { @@ -183,24 +186,35 @@ fn engine_loop(core: Core, rx: Receiver) { let tx_events = core.sync_tx.clone(); let watched_events = watched.clone(); - let mut watcher = - match notify::recommended_watcher(move |res: notify::Result| { - let Ok(event) = res else { return }; - // Ignore pure metadata/access noise; content changes are what matter. - if matches!(event.kind, notify::EventKind::Access(_)) { - return; - } - let msg = event_target(&watched_events.lock(), &event.paths); - let _ = tx_events.send(msg); - }) { - Ok(w) => w, + let handler = move |res: notify::Result| { + let event = match res { + Ok(event) => event, + // Running out of inotify watches for a new subfolder, say: that + // subfolder's changes are then found by the poll only (B262). Err(e) => { - warn!(error = %e, "filesystem watcher unavailable; sync is poll-only"); - // Still run the loop so polling and explicit SyncNow work. - poll_only_loop(&core, rx); + warn!(error = %e, "sync: filesystem watcher error"); return; } }; + // Ignore pure metadata/access noise; content changes are what matter. + if matches!(event.kind, notify::EventKind::Access(_)) { + return; + } + let msg = event_target(&watched_events.lock(), &event.paths); + let _ = tx_events.send(msg); + }; + // The local walk skips symlinks, so following them here would only watch + // trees outside the folder, and run passes over it for changes made there. + let config = notify::Config::default().with_follow_symlinks(false); + let mut watcher = match notify::RecommendedWatcher::new(handler, config) { + Ok(w) => w, + Err(e) => { + warn!(error = %e, "filesystem watcher unavailable; sync is poll-only"); + // Still run the loop so polling and explicit SyncNow work. + poll_only_loop(&core, rx); + return; + } + }; // Periodic remote poll. { @@ -218,8 +232,9 @@ fn engine_loop(core: Core, rx: Receiver) { .ok(); } - rewatch(&core, &mut watcher, &watched); - reconcile_all(&core); + let mut unwatched = rewatch(&core, &mut watcher, &watched); + let mut last_rewatch = Instant::now(); + run_burst(&core, &HashSet::new(), true); while let Ok(msg) = rx.recv() { let mut ids: HashSet = HashSet::new(); @@ -232,19 +247,16 @@ fn engine_loop(core: Core, rx: Receiver) { return; } + // A folder whose watch failed is tried again now and then, not only + // when the folder set changes. + if unwatched && last_rewatch.elapsed() >= REWATCH_RETRY { + do_rewatch = true; + } if do_rewatch { - rewatch(&core, &mut watcher, &watched); - } - if all { - reconcile_all(&core); - } else { - for id in ids { - if core.sync_paused() { - break; - } - run_pass_by_id(&core, id); - } + unwatched = rewatch(&core, &mut watcher, &watched); + last_rewatch = Instant::now(); } + run_burst(&core, &ids, all); } } @@ -261,7 +273,7 @@ fn poll_only_loop(core: &Core, rx: Receiver) { } }); } - reconcile_all(core); + run_burst(core, &HashSet::new(), true); while let Ok(msg) = rx.recv() { let mut ids: HashSet = HashSet::new(); let mut all = false; @@ -272,19 +284,59 @@ fn poll_only_loop(core: &Core, rx: Receiver) { info!("sync engine stopping"); return; } - if all { - reconcile_all(core); - } else { - for id in ids { - if core.sync_paused() { - break; - } - run_pass_by_id(core, id); + run_burst(core, &ids, all); + } +} + +/// Run the passes one burst asked for: the folders named by id, then, when the +/// burst also holds a `ReconcileAll`, every other folder. +/// +/// The named folders go first. A poll's `ReconcileAll` in the same burst used +/// to take their place, and a folder just added, which has the highest id, +/// waited behind a full pass over every other folder (`docs/BUGS.md` B260). +/// +/// Skipped while sync is paused, and cut short when a pause lands between +/// passes: the resume sends a fresh [`SyncMsg::ReconcileAll`], which walks +/// everything the watcher reported meanwhile. +fn run_burst(core: &Core, ids: &HashSet, all: bool) { + if core.sync_paused() { + debug!("sync: paused, skipping reconcile"); + return; + } + let folders = if all { + match core.db.sync_folder_list() { + Ok(folders) => folders, + Err(e) => { + warn!(error = ?e, "sync: cannot list folders"); + Vec::new() } } + } else { + Vec::new() + }; + let listed: Vec = folders.iter().map(|f| f.id).collect(); + for id in burst_order(ids, &listed) { + if core.sync_paused() { + break; + } + // A folder named by id is read again: it may have been added or + // changed after the list was read. + match folders.iter().find(|f| f.id == id) { + Some(folder) if !ids.contains(&id) => run_pass(core, folder), + _ => run_pass_by_id(core, id), + } } } +/// The order of a burst's passes: the folders named by id, lowest first, then +/// the rest of `listed` in its own order. +pub(super) fn burst_order(ids: &HashSet, listed: &[i64]) -> Vec { + let mut order: Vec = ids.iter().copied().collect(); + order.sort_unstable(); + order.extend(listed.iter().copied().filter(|id| !ids.contains(id))); + order +} + /// Absorb the rest of an event burst, returning once the watcher has been quiet /// for `quiet` or `cap` has elapsed since the first event — whichever comes /// first. The caller has already classified the event that opened the burst. @@ -356,26 +408,6 @@ pub(super) fn classify( true } -/// Reconcile every mirror folder in turn. Skipped while sync is paused: the -/// resume sends a fresh [`SyncMsg::ReconcileAll`], which walks everything the -/// watcher reported meanwhile. -fn reconcile_all(core: &Core) { - if core.sync_paused() { - debug!("sync: paused, skipping reconcile"); - return; - } - let folders = match core.db.sync_folder_list() { - Ok(f) => f, - Err(e) => { - warn!(error = ?e, "sync: cannot list folders"); - return; - } - }; - for folder in folders { - run_pass(core, &folder); - } -} - /// The reconcile a watcher event asks for: the one mirror folder all its paths /// lie in, so an edit in one folder does not walk every other one too. An event /// that spans folders, or that no watched folder claims (a watch list a moment @@ -396,7 +428,8 @@ pub(super) fn event_target(watched: &[(PathBuf, i64)], paths: &[PathBuf]) -> Syn } /// Bring the filesystem watches in line with the current mirror-folder set: -/// watch newly-added folders, drop removed ones. +/// watch newly-added folders, drop removed ones. Returns whether a folder is +/// left unwatched because its watch failed. /// /// The watch calls run on a copy, outside the lock: the watcher's event /// callback takes the same lock, and a backend that hands a watch request to @@ -405,7 +438,7 @@ fn rewatch( core: &Core, watcher: &mut notify::RecommendedWatcher, watched: &Mutex>, -) { +) -> bool { let want: Vec<(PathBuf, i64)> = match core.db.sync_folder_list() { Ok(f) => f .into_iter() @@ -414,30 +447,60 @@ fn rewatch( .collect(), Err(e) => { warn!(error = ?e, "sync: cannot list folders to watch"); - return; + return false; } }; let mut have = watched.lock().clone(); // Drop watches no longer wanted. - have.retain(|(path, _)| { - if want.iter().any(|(p, _)| p == path) { - true - } else { - let _ = watcher.unwatch(path); - false - } - }); + for path in keep_wanted(&mut have, &want) { + let _ = watcher.unwatch(&path); + } // Add newly wanted watches. + let mut failed = false; for (path, id) in &want { if have.iter().any(|(p, _)| p == path) { continue; } match watcher.watch(path, RecursiveMode::Recursive) { Ok(()) => have.push((path.clone(), *id)), - Err(e) => warn!(path = %path.display(), error = %e, "sync: watch failed"), + Err(e) => { + // A recursive watch that fails partway keeps the watches it had + // added. Their events would match no folder and run a pass over + // every one, and they hold watches the next folder needs (B262). + let _ = watcher.unwatch(path); + failed = true; + warn!( + path = %path.display(), + error = %e, + "sync: watch failed; the poll finds this folder's changes until a retry \ + works. A low fs.inotify.max_user_watches is the usual cause" + ); + } } } *watched.lock() = have; + failed +} + +/// Keep the watches in `have` that `want` still asks for, and return the paths +/// of the others, to unwatch. +/// +/// A path is matched alone, but takes the id `want` gives it: a folder removed +/// and added again in one burst keeps its watch, and its events go to the new +/// row rather than to the removed one (`docs/BUGS.md` B272). +pub(super) fn keep_wanted(have: &mut Vec<(PathBuf, i64)>, want: &[(PathBuf, i64)]) -> Vec { + let mut dropped = Vec::new(); + have.retain_mut(|(path, id)| match want.iter().find(|(p, _)| p == path) { + Some((_, wanted)) => { + *id = *wanted; + true + } + None => { + dropped.push(path.clone()); + false + } + }); + dropped } // ---- reconcile ------------------------------------------------------------ diff --git a/docs/BUGS.md b/docs/BUGS.md index 2103de3..9814d4e 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -69,7 +69,6 @@ original wording. | Entry | Title | What is left | |---|---|---| | [B273](#b273--a-restore-proposes-another-computers-paths-when-the-two-share-a-hostname) | A restore proposes another computer's paths when the two share a hostname | Not fixed | -| [B272](#b272--after-a-folder-is-removed-and-added-again-its-file-events-go-to-the-old-id) | After a folder is removed and added again, its file events go to the old id | Not fixed | | [B271](#b271--one-failed-listing-makes-a-large-first-sync-start-over-from-nothing) | One failed listing makes a large first sync start over from nothing | Not fixed | | [B270](#b270--one-file-left-in-a-mirror-folder-clears-the-wipe-guard-for-all-the-others) | One file left in a mirror folder clears the wipe guard for all the others | Not fixed | | [B269](#b269--a-restore-that-skipped-every-folder-reports-success) | A restore that skipped every folder reports success | Not fixed | @@ -79,9 +78,7 @@ original wording. | [B265](#b265--address-keys-whose-token-is-for-an-inactive-account-key-never-open) | Address keys whose token is for an inactive account key never open | Needs an SDK change; not reproduced | | [B264](#b264--a-mount-that-fails-after-starting-its-workers-leaves-them-running) | A mount that fails after starting its workers leaves them running | Not fixed | | [B263](#b263--a-mount-that-can-never-succeed-retries-every-5-s-and-tells-only-the-journal) | A mount that can never succeed retries every 5 s and tells only the journal | Not fixed | -| [B262](#b262--watching-a-large-synced-folder-holds-up-the-sync-engine-and-a-failed-watch-is-never-retried) | Watching a large synced folder holds up the sync engine, and a failed watch is never retried | Not fixed | | [B261](#b261--a-folder-that-has-never-synced-reads-up-to-date) | A folder that has never synced reads "up to date" | Not fixed | -| [B260](#b260--the-poll-pushes-a-new-folders-first-sync-behind-every-other-folder) | The poll pushes a new folder's first sync behind every other folder | Not fixed | | [B258](#b258--journalctl--p-warning-shows-nothing-whatever-the-daemon-logged) | `journalctl -p warning` shows nothing, whatever the daemon logged | Not fixed | | [B134](#b134--a-file-deleted-online-while-the-drain-checks-its-write-is-kept-as-a-conflict-copy) | A file deleted online while the drain checks its write is kept as a conflict copy | Only with `local_first` off | | [B91](#b91--the-daemon-hung-overnight-and-nothing-could-say-what-it-was-waiting-on) | The daemon hung overnight, and nothing could say what it was waiting on | The cause is not diagnosed; the daemon now reports where it is stuck and is restarted | @@ -95,6 +92,7 @@ original wording. | Entry | Title | What is left | |---|---|---| +| [B262](#b262--watching-a-large-synced-folder-holds-up-the-sync-engine-and-a-failed-watch-is-never-retried) | Watching a large synced folder holds up the sync engine, and a failed watch is never retried | The first pass still waits for the watch; a failed watch is not shown in the app | | [B259](#b259--the-sync-engine-can-die-or-hang-and-nothing-shows-it-until-a-restart) | The sync engine can die or hang, and nothing shows it until a restart | No pass deadline; an ended engine is reported, not restarted; the trigger in #53 is not known | | [B199](#b199--a-stopped-share-stays-on-the-shared-by-me-page-for-minutes) | A stopped share stays on the Shared by me page for minutes | The share itself is not deleted; the SDK has no call for it | | [B88](#b88--pdfs-pin-rename-move-delete-and-sharing-still-cannot-name-a-path-under-a-secondary-mount) | `pdfs pin`, `rename`, `move`, `delete` and sharing still cannot name a path under a secondary mount | `pin`, `unpin`, opening a file, thumbnails, uploads, sharing and versions | @@ -116,6 +114,8 @@ a live account since. | Entry | Title | |---|---| +| [B272](#b272--after-a-folder-is-removed-and-added-again-its-file-events-go-to-the-old-id) | After a folder is removed and added again, its file events go to the old id | +| [B260](#b260--the-poll-pushes-a-new-folders-first-sync-behind-every-other-folder) | The poll pushes a new folder's first sync behind every other folder | | [B256](#b256--pdfs-copy-right-after-a-save-copies-the-old-version) | `pdfs copy` right after a save copies the old version | | [B255](#b255--a-synced-folder-whose-files-are-all-gone-here-is-stuck-for-good) | A synced folder whose files are all gone here is stuck for good | | [B254](#b254--an-edit-in-the-same-second-as-a-new-upload-never-arrives) | An edit in the same second as a new upload never arrives | @@ -395,7 +395,7 @@ written before it existed. ### B272 — After a folder is removed and added again, its file events go to the old id -**Status:** Open +**Status:** Fixed, not verified live **Found:** 2026-10-31, code audit after issue #53. **Where:** `rewatch` in `crates/pdfs-fuse/src/sync/engine.rs` @@ -407,8 +407,10 @@ two-minute poll, and the same goes for every later edit until the daemon restart path is still wanted, so the old entry stays with the removed folder's id. Its events become `Reconcile(old id)`, `sync_folder_get` finds no row, and the request is dropped. -**Fix.** Not done. Compare `(path, id)` pairs, and update the id of a path that is still -watched. +**Fix.** `keep_wanted` keeps a watch whose path is still wanted and gives it the id the +folder list has now. + +**Test:** `a_re_added_folder_keeps_its_watch_under_the_new_id` in `pdfs-fuse`. --- @@ -613,7 +615,8 @@ show it (a state file, and `STATUS=` to systemd). ### B262 — Watching a large synced folder holds up the sync engine, and a failed watch is never retried -**Status:** Open +**Status:** Partly fixed. A watch is still set up before the first pass, and a failed watch +is not shown in the folder's state. **Found:** 2026-10-31, code audit after issue #53. **Where:** `rewatch` and `engine_loop` in `crates/pdfs-fuse/src/sync/engine.rs` @@ -631,8 +634,16 @@ line, the folder is never watched, and later changes are found only by the two-m and they use up watches the next folder needs. - Errors the watcher reports later are dropped in its callback. -**Fix.** Not done. Reconcile first, unwatch after a partial failure and retry with a back-off, -turn off symlink following, and show a watch failure in the folder's state. +**Fix.** +- A failed watch is unwatched, which removes the watches it had added under the folder, and + is tried again every 10 minutes. The warning names `fs.inotify.max_user_watches`. +- The watcher no longer follows symlinks. +- Errors the watcher reports are logged. +- Not done: the watch still runs before the folder's first pass. Watching first is what keeps + a change made during the pass from being missed, so the order stays; the cost is one walk + of a new folder's tree. A failed watch is not shown in the folder's state yet. + +**Test:** none for the watch failure itself; it needs the inotify limit reached. --- @@ -660,7 +671,7 @@ account-wide pause on the folder rows. ### B260 — The poll pushes a new folder's first sync behind every other folder -**Status:** Open +**Status:** Fixed, not verified live **Found:** 2026-10-31, code audit after issue #53. **Where:** `engine_loop` in `crates/pdfs-fuse/src/sync/engine.rs` @@ -673,8 +684,11 @@ running. When a burst holds both a `ReconcileAll` and a folder's `Reconcile(id)` only `reconcile_all`, which goes in id order. A new folder has the highest id, so it always goes last. -**Fix.** Not done. Run the folders asked for by id before the rest, and don't queue a poll -while a pass runs. +**Fix.** `run_burst` runs the folders a burst names by id first, lowest id first, and then +every other folder when the burst also holds a `ReconcileAll`. A named folder is read again +before its pass. A pause that lands between passes now stops the rest of the burst. + +**Test:** `named_folders_run_before_the_rest_of_a_burst` in `pdfs-fuse`. --- From 437d14b0f5f51f6e992bf2c40699ec4bf9997df1 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 31 Oct 2026 10:30:52 +0100 Subject: [PATCH 05/12] fix(gui): say a folder is waiting for its first sync, not up to date A new sync folder row starts with state idle, so the app called a folder up to date before any of it reached Drive (B261, issue #53). A mirror folder that has never synced now reads "waiting for first sync", and the This computer row counts such folders. All catalogs translated. --- crates/pdfs-gui/src/app/pages/devices.rs | 55 ++++++- crates/pdfs-gui/src/app/pages/locations.rs | 23 ++- docs/BUGS.md | 15 +- po/de.po | 175 ++++++++++---------- po/en_GB.po | 175 ++++++++++---------- po/es.po | 175 ++++++++++---------- po/fr.po | 175 ++++++++++---------- po/it.po | 175 ++++++++++---------- po/nl.po | 175 ++++++++++---------- po/pdfs.pot | 175 ++++++++++---------- po/pl.po | 176 +++++++++++---------- po/pt_BR.po | 175 ++++++++++---------- 12 files changed, 923 insertions(+), 746 deletions(-) diff --git a/crates/pdfs-gui/src/app/pages/devices.rs b/crates/pdfs-gui/src/app/pages/devices.rs index 16635ad..b11517a 100644 --- a/crates/pdfs-gui/src/app/pages/devices.rs +++ b/crates/pdfs-gui/src/app/pages/devices.rs @@ -597,6 +597,19 @@ pub(crate) fn this_computer_subtitle(folders: &[SyncFolderInfo]) -> String { folders.len() as u64, &[], ); + // A folder whose first pass has not run is not backed up yet, whatever its + // state column says (`docs/BUGS.md` B261). + let waiting = folders.iter().filter(|f| never_synced(f)).count(); + let count = match waiting { + 0 => count, + // Translators: {folders} is "N folders backed up"; {n} counts the folders whose first sync has not run yet. + n => ngettext_f( + "{folders} · {n} waiting for its first sync", + "{folders} · {n} waiting for their first sync", + n as u64, + &[("folders", &count)], + ), + }; let attention = folders .iter() .filter(|f| f.state == "error" || f.state == "conflict") @@ -613,12 +626,21 @@ pub(crate) fn this_computer_subtitle(folders: &[SyncFolderInfo]) -> String { } } -/// Human label for a synced folder's `state` column. -pub(crate) fn sync_state_label(state: &str) -> String { +/// Whether a synced folder is a mirror whose first pass has not finished. +/// Its `state` starts out `idle`, which would read as "up to date". +fn never_synced(folder: &SyncFolderInfo) -> bool { + folder.mode == "mirror" && folder.last_sync == 0 && folder.state == "idle" +} + +/// Human label for a synced folder's `state` column. `never_synced` is a +/// mirror folder whose first pass has not finished: its `state` starts out +/// `idle`, and "up to date" would say the opposite of the truth (B261). +pub(crate) fn sync_state_label(state: &str, never_synced: bool) -> String { match state { "syncing" => gettext("syncing…"), "error" => gettext("sync error"), "conflict" => gettext("needs attention"), + _ if never_synced => gettext("waiting for first sync"), _ => gettext("up to date"), } } @@ -1274,7 +1296,7 @@ mod tests { remote_uid: "vol~link".into(), mode: "mirror".into(), state: state.into(), - last_sync: 0, + last_sync: 1_700_000_000, pending_mode: None, progress: None, paused: false, @@ -1286,6 +1308,33 @@ mod tests { assert_eq!(this_computer_subtitle(&[]), "No folders backed up yet"); } + /// A folder added a moment ago is not backed up yet, and the row must not + /// say it is (`docs/BUGS.md` B261). An on-demand folder has no first sync. + #[test] + fn a_folder_before_its_first_sync_is_not_counted_as_backed_up() { + let new = SyncFolderInfo { + last_sync: 0, + ..folder("idle") + }; + assert_eq!( + this_computer_subtitle(&[folder("idle"), new.clone()]), + "2 folders backed up · 1 waiting for its first sync" + ); + let failed = SyncFolderInfo { + state: "error".into(), + ..new.clone() + }; + assert_eq!( + this_computer_subtitle(&[new.clone(), failed]), + "2 folders backed up · 1 waiting for its first sync · 1 needs attention" + ); + let on_demand = SyncFolderInfo { + mode: "ondemand".into(), + ..new + }; + assert_eq!(this_computer_subtitle(&[on_demand]), "1 folder backed up"); + } + #[test] fn folders_needing_attention_are_counted_not_hidden() { // The per-folder rows live on the Sync page, so this row is the only diff --git a/crates/pdfs-gui/src/app/pages/locations.rs b/crates/pdfs-gui/src/app/pages/locations.rs index 65963d2..a538248 100644 --- a/crates/pdfs-gui/src/app/pages/locations.rs +++ b/crates/pdfs-gui/src/app/pages/locations.rs @@ -1729,7 +1729,10 @@ pub(crate) fn location_subtitle(spec: &MountSpec) -> String { parts.push(match &spec.progress { Some(p) => sync_progress_label(p), None if spec.paused => pgettext("state", "paused"), - None => sync_state_label(&spec.state).to_string(), + None => sync_state_label( + &spec.state, + spec.mode == MountMode::Mirror && spec.last_sync == 0, + ), }); } // Only the locations that are supposed to have a session report its absence: @@ -1775,7 +1778,25 @@ mod tests { MountMode::Mirror, false, ); + assert_eq!(location_subtitle(&spec), "Synced · waiting for first sync"); + } + + /// A folder whose first pass has not run is not up to date, though its + /// state column starts out `idle` (`docs/BUGS.md` B261). + #[test] + fn a_folder_reads_up_to_date_only_after_its_first_sync() { + let mut spec = spec( + MountKind::Device { sync_folder_id: 7 }, + MountMode::Mirror, + false, + ); + assert_eq!(location_subtitle(&spec), "Synced · waiting for first sync"); + spec.last_sync = 1_700_000_000; assert_eq!(location_subtitle(&spec), "Synced · up to date"); + // A folder that failed before it ever synced says so. + spec.last_sync = 0; + spec.state = "error".into(); + assert_eq!(location_subtitle(&spec), "Synced · sync error"); } #[test] diff --git a/docs/BUGS.md b/docs/BUGS.md index 319b4ca..e15e669 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -80,7 +80,6 @@ original wording. | [B264](#b264--a-mount-that-fails-after-starting-its-workers-leaves-them-running) | A mount that fails after starting its workers leaves them running | Not fixed | | [B263](#b263--a-mount-that-can-never-succeed-retries-every-5-s-and-tells-only-the-journal) | A mount that can never succeed retries every 5 s and tells only the journal | Not fixed | | [B262](#b262--watching-a-large-synced-folder-holds-up-the-sync-engine-and-a-failed-watch-is-never-retried) | Watching a large synced folder holds up the sync engine, and a failed watch is never retried | Not fixed | -| [B261](#b261--a-folder-that-has-never-synced-reads-up-to-date) | A folder that has never synced reads "up to date" | Not fixed | | [B260](#b260--the-poll-pushes-a-new-folders-first-sync-behind-every-other-folder) | The poll pushes a new folder's first sync behind every other folder | Not fixed | | [B259](#b259--the-sync-engine-can-die-or-hang-and-nothing-shows-it-until-a-restart) | The sync engine can die or hang, and nothing shows it until a restart | Not fixed; the trigger in #53 is not known | | [B258](#b258--journalctl--p-warning-shows-nothing-whatever-the-daemon-logged) | `journalctl -p warning` shows nothing, whatever the daemon logged | Not fixed | @@ -116,6 +115,7 @@ a live account since. | Entry | Title | |---|---| +| [B261](#b261--a-folder-that-has-never-synced-reads-up-to-date) | A folder that has never synced reads "up to date" | | [B256](#b256--pdfs-copy-right-after-a-save-copies-the-old-version) | `pdfs copy` right after a save copies the old version | | [B255](#b255--a-synced-folder-whose-files-are-all-gone-here-is-stuck-for-good) | A synced folder whose files are all gone here is stuck for good | | [B254](#b254--an-edit-in-the-same-second-as-a-new-upload-never-arrives) | An edit in the same second as a new upload never arrives | @@ -638,7 +638,7 @@ turn off symlink following, and show a watch failure in the folder's state. ### B261 — A folder that has never synced reads "up to date" -**Status:** Open +**Status:** Fixed, not verified live **Found:** 2026-10-31, issue #53: "In Computers I add the folder. It appears as synced." **Where:** `sync_folder` in `crates/pdfs-core/src/db/migrations.rs`; `sync_state_label` in `crates/pdfs-gui/src/app/pages/devices.rs`; the location rows in @@ -653,8 +653,15 @@ every folder while sync is paused for the whole account. it, and a test pins the label for that case. The rows only show a folder's own pause, not the account-wide one. -**Fix.** Not done. Show "waiting for first sync" when `last_sync` is 0, and show the -account-wide pause on the folder rows. +**Fix.** A mirror folder whose `last_sync` is 0 and whose state is `idle` reads "waiting for +first sync" on its row. The This computer row on the Computers page counts such folders, as in +"2 folders backed up · 1 waiting for its first sync". An on-demand folder has no first sync, +so it is left out. The account-wide pause stays where it already is, the pause card at the top +of the page; repeating it on every row would only add noise. + +**Test:** `a_folder_reads_up_to_date_only_after_its_first_sync` and +`a_folder_before_its_first_sync_is_not_counted_as_backed_up` in `pdfs-gui`; the pinned test now +expects the new label. --- diff --git a/po/de.po b/po/de.po index 7ac1044..ec987ec 100644 --- a/po/de.po +++ b/po/de.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:26+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: German\n" @@ -249,13 +249,13 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:1917 #: crates/pdfs-gui/src/app/pages/browser.rs:2039 #: crates/pdfs-gui/src/app/pages/browser.rs:2540 -#: crates/pdfs-gui/src/app/pages/devices.rs:804 -#: crates/pdfs-gui/src/app/pages/devices.rs:957 -#: crates/pdfs-gui/src/app/pages/devices.rs:995 -#: crates/pdfs-gui/src/app/pages/devices.rs:1047 -#: crates/pdfs-gui/src/app/pages/devices.rs:1091 -#: crates/pdfs-gui/src/app/pages/devices.rs:1131 -#: crates/pdfs-gui/src/app/pages/devices.rs:1194 +#: crates/pdfs-gui/src/app/pages/devices.rs:826 +#: crates/pdfs-gui/src/app/pages/devices.rs:979 +#: crates/pdfs-gui/src/app/pages/devices.rs:1017 +#: crates/pdfs-gui/src/app/pages/devices.rs:1069 +#: crates/pdfs-gui/src/app/pages/devices.rs:1113 +#: crates/pdfs-gui/src/app/pages/devices.rs:1153 +#: crates/pdfs-gui/src/app/pages/devices.rs:1216 #: crates/pdfs-gui/src/app/pages/locations.rs:850 #: crates/pdfs-gui/src/app/pages/locations.rs:1054 #: crates/pdfs-gui/src/app/pages/locations.rs:1537 @@ -350,7 +350,7 @@ msgstr "Raster / Liste" #: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/browser.rs:1896 #: crates/pdfs-gui/src/app/pages/browser.rs:1918 -#: crates/pdfs-gui/src/app/pages/devices.rs:996 +#: crates/pdfs-gui/src/app/pages/devices.rs:1018 #: crates/pdfs-gui/src/app/widgets/details.rs:161 msgid "Rename" msgstr "Umbenennen" @@ -682,7 +682,7 @@ msgid "Couldn't add a sync folder" msgstr "Synchronisierungsordner konnte nicht hinzugefügt werden" #: crates/pdfs-gui/src/app/pages/activity_text.rs:58 -#: crates/pdfs-gui/src/app/pages/devices.rs:839 +#: crates/pdfs-gui/src/app/pages/devices.rs:861 msgid "Couldn't restore folders" msgstr "Ordner konnten nicht wiederhergestellt werden" @@ -1274,12 +1274,12 @@ msgstr "Alben konnten nicht geladen werden" #: crates/pdfs-gui/src/app/pages/browser.rs:1873 #: crates/pdfs-gui/src/app/pages/browser.rs:2500 #: crates/pdfs-gui/src/app/pages/browser.rs:2650 -#: crates/pdfs-gui/src/app/pages/devices.rs:707 -#: crates/pdfs-gui/src/app/pages/devices.rs:747 -#: crates/pdfs-gui/src/app/pages/devices.rs:900 -#: crates/pdfs-gui/src/app/pages/devices.rs:1170 -#: crates/pdfs-gui/src/app/pages/devices.rs:1231 -#: crates/pdfs-gui/src/app/pages/devices.rs:1260 +#: crates/pdfs-gui/src/app/pages/devices.rs:729 +#: crates/pdfs-gui/src/app/pages/devices.rs:769 +#: crates/pdfs-gui/src/app/pages/devices.rs:922 +#: crates/pdfs-gui/src/app/pages/devices.rs:1192 +#: crates/pdfs-gui/src/app/pages/devices.rs:1253 +#: crates/pdfs-gui/src/app/pages/devices.rs:1282 #: crates/pdfs-gui/src/app/pages/duplicates.rs:221 #: crates/pdfs-gui/src/app/pages/locations.rs:577 #: crates/pdfs-gui/src/app/pages/locations.rs:824 @@ -1929,7 +1929,7 @@ msgid "{status}. {message}" msgstr "{status}. {message}" #: crates/pdfs-gui/src/app/pages/browser.rs:1900 -#: crates/pdfs-gui/src/app/pages/devices.rs:989 +#: crates/pdfs-gui/src/app/pages/devices.rs:1011 msgid "New name" msgstr "Neuer Name" @@ -1954,7 +1954,7 @@ msgstr "Den Zielordner auswählen." #: crates/pdfs-gui/src/app/pages/browser.rs:1989 #: crates/pdfs-gui/src/app/pages/browser.rs:1994 -#: crates/pdfs-gui/src/app/pages/devices.rs:1195 +#: crates/pdfs-gui/src/app/pages/devices.rs:1217 msgid "Move Here" msgstr "Hierher verschieben" @@ -2279,76 +2279,87 @@ msgid_plural "{n} folders backed up" msgstr[0] "{n} Ordner gesichert" msgstr[1] "{n} Ordner gesichert" -#: crates/pdfs-gui/src/app/pages/devices.rs:608 +#: crates/pdfs-gui/src/app/pages/devices.rs:607 +#, rust-format +msgid "{folders} · {n} waiting for its first sync" +msgid_plural "{folders} · {n} waiting for their first sync" +msgstr[0] "{folders} · {n} wartet auf die erste Synchronisierung" +msgstr[1] "{folders} · {n} warten auf die erste Synchronisierung" + +#: crates/pdfs-gui/src/app/pages/devices.rs:621 #, rust-format msgid "{folders} · {n} needs attention" msgid_plural "{folders} · {n} need attention" msgstr[0] "{folders} · {n} erfordert Aufmerksamkeit" msgstr[1] "{folders} · {n} erfordern Aufmerksamkeit" -#: crates/pdfs-gui/src/app/pages/devices.rs:619 +#: crates/pdfs-gui/src/app/pages/devices.rs:640 msgid "syncing…" msgstr "wird synchronisiert …" -#: crates/pdfs-gui/src/app/pages/devices.rs:620 +#: crates/pdfs-gui/src/app/pages/devices.rs:641 msgid "sync error" msgstr "Synchronisierungsfehler" -#: crates/pdfs-gui/src/app/pages/devices.rs:621 +#: crates/pdfs-gui/src/app/pages/devices.rs:642 msgid "needs attention" msgstr "erfordert Aufmerksamkeit" -#: crates/pdfs-gui/src/app/pages/devices.rs:622 +#: crates/pdfs-gui/src/app/pages/devices.rs:643 +msgid "waiting for first sync" +msgstr "wartet auf erste Synchronisierung" + +#: crates/pdfs-gui/src/app/pages/devices.rs:644 msgid "up to date" msgstr "aktuell" -#: crates/pdfs-gui/src/app/pages/devices.rs:632 +#: crates/pdfs-gui/src/app/pages/devices.rs:654 msgid "checking for changes…" msgstr "wird auf Änderungen geprüft …" -#: crates/pdfs-gui/src/app/pages/devices.rs:638 +#: crates/pdfs-gui/src/app/pages/devices.rs:660 #, rust-format msgid "checking for changes — {n} item checked" msgid_plural "checking for changes — {n} items checked" msgstr[0] "wird auf Änderungen geprüft – {n} Element geprüft" msgstr[1] "wird auf Änderungen geprüft – {n} Elemente geprüft" -#: crates/pdfs-gui/src/app/pages/devices.rs:645 +#: crates/pdfs-gui/src/app/pages/devices.rs:667 #, rust-format msgid "checking for changes — {done} of {total}" msgstr "wird auf Änderungen geprüft – {done} von {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:657 +#: crates/pdfs-gui/src/app/pages/devices.rs:679 #, rust-format msgid "syncing {done} of {total}" msgstr "{done} von {total} wird synchronisiert" -#: crates/pdfs-gui/src/app/pages/devices.rs:663 +#: crates/pdfs-gui/src/app/pages/devices.rs:685 #, rust-format msgid "syncing {file} — {done} of {total}" msgstr "{file} wird synchronisiert – {done} von {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:675 +#: crates/pdfs-gui/src/app/pages/devices.rs:697 msgid "Add Folder to Sync" msgstr "Ordner zur Synchronisierung hinzufügen" -#: crates/pdfs-gui/src/app/pages/devices.rs:699 +#: crates/pdfs-gui/src/app/pages/devices.rs:721 #: crates/pdfs-gui/src/app/pages/locations.rs:1559 #: crates/pdfs-gui/src/app/pages/locations.rs:1686 msgid "Syncing folder…" msgstr "Ordner wird synchronisiert …" -#: crates/pdfs-gui/src/app/pages/devices.rs:702 -#: crates/pdfs-gui/src/app/pages/devices.rs:706 +#: crates/pdfs-gui/src/app/pages/devices.rs:724 +#: crates/pdfs-gui/src/app/pages/devices.rs:728 msgid "Couldn't add folder" msgstr "Ordner konnte nicht hinzugefügt werden" -#: crates/pdfs-gui/src/app/pages/devices.rs:740 -#: crates/pdfs-gui/src/app/pages/devices.rs:746 +#: crates/pdfs-gui/src/app/pages/devices.rs:762 +#: crates/pdfs-gui/src/app/pages/devices.rs:768 msgid "Couldn't list folders to restore" msgstr "Wiederherstellbare Ordner konnten nicht aufgelistet werden" -#: crates/pdfs-gui/src/app/pages/devices.rs:772 +#: crates/pdfs-gui/src/app/pages/devices.rs:794 msgid "" "Tick the folders to sync to this machine, and adjust where each one should " "live." @@ -2356,19 +2367,19 @@ msgstr "" "Die Ordner ankreuzen, die mit diesem Rechner synchronisiert werden sollen, " "und für jeden den Speicherort anpassen." -#: crates/pdfs-gui/src/app/pages/devices.rs:798 +#: crates/pdfs-gui/src/app/pages/devices.rs:820 msgid "Restore Folders" msgstr "Ordner wiederherstellen" -#: crates/pdfs-gui/src/app/pages/devices.rs:805 +#: crates/pdfs-gui/src/app/pages/devices.rs:827 msgid "Restore" msgstr "Wiederherstellen" -#: crates/pdfs-gui/src/app/pages/devices.rs:838 +#: crates/pdfs-gui/src/app/pages/devices.rs:860 msgid "Restoring folders…" msgstr "Ordner werden wiederhergestellt …" -#: crates/pdfs-gui/src/app/pages/devices.rs:850 +#: crates/pdfs-gui/src/app/pages/devices.rs:872 #, rust-format msgid "" "These folders are backed up from “{name}”. Restored folders keep syncing " @@ -2377,37 +2388,37 @@ msgstr "" "Diese Ordner werden von „{name}“ gesichert. Wiederhergestellte Ordner werden " "weiterhin mit der Kopie dieses Computers synchronisiert." -#: crates/pdfs-gui/src/app/pages/devices.rs:853 +#: crates/pdfs-gui/src/app/pages/devices.rs:875 msgid "These folders are backed up under this computer in Proton Drive." msgstr "Diese Ordner werden in Proton Drive unter diesem Computer gesichert." -#: crates/pdfs-gui/src/app/pages/devices.rs:862 +#: crates/pdfs-gui/src/app/pages/devices.rs:884 #, rust-format msgid "Nothing to restore — “{name}”'s folders are all synced here." msgstr "" "Nichts wiederherzustellen – alle Ordner von „{name}“ werden bereits hier " "synchronisiert." -#: crates/pdfs-gui/src/app/pages/devices.rs:865 +#: crates/pdfs-gui/src/app/pages/devices.rs:887 msgid "Nothing to restore — this computer's folders are all synced here." msgstr "" "Nichts wiederherzustellen – alle Ordner dieses Computers werden bereits hier " "synchronisiert." -#: crates/pdfs-gui/src/app/pages/devices.rs:889 +#: crates/pdfs-gui/src/app/pages/devices.rs:911 msgid "Folder will switch to online only" msgstr "Ordner wird auf „Nur online“ umgestellt" -#: crates/pdfs-gui/src/app/pages/devices.rs:891 +#: crates/pdfs-gui/src/app/pages/devices.rs:913 msgid "Folder will download and stay synced" msgstr "Ordner wird heruntergeladen und bleibt synchronisiert" -#: crates/pdfs-gui/src/app/pages/devices.rs:895 -#: crates/pdfs-gui/src/app/pages/devices.rs:899 +#: crates/pdfs-gui/src/app/pages/devices.rs:917 +#: crates/pdfs-gui/src/app/pages/devices.rs:921 msgid "Couldn't change mode" msgstr "Modus konnte nicht geändert werden" -#: crates/pdfs-gui/src/app/pages/devices.rs:931 +#: crates/pdfs-gui/src/app/pages/devices.rs:953 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2425,7 +2436,7 @@ msgstr "" "Ordner auf „Synchronisiert“ umstellen und vor dem Entfernen warten, bis der " "Download abgeschlossen ist." -#: crates/pdfs-gui/src/app/pages/devices.rs:937 +#: crates/pdfs-gui/src/app/pages/devices.rs:959 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2439,68 +2450,68 @@ msgstr "" "mehr synchronisiert. Es kann zusätzlich ausgewählt werden, die Kopie in " "Proton Drive zu löschen." -#: crates/pdfs-gui/src/app/pages/devices.rs:942 +#: crates/pdfs-gui/src/app/pages/devices.rs:964 msgid "Stop Syncing Folder?" msgstr "Synchronisierung des Ordners beenden?" -#: crates/pdfs-gui/src/app/pages/devices.rs:947 +#: crates/pdfs-gui/src/app/pages/devices.rs:969 msgid "Also delete from Proton Drive" msgstr "Auch aus Proton Drive löschen" -#: crates/pdfs-gui/src/app/pages/devices.rs:949 +#: crates/pdfs-gui/src/app/pages/devices.rs:971 msgid "Deletes the only copy of these files." msgstr "Löscht die einzige Kopie dieser Dateien." -#: crates/pdfs-gui/src/app/pages/devices.rs:951 +#: crates/pdfs-gui/src/app/pages/devices.rs:973 msgid "The local copy is unaffected." msgstr "Die lokale Kopie bleibt unverändert." -#: crates/pdfs-gui/src/app/pages/devices.rs:958 +#: crates/pdfs-gui/src/app/pages/devices.rs:980 msgid "Stop Syncing" msgstr "Synchronisierung beenden" -#: crates/pdfs-gui/src/app/pages/devices.rs:971 +#: crates/pdfs-gui/src/app/pages/devices.rs:993 msgid "Stopped syncing folder" msgstr "Synchronisierung des Ordners beendet" -#: crates/pdfs-gui/src/app/pages/devices.rs:972 +#: crates/pdfs-gui/src/app/pages/devices.rs:994 msgid "Couldn't stop syncing the folder" msgstr "Synchronisierung des Ordners konnte nicht beendet werden" -#: crates/pdfs-gui/src/app/pages/devices.rs:983 +#: crates/pdfs-gui/src/app/pages/devices.rs:1005 msgid "Rename Computer" msgstr "Computer umbenennen" #. Translators: {name} is the computer's current name. -#: crates/pdfs-gui/src/app/pages/devices.rs:985 +#: crates/pdfs-gui/src/app/pages/devices.rs:1007 #, rust-format msgid "Rename “{name}”." msgstr "„{name}“ umbenennen." -#: crates/pdfs-gui/src/app/pages/devices.rs:1010 -#: crates/pdfs-gui/src/app/pages/devices.rs:1022 +#: crates/pdfs-gui/src/app/pages/devices.rs:1032 +#: crates/pdfs-gui/src/app/pages/devices.rs:1044 msgid "Couldn't rename the computer" msgstr "Der Computer konnte nicht umbenannt werden" -#: crates/pdfs-gui/src/app/pages/devices.rs:1011 +#: crates/pdfs-gui/src/app/pages/devices.rs:1033 msgid "A name is required." msgstr "Ein Name ist erforderlich." -#: crates/pdfs-gui/src/app/pages/devices.rs:1021 +#: crates/pdfs-gui/src/app/pages/devices.rs:1043 msgid "Computer renamed" msgstr "Computer umbenannt" #. Translators: {name} is the computer's name, which the user must type. -#: crates/pdfs-gui/src/app/pages/devices.rs:1034 +#: crates/pdfs-gui/src/app/pages/devices.rs:1056 #, rust-format msgid "Type “{name}” to confirm" msgstr "Zur Bestätigung „{name}“ eingeben" -#: crates/pdfs-gui/src/app/pages/devices.rs:1039 +#: crates/pdfs-gui/src/app/pages/devices.rs:1061 msgid "Remove Computer?" msgstr "Computer entfernen?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1042 +#: crates/pdfs-gui/src/app/pages/devices.rs:1064 #, rust-format msgid "" "Remove “{name}” from this account?\n" @@ -2515,26 +2526,26 @@ msgstr "" "gelöscht. Die Dateien auf dem Computer selbst bleiben unberührt – dies kann " "jedoch von hier aus nicht rückgängig gemacht werden." -#: crates/pdfs-gui/src/app/pages/devices.rs:1048 +#: crates/pdfs-gui/src/app/pages/devices.rs:1070 #: crates/pdfs-gui/src/app/pages/shared.rs:1025 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:634 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:876 msgid "Remove" msgstr "Entfernen" -#: crates/pdfs-gui/src/app/pages/devices.rs:1065 +#: crates/pdfs-gui/src/app/pages/devices.rs:1087 msgid "Computer removed" msgstr "Computer entfernt" -#: crates/pdfs-gui/src/app/pages/devices.rs:1066 +#: crates/pdfs-gui/src/app/pages/devices.rs:1088 msgid "Couldn't remove the computer" msgstr "Der Computer konnte nicht entfernt werden" -#: crates/pdfs-gui/src/app/pages/devices.rs:1084 +#: crates/pdfs-gui/src/app/pages/devices.rs:1106 msgid "Continue This Backup Here?" msgstr "Diese Sicherung hier fortsetzen?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1087 +#: crates/pdfs-gui/src/app/pages/devices.rs:1109 #, rust-format msgid "" "This computer takes over the backup of “{name}”, for example after a " @@ -2553,25 +2564,25 @@ msgstr "" "werden nicht verschoben. Mit „Auf diesem Computer wiederherstellen“ lässt " "sich danach zurückholen, was „{name}“ synchronisiert hat." -#: crates/pdfs-gui/src/app/pages/devices.rs:1092 +#: crates/pdfs-gui/src/app/pages/devices.rs:1114 msgid "Continue Here" msgstr "Hier fortsetzen" #. Translators: {name} is the name of the computer whose backup this one took over. -#: crates/pdfs-gui/src/app/pages/devices.rs:1099 +#: crates/pdfs-gui/src/app/pages/devices.rs:1121 #, rust-format msgid "Now backing up as “{name}”" msgstr "Sicherung jetzt als „{name}“" -#: crates/pdfs-gui/src/app/pages/devices.rs:1108 +#: crates/pdfs-gui/src/app/pages/devices.rs:1130 msgid "Couldn't continue that backup here" msgstr "Diese Sicherung konnte hier nicht fortgesetzt werden" -#: crates/pdfs-gui/src/app/pages/devices.rs:1124 +#: crates/pdfs-gui/src/app/pages/devices.rs:1146 msgid "Sync With This Computer?" msgstr "Mit diesem Computer synchronisieren?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1127 +#: crates/pdfs-gui/src/app/pages/devices.rs:1149 #, rust-format msgid "" "This computer and “{name}” keep the same folders in step. A change or a " @@ -2588,25 +2599,25 @@ msgstr "" "„{name}“. Bereits hier synchronisierte Ordner bleiben, wo sie sind. Als " "Nächstes wählen Sie, welche seiner Ordner hier synchronisiert werden." -#: crates/pdfs-gui/src/app/pages/devices.rs:1132 +#: crates/pdfs-gui/src/app/pages/devices.rs:1154 msgid "Sync Here" msgstr "Hier synchronisieren" #. Translators: {name} is the name of the other computer this one now syncs with. -#: crates/pdfs-gui/src/app/pages/devices.rs:1139 +#: crates/pdfs-gui/src/app/pages/devices.rs:1161 #, rust-format msgid "Now syncing with “{name}”" msgstr "Jetzt synchron mit „{name}“" -#: crates/pdfs-gui/src/app/pages/devices.rs:1155 +#: crates/pdfs-gui/src/app/pages/devices.rs:1177 msgid "Couldn't sync with that computer" msgstr "Synchronisierung mit diesem Computer nicht möglich" -#: crates/pdfs-gui/src/app/pages/devices.rs:1187 +#: crates/pdfs-gui/src/app/pages/devices.rs:1209 msgid "Move Backup to This Computer?" msgstr "Sicherung auf diesen Computer verschieben?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1190 +#: crates/pdfs-gui/src/app/pages/devices.rs:1212 #, rust-format msgid "" "The folders backed up by “{name}” move into this computer's backup in Proton " @@ -2623,12 +2634,12 @@ msgstr "" "verschwinden." #. Translators: {name} is the name of the computer whose folders moved to this one. -#: crates/pdfs-gui/src/app/pages/devices.rs:1202 +#: crates/pdfs-gui/src/app/pages/devices.rs:1224 #, rust-format msgid "Moved the backup of “{name}”" msgstr "Sicherung von „{name}“ verschoben" -#: crates/pdfs-gui/src/app/pages/devices.rs:1216 +#: crates/pdfs-gui/src/app/pages/devices.rs:1238 msgid "Couldn't move that backup" msgstr "Diese Sicherung konnte nicht verschoben werden" @@ -3374,7 +3385,7 @@ msgid "paused" msgstr "pausiert" #. Translators: a folder's files can't be opened right now. -#: crates/pdfs-gui/src/app/pages/locations.rs:1742 +#: crates/pdfs-gui/src/app/pages/locations.rs:1745 msgid "not available" msgstr "nicht verfügbar" diff --git a/po/en_GB.po b/po/en_GB.po index 5a2e3c1..c9e37c3 100644 --- a/po/en_GB.po +++ b/po/en_GB.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:26+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: English (United Kingdom)\n" @@ -245,13 +245,13 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:1917 #: crates/pdfs-gui/src/app/pages/browser.rs:2039 #: crates/pdfs-gui/src/app/pages/browser.rs:2540 -#: crates/pdfs-gui/src/app/pages/devices.rs:804 -#: crates/pdfs-gui/src/app/pages/devices.rs:957 -#: crates/pdfs-gui/src/app/pages/devices.rs:995 -#: crates/pdfs-gui/src/app/pages/devices.rs:1047 -#: crates/pdfs-gui/src/app/pages/devices.rs:1091 -#: crates/pdfs-gui/src/app/pages/devices.rs:1131 -#: crates/pdfs-gui/src/app/pages/devices.rs:1194 +#: crates/pdfs-gui/src/app/pages/devices.rs:826 +#: crates/pdfs-gui/src/app/pages/devices.rs:979 +#: crates/pdfs-gui/src/app/pages/devices.rs:1017 +#: crates/pdfs-gui/src/app/pages/devices.rs:1069 +#: crates/pdfs-gui/src/app/pages/devices.rs:1113 +#: crates/pdfs-gui/src/app/pages/devices.rs:1153 +#: crates/pdfs-gui/src/app/pages/devices.rs:1216 #: crates/pdfs-gui/src/app/pages/locations.rs:850 #: crates/pdfs-gui/src/app/pages/locations.rs:1054 #: crates/pdfs-gui/src/app/pages/locations.rs:1537 @@ -346,7 +346,7 @@ msgstr "Grid / list" #: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/browser.rs:1896 #: crates/pdfs-gui/src/app/pages/browser.rs:1918 -#: crates/pdfs-gui/src/app/pages/devices.rs:996 +#: crates/pdfs-gui/src/app/pages/devices.rs:1018 #: crates/pdfs-gui/src/app/widgets/details.rs:161 msgid "Rename" msgstr "Rename" @@ -674,7 +674,7 @@ msgid "Couldn't add a sync folder" msgstr "Couldn't add a sync folder" #: crates/pdfs-gui/src/app/pages/activity_text.rs:58 -#: crates/pdfs-gui/src/app/pages/devices.rs:839 +#: crates/pdfs-gui/src/app/pages/devices.rs:861 msgid "Couldn't restore folders" msgstr "Couldn't restore folders" @@ -1262,12 +1262,12 @@ msgstr "Couldn't load albums" #: crates/pdfs-gui/src/app/pages/browser.rs:1873 #: crates/pdfs-gui/src/app/pages/browser.rs:2500 #: crates/pdfs-gui/src/app/pages/browser.rs:2650 -#: crates/pdfs-gui/src/app/pages/devices.rs:707 -#: crates/pdfs-gui/src/app/pages/devices.rs:747 -#: crates/pdfs-gui/src/app/pages/devices.rs:900 -#: crates/pdfs-gui/src/app/pages/devices.rs:1170 -#: crates/pdfs-gui/src/app/pages/devices.rs:1231 -#: crates/pdfs-gui/src/app/pages/devices.rs:1260 +#: crates/pdfs-gui/src/app/pages/devices.rs:729 +#: crates/pdfs-gui/src/app/pages/devices.rs:769 +#: crates/pdfs-gui/src/app/pages/devices.rs:922 +#: crates/pdfs-gui/src/app/pages/devices.rs:1192 +#: crates/pdfs-gui/src/app/pages/devices.rs:1253 +#: crates/pdfs-gui/src/app/pages/devices.rs:1282 #: crates/pdfs-gui/src/app/pages/duplicates.rs:221 #: crates/pdfs-gui/src/app/pages/locations.rs:577 #: crates/pdfs-gui/src/app/pages/locations.rs:824 @@ -1910,7 +1910,7 @@ msgid "{status}. {message}" msgstr "{status}. {message}" #: crates/pdfs-gui/src/app/pages/browser.rs:1900 -#: crates/pdfs-gui/src/app/pages/devices.rs:989 +#: crates/pdfs-gui/src/app/pages/devices.rs:1011 msgid "New name" msgstr "New name" @@ -1935,7 +1935,7 @@ msgstr "Choose the folder to move into." #: crates/pdfs-gui/src/app/pages/browser.rs:1989 #: crates/pdfs-gui/src/app/pages/browser.rs:1994 -#: crates/pdfs-gui/src/app/pages/devices.rs:1195 +#: crates/pdfs-gui/src/app/pages/devices.rs:1217 msgid "Move Here" msgstr "Move Here" @@ -2256,76 +2256,87 @@ msgid_plural "{n} folders backed up" msgstr[0] "{n} folder backed up" msgstr[1] "{n} folders backed up" -#: crates/pdfs-gui/src/app/pages/devices.rs:608 +#: crates/pdfs-gui/src/app/pages/devices.rs:607 +#, rust-format +msgid "{folders} · {n} waiting for its first sync" +msgid_plural "{folders} · {n} waiting for their first sync" +msgstr[0] "{folders} · {n} waiting for its first sync" +msgstr[1] "{folders} · {n} waiting for their first sync" + +#: crates/pdfs-gui/src/app/pages/devices.rs:621 #, rust-format msgid "{folders} · {n} needs attention" msgid_plural "{folders} · {n} need attention" msgstr[0] "{folders} · {n} needs attention" msgstr[1] "{folders} · {n} need attention" -#: crates/pdfs-gui/src/app/pages/devices.rs:619 +#: crates/pdfs-gui/src/app/pages/devices.rs:640 msgid "syncing…" msgstr "syncing…" -#: crates/pdfs-gui/src/app/pages/devices.rs:620 +#: crates/pdfs-gui/src/app/pages/devices.rs:641 msgid "sync error" msgstr "sync error" -#: crates/pdfs-gui/src/app/pages/devices.rs:621 +#: crates/pdfs-gui/src/app/pages/devices.rs:642 msgid "needs attention" msgstr "needs attention" -#: crates/pdfs-gui/src/app/pages/devices.rs:622 +#: crates/pdfs-gui/src/app/pages/devices.rs:643 +msgid "waiting for first sync" +msgstr "waiting for first sync" + +#: crates/pdfs-gui/src/app/pages/devices.rs:644 msgid "up to date" msgstr "up to date" -#: crates/pdfs-gui/src/app/pages/devices.rs:632 +#: crates/pdfs-gui/src/app/pages/devices.rs:654 msgid "checking for changes…" msgstr "checking for changes…" -#: crates/pdfs-gui/src/app/pages/devices.rs:638 +#: crates/pdfs-gui/src/app/pages/devices.rs:660 #, rust-format msgid "checking for changes — {n} item checked" msgid_plural "checking for changes — {n} items checked" msgstr[0] "checking for changes — {n} item checked" msgstr[1] "checking for changes — {n} items checked" -#: crates/pdfs-gui/src/app/pages/devices.rs:645 +#: crates/pdfs-gui/src/app/pages/devices.rs:667 #, rust-format msgid "checking for changes — {done} of {total}" msgstr "checking for changes — {done} of {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:657 +#: crates/pdfs-gui/src/app/pages/devices.rs:679 #, rust-format msgid "syncing {done} of {total}" msgstr "syncing {done} of {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:663 +#: crates/pdfs-gui/src/app/pages/devices.rs:685 #, rust-format msgid "syncing {file} — {done} of {total}" msgstr "syncing {file} — {done} of {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:675 +#: crates/pdfs-gui/src/app/pages/devices.rs:697 msgid "Add Folder to Sync" msgstr "Add Folder to Sync" -#: crates/pdfs-gui/src/app/pages/devices.rs:699 +#: crates/pdfs-gui/src/app/pages/devices.rs:721 #: crates/pdfs-gui/src/app/pages/locations.rs:1559 #: crates/pdfs-gui/src/app/pages/locations.rs:1686 msgid "Syncing folder…" msgstr "Syncing folder…" -#: crates/pdfs-gui/src/app/pages/devices.rs:702 -#: crates/pdfs-gui/src/app/pages/devices.rs:706 +#: crates/pdfs-gui/src/app/pages/devices.rs:724 +#: crates/pdfs-gui/src/app/pages/devices.rs:728 msgid "Couldn't add folder" msgstr "Couldn't add folder" -#: crates/pdfs-gui/src/app/pages/devices.rs:740 -#: crates/pdfs-gui/src/app/pages/devices.rs:746 +#: crates/pdfs-gui/src/app/pages/devices.rs:762 +#: crates/pdfs-gui/src/app/pages/devices.rs:768 msgid "Couldn't list folders to restore" msgstr "Couldn't list folders to restore" -#: crates/pdfs-gui/src/app/pages/devices.rs:772 +#: crates/pdfs-gui/src/app/pages/devices.rs:794 msgid "" "Tick the folders to sync to this machine, and adjust where each one should " "live." @@ -2333,19 +2344,19 @@ msgstr "" "Tick the folders to sync to this machine, and adjust where each one should " "live." -#: crates/pdfs-gui/src/app/pages/devices.rs:798 +#: crates/pdfs-gui/src/app/pages/devices.rs:820 msgid "Restore Folders" msgstr "Restore Folders" -#: crates/pdfs-gui/src/app/pages/devices.rs:805 +#: crates/pdfs-gui/src/app/pages/devices.rs:827 msgid "Restore" msgstr "Restore" -#: crates/pdfs-gui/src/app/pages/devices.rs:838 +#: crates/pdfs-gui/src/app/pages/devices.rs:860 msgid "Restoring folders…" msgstr "Restoring folders…" -#: crates/pdfs-gui/src/app/pages/devices.rs:850 +#: crates/pdfs-gui/src/app/pages/devices.rs:872 #, rust-format msgid "" "These folders are backed up from “{name}”. Restored folders keep syncing " @@ -2354,33 +2365,33 @@ msgstr "" "These folders are backed up from “{name}”. Restored folders keep syncing " "with that computer's copy." -#: crates/pdfs-gui/src/app/pages/devices.rs:853 +#: crates/pdfs-gui/src/app/pages/devices.rs:875 msgid "These folders are backed up under this computer in Proton Drive." msgstr "These folders are backed up under this computer in Proton Drive." -#: crates/pdfs-gui/src/app/pages/devices.rs:862 +#: crates/pdfs-gui/src/app/pages/devices.rs:884 #, rust-format msgid "Nothing to restore — “{name}”'s folders are all synced here." msgstr "Nothing to restore — “{name}”'s folders are all synced here." -#: crates/pdfs-gui/src/app/pages/devices.rs:865 +#: crates/pdfs-gui/src/app/pages/devices.rs:887 msgid "Nothing to restore — this computer's folders are all synced here." msgstr "Nothing to restore — this computer's folders are all synced here." -#: crates/pdfs-gui/src/app/pages/devices.rs:889 +#: crates/pdfs-gui/src/app/pages/devices.rs:911 msgid "Folder will switch to online only" msgstr "Folder will switch to online only" -#: crates/pdfs-gui/src/app/pages/devices.rs:891 +#: crates/pdfs-gui/src/app/pages/devices.rs:913 msgid "Folder will download and stay synced" msgstr "Folder will download and stay synced" -#: crates/pdfs-gui/src/app/pages/devices.rs:895 -#: crates/pdfs-gui/src/app/pages/devices.rs:899 +#: crates/pdfs-gui/src/app/pages/devices.rs:917 +#: crates/pdfs-gui/src/app/pages/devices.rs:921 msgid "Couldn't change mode" msgstr "Couldn't change mode" -#: crates/pdfs-gui/src/app/pages/devices.rs:931 +#: crates/pdfs-gui/src/app/pages/devices.rs:953 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2397,7 +2408,7 @@ msgstr "" "copy, cancel, switch the folder to Synced, and wait for the download to " "finish before removing it." -#: crates/pdfs-gui/src/app/pages/devices.rs:937 +#: crates/pdfs-gui/src/app/pages/devices.rs:959 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2410,68 +2421,68 @@ msgstr "" "The local files stay on this disk and simply stop being synced. Choose " "whether to also delete the copy in Proton Drive." -#: crates/pdfs-gui/src/app/pages/devices.rs:942 +#: crates/pdfs-gui/src/app/pages/devices.rs:964 msgid "Stop Syncing Folder?" msgstr "Stop Syncing Folder?" -#: crates/pdfs-gui/src/app/pages/devices.rs:947 +#: crates/pdfs-gui/src/app/pages/devices.rs:969 msgid "Also delete from Proton Drive" msgstr "Also delete from Proton Drive" -#: crates/pdfs-gui/src/app/pages/devices.rs:949 +#: crates/pdfs-gui/src/app/pages/devices.rs:971 msgid "Deletes the only copy of these files." msgstr "Deletes the only copy of these files." -#: crates/pdfs-gui/src/app/pages/devices.rs:951 +#: crates/pdfs-gui/src/app/pages/devices.rs:973 msgid "The local copy is unaffected." msgstr "The local copy is unaffected." -#: crates/pdfs-gui/src/app/pages/devices.rs:958 +#: crates/pdfs-gui/src/app/pages/devices.rs:980 msgid "Stop Syncing" msgstr "Stop Syncing" -#: crates/pdfs-gui/src/app/pages/devices.rs:971 +#: crates/pdfs-gui/src/app/pages/devices.rs:993 msgid "Stopped syncing folder" msgstr "Stopped syncing folder" -#: crates/pdfs-gui/src/app/pages/devices.rs:972 +#: crates/pdfs-gui/src/app/pages/devices.rs:994 msgid "Couldn't stop syncing the folder" msgstr "Couldn't stop syncing the folder" -#: crates/pdfs-gui/src/app/pages/devices.rs:983 +#: crates/pdfs-gui/src/app/pages/devices.rs:1005 msgid "Rename Computer" msgstr "Rename Computer" #. Translators: {name} is the computer's current name. -#: crates/pdfs-gui/src/app/pages/devices.rs:985 +#: crates/pdfs-gui/src/app/pages/devices.rs:1007 #, rust-format msgid "Rename “{name}”." msgstr "Rename “{name}”." -#: crates/pdfs-gui/src/app/pages/devices.rs:1010 -#: crates/pdfs-gui/src/app/pages/devices.rs:1022 +#: crates/pdfs-gui/src/app/pages/devices.rs:1032 +#: crates/pdfs-gui/src/app/pages/devices.rs:1044 msgid "Couldn't rename the computer" msgstr "Couldn't rename the computer" -#: crates/pdfs-gui/src/app/pages/devices.rs:1011 +#: crates/pdfs-gui/src/app/pages/devices.rs:1033 msgid "A name is required." msgstr "A name is required." -#: crates/pdfs-gui/src/app/pages/devices.rs:1021 +#: crates/pdfs-gui/src/app/pages/devices.rs:1043 msgid "Computer renamed" msgstr "Computer renamed" #. Translators: {name} is the computer's name, which the user must type. -#: crates/pdfs-gui/src/app/pages/devices.rs:1034 +#: crates/pdfs-gui/src/app/pages/devices.rs:1056 #, rust-format msgid "Type “{name}” to confirm" msgstr "Type “{name}” to confirm" -#: crates/pdfs-gui/src/app/pages/devices.rs:1039 +#: crates/pdfs-gui/src/app/pages/devices.rs:1061 msgid "Remove Computer?" msgstr "Remove Computer?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1042 +#: crates/pdfs-gui/src/app/pages/devices.rs:1064 #, rust-format msgid "" "Remove “{name}” from this account?\n" @@ -2486,26 +2497,26 @@ msgstr "" "on that computer itself are not touched — but this cannot be undone from " "here." -#: crates/pdfs-gui/src/app/pages/devices.rs:1048 +#: crates/pdfs-gui/src/app/pages/devices.rs:1070 #: crates/pdfs-gui/src/app/pages/shared.rs:1025 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:634 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:876 msgid "Remove" msgstr "Remove" -#: crates/pdfs-gui/src/app/pages/devices.rs:1065 +#: crates/pdfs-gui/src/app/pages/devices.rs:1087 msgid "Computer removed" msgstr "Computer removed" -#: crates/pdfs-gui/src/app/pages/devices.rs:1066 +#: crates/pdfs-gui/src/app/pages/devices.rs:1088 msgid "Couldn't remove the computer" msgstr "Couldn't remove the computer" -#: crates/pdfs-gui/src/app/pages/devices.rs:1084 +#: crates/pdfs-gui/src/app/pages/devices.rs:1106 msgid "Continue This Backup Here?" msgstr "Continue This Backup Here?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1087 +#: crates/pdfs-gui/src/app/pages/devices.rs:1109 #, rust-format msgid "" "This computer takes over the backup of “{name}”, for example after a " @@ -2524,25 +2535,25 @@ msgstr "" "“Restore to This Computer” afterwards to bring back what “{name}” was " "syncing." -#: crates/pdfs-gui/src/app/pages/devices.rs:1092 +#: crates/pdfs-gui/src/app/pages/devices.rs:1114 msgid "Continue Here" msgstr "Continue Here" #. Translators: {name} is the name of the computer whose backup this one took over. -#: crates/pdfs-gui/src/app/pages/devices.rs:1099 +#: crates/pdfs-gui/src/app/pages/devices.rs:1121 #, rust-format msgid "Now backing up as “{name}”" msgstr "Now backing up as “{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1108 +#: crates/pdfs-gui/src/app/pages/devices.rs:1130 msgid "Couldn't continue that backup here" msgstr "Couldn't continue that backup here" -#: crates/pdfs-gui/src/app/pages/devices.rs:1124 +#: crates/pdfs-gui/src/app/pages/devices.rs:1146 msgid "Sync With This Computer?" msgstr "Sync With This Computer?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1127 +#: crates/pdfs-gui/src/app/pages/devices.rs:1149 #, rust-format msgid "" "This computer and “{name}” keep the same folders in step. A change or a " @@ -2559,25 +2570,25 @@ msgstr "" "Folders already synced here stay where they are. Next you choose which of " "its folders to sync here." -#: crates/pdfs-gui/src/app/pages/devices.rs:1132 +#: crates/pdfs-gui/src/app/pages/devices.rs:1154 msgid "Sync Here" msgstr "Sync Here" #. Translators: {name} is the name of the other computer this one now syncs with. -#: crates/pdfs-gui/src/app/pages/devices.rs:1139 +#: crates/pdfs-gui/src/app/pages/devices.rs:1161 #, rust-format msgid "Now syncing with “{name}”" msgstr "Now syncing with “{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1155 +#: crates/pdfs-gui/src/app/pages/devices.rs:1177 msgid "Couldn't sync with that computer" msgstr "Couldn't sync with that computer" -#: crates/pdfs-gui/src/app/pages/devices.rs:1187 +#: crates/pdfs-gui/src/app/pages/devices.rs:1209 msgid "Move Backup to This Computer?" msgstr "Move Backup to This Computer?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1190 +#: crates/pdfs-gui/src/app/pages/devices.rs:1212 #, rust-format msgid "" "The folders backed up by “{name}” move into this computer's backup in Proton " @@ -2593,12 +2604,12 @@ msgstr "" "Stop Proton Drive on “{name}” first. Otherwise it sees its folders disappear." #. Translators: {name} is the name of the computer whose folders moved to this one. -#: crates/pdfs-gui/src/app/pages/devices.rs:1202 +#: crates/pdfs-gui/src/app/pages/devices.rs:1224 #, rust-format msgid "Moved the backup of “{name}”" msgstr "Moved the backup of “{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1216 +#: crates/pdfs-gui/src/app/pages/devices.rs:1238 msgid "Couldn't move that backup" msgstr "Couldn't move that backup" @@ -3321,7 +3332,7 @@ msgid "paused" msgstr "paused" #. Translators: a folder's files can't be opened right now. -#: crates/pdfs-gui/src/app/pages/locations.rs:1742 +#: crates/pdfs-gui/src/app/pages/locations.rs:1745 msgid "not available" msgstr "not available" diff --git a/po/es.po b/po/es.po index 9ecbbaf..21ed428 100644 --- a/po/es.po +++ b/po/es.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:26+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Spanish\n" @@ -247,13 +247,13 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:1917 #: crates/pdfs-gui/src/app/pages/browser.rs:2039 #: crates/pdfs-gui/src/app/pages/browser.rs:2540 -#: crates/pdfs-gui/src/app/pages/devices.rs:804 -#: crates/pdfs-gui/src/app/pages/devices.rs:957 -#: crates/pdfs-gui/src/app/pages/devices.rs:995 -#: crates/pdfs-gui/src/app/pages/devices.rs:1047 -#: crates/pdfs-gui/src/app/pages/devices.rs:1091 -#: crates/pdfs-gui/src/app/pages/devices.rs:1131 -#: crates/pdfs-gui/src/app/pages/devices.rs:1194 +#: crates/pdfs-gui/src/app/pages/devices.rs:826 +#: crates/pdfs-gui/src/app/pages/devices.rs:979 +#: crates/pdfs-gui/src/app/pages/devices.rs:1017 +#: crates/pdfs-gui/src/app/pages/devices.rs:1069 +#: crates/pdfs-gui/src/app/pages/devices.rs:1113 +#: crates/pdfs-gui/src/app/pages/devices.rs:1153 +#: crates/pdfs-gui/src/app/pages/devices.rs:1216 #: crates/pdfs-gui/src/app/pages/locations.rs:850 #: crates/pdfs-gui/src/app/pages/locations.rs:1054 #: crates/pdfs-gui/src/app/pages/locations.rs:1537 @@ -348,7 +348,7 @@ msgstr "Cuadrícula / lista" #: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/browser.rs:1896 #: crates/pdfs-gui/src/app/pages/browser.rs:1918 -#: crates/pdfs-gui/src/app/pages/devices.rs:996 +#: crates/pdfs-gui/src/app/pages/devices.rs:1018 #: crates/pdfs-gui/src/app/widgets/details.rs:161 msgid "Rename" msgstr "Renombrar" @@ -679,7 +679,7 @@ msgid "Couldn't add a sync folder" msgstr "No se pudo añadir una carpeta sincronizada" #: crates/pdfs-gui/src/app/pages/activity_text.rs:58 -#: crates/pdfs-gui/src/app/pages/devices.rs:839 +#: crates/pdfs-gui/src/app/pages/devices.rs:861 msgid "Couldn't restore folders" msgstr "No se pudieron restaurar las carpetas" @@ -1268,12 +1268,12 @@ msgstr "No se pudieron cargar los álbumes" #: crates/pdfs-gui/src/app/pages/browser.rs:1873 #: crates/pdfs-gui/src/app/pages/browser.rs:2500 #: crates/pdfs-gui/src/app/pages/browser.rs:2650 -#: crates/pdfs-gui/src/app/pages/devices.rs:707 -#: crates/pdfs-gui/src/app/pages/devices.rs:747 -#: crates/pdfs-gui/src/app/pages/devices.rs:900 -#: crates/pdfs-gui/src/app/pages/devices.rs:1170 -#: crates/pdfs-gui/src/app/pages/devices.rs:1231 -#: crates/pdfs-gui/src/app/pages/devices.rs:1260 +#: crates/pdfs-gui/src/app/pages/devices.rs:729 +#: crates/pdfs-gui/src/app/pages/devices.rs:769 +#: crates/pdfs-gui/src/app/pages/devices.rs:922 +#: crates/pdfs-gui/src/app/pages/devices.rs:1192 +#: crates/pdfs-gui/src/app/pages/devices.rs:1253 +#: crates/pdfs-gui/src/app/pages/devices.rs:1282 #: crates/pdfs-gui/src/app/pages/duplicates.rs:221 #: crates/pdfs-gui/src/app/pages/locations.rs:577 #: crates/pdfs-gui/src/app/pages/locations.rs:824 @@ -1921,7 +1921,7 @@ msgid "{status}. {message}" msgstr "{status}. {message}" #: crates/pdfs-gui/src/app/pages/browser.rs:1900 -#: crates/pdfs-gui/src/app/pages/devices.rs:989 +#: crates/pdfs-gui/src/app/pages/devices.rs:1011 msgid "New name" msgstr "Nombre nuevo" @@ -1946,7 +1946,7 @@ msgstr "Elija la carpeta de destino." #: crates/pdfs-gui/src/app/pages/browser.rs:1989 #: crates/pdfs-gui/src/app/pages/browser.rs:1994 -#: crates/pdfs-gui/src/app/pages/devices.rs:1195 +#: crates/pdfs-gui/src/app/pages/devices.rs:1217 msgid "Move Here" msgstr "Mover aquí" @@ -2268,76 +2268,87 @@ msgid_plural "{n} folders backed up" msgstr[0] "{n} carpeta con copia de seguridad" msgstr[1] "{n} carpetas con copia de seguridad" -#: crates/pdfs-gui/src/app/pages/devices.rs:608 +#: crates/pdfs-gui/src/app/pages/devices.rs:607 +#, rust-format +msgid "{folders} · {n} waiting for its first sync" +msgid_plural "{folders} · {n} waiting for their first sync" +msgstr[0] "{folders} · {n} espera su primera sincronización" +msgstr[1] "{folders} · {n} esperan su primera sincronización" + +#: crates/pdfs-gui/src/app/pages/devices.rs:621 #, rust-format msgid "{folders} · {n} needs attention" msgid_plural "{folders} · {n} need attention" msgstr[0] "{folders} · {n} requiere atención" msgstr[1] "{folders} · {n} requieren atención" -#: crates/pdfs-gui/src/app/pages/devices.rs:619 +#: crates/pdfs-gui/src/app/pages/devices.rs:640 msgid "syncing…" msgstr "sincronizando…" -#: crates/pdfs-gui/src/app/pages/devices.rs:620 +#: crates/pdfs-gui/src/app/pages/devices.rs:641 msgid "sync error" msgstr "error de sincronización" -#: crates/pdfs-gui/src/app/pages/devices.rs:621 +#: crates/pdfs-gui/src/app/pages/devices.rs:642 msgid "needs attention" msgstr "requiere atención" -#: crates/pdfs-gui/src/app/pages/devices.rs:622 +#: crates/pdfs-gui/src/app/pages/devices.rs:643 +msgid "waiting for first sync" +msgstr "esperando la primera sincronización" + +#: crates/pdfs-gui/src/app/pages/devices.rs:644 msgid "up to date" msgstr "actualizado" -#: crates/pdfs-gui/src/app/pages/devices.rs:632 +#: crates/pdfs-gui/src/app/pages/devices.rs:654 msgid "checking for changes…" msgstr "buscando cambios…" -#: crates/pdfs-gui/src/app/pages/devices.rs:638 +#: crates/pdfs-gui/src/app/pages/devices.rs:660 #, rust-format msgid "checking for changes — {n} item checked" msgid_plural "checking for changes — {n} items checked" msgstr[0] "buscando cambios: {n} elemento revisado" msgstr[1] "buscando cambios: {n} elementos revisados" -#: crates/pdfs-gui/src/app/pages/devices.rs:645 +#: crates/pdfs-gui/src/app/pages/devices.rs:667 #, rust-format msgid "checking for changes — {done} of {total}" msgstr "buscando cambios: {done} de {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:657 +#: crates/pdfs-gui/src/app/pages/devices.rs:679 #, rust-format msgid "syncing {done} of {total}" msgstr "sincronizando {done} de {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:663 +#: crates/pdfs-gui/src/app/pages/devices.rs:685 #, rust-format msgid "syncing {file} — {done} of {total}" msgstr "sincronizando {file}: {done} de {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:675 +#: crates/pdfs-gui/src/app/pages/devices.rs:697 msgid "Add Folder to Sync" msgstr "Añadir carpeta para sincronizar" -#: crates/pdfs-gui/src/app/pages/devices.rs:699 +#: crates/pdfs-gui/src/app/pages/devices.rs:721 #: crates/pdfs-gui/src/app/pages/locations.rs:1559 #: crates/pdfs-gui/src/app/pages/locations.rs:1686 msgid "Syncing folder…" msgstr "Sincronizando la carpeta…" -#: crates/pdfs-gui/src/app/pages/devices.rs:702 -#: crates/pdfs-gui/src/app/pages/devices.rs:706 +#: crates/pdfs-gui/src/app/pages/devices.rs:724 +#: crates/pdfs-gui/src/app/pages/devices.rs:728 msgid "Couldn't add folder" msgstr "No se pudo añadir la carpeta" -#: crates/pdfs-gui/src/app/pages/devices.rs:740 -#: crates/pdfs-gui/src/app/pages/devices.rs:746 +#: crates/pdfs-gui/src/app/pages/devices.rs:762 +#: crates/pdfs-gui/src/app/pages/devices.rs:768 msgid "Couldn't list folders to restore" msgstr "No se pudieron listar las carpetas para restaurar" -#: crates/pdfs-gui/src/app/pages/devices.rs:772 +#: crates/pdfs-gui/src/app/pages/devices.rs:794 msgid "" "Tick the folders to sync to this machine, and adjust where each one should " "live." @@ -2345,19 +2356,19 @@ msgstr "" "Marque las carpetas que sincronizar con esta máquina y ajuste dónde debe " "estar cada una." -#: crates/pdfs-gui/src/app/pages/devices.rs:798 +#: crates/pdfs-gui/src/app/pages/devices.rs:820 msgid "Restore Folders" msgstr "Restaurar carpetas" -#: crates/pdfs-gui/src/app/pages/devices.rs:805 +#: crates/pdfs-gui/src/app/pages/devices.rs:827 msgid "Restore" msgstr "Restaurar" -#: crates/pdfs-gui/src/app/pages/devices.rs:838 +#: crates/pdfs-gui/src/app/pages/devices.rs:860 msgid "Restoring folders…" msgstr "Restaurando carpetas…" -#: crates/pdfs-gui/src/app/pages/devices.rs:850 +#: crates/pdfs-gui/src/app/pages/devices.rs:872 #, rust-format msgid "" "These folders are backed up from “{name}”. Restored folders keep syncing " @@ -2366,36 +2377,36 @@ msgstr "" "Estas carpetas tienen copia de seguridad desde “{name}”. Las carpetas " "restauradas siguen sincronizándose con la copia de ese equipo." -#: crates/pdfs-gui/src/app/pages/devices.rs:853 +#: crates/pdfs-gui/src/app/pages/devices.rs:875 msgid "These folders are backed up under this computer in Proton Drive." msgstr "" "Estas carpetas tienen copia de seguridad bajo este equipo en Proton Drive." -#: crates/pdfs-gui/src/app/pages/devices.rs:862 +#: crates/pdfs-gui/src/app/pages/devices.rs:884 #, rust-format msgid "Nothing to restore — “{name}”'s folders are all synced here." msgstr "" "Nada que restaurar: todas las carpetas de “{name}” ya se sincronizan aquí." -#: crates/pdfs-gui/src/app/pages/devices.rs:865 +#: crates/pdfs-gui/src/app/pages/devices.rs:887 msgid "Nothing to restore — this computer's folders are all synced here." msgstr "" "Nada que restaurar: todas las carpetas de este equipo ya se sincronizan aquí." -#: crates/pdfs-gui/src/app/pages/devices.rs:889 +#: crates/pdfs-gui/src/app/pages/devices.rs:911 msgid "Folder will switch to online only" msgstr "La carpeta pasará a solo en línea" -#: crates/pdfs-gui/src/app/pages/devices.rs:891 +#: crates/pdfs-gui/src/app/pages/devices.rs:913 msgid "Folder will download and stay synced" msgstr "La carpeta se descargará y permanecerá sincronizada" -#: crates/pdfs-gui/src/app/pages/devices.rs:895 -#: crates/pdfs-gui/src/app/pages/devices.rs:899 +#: crates/pdfs-gui/src/app/pages/devices.rs:917 +#: crates/pdfs-gui/src/app/pages/devices.rs:921 msgid "Couldn't change mode" msgstr "No se pudo cambiar el modo" -#: crates/pdfs-gui/src/app/pages/devices.rs:931 +#: crates/pdfs-gui/src/app/pages/devices.rs:953 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2412,7 +2423,7 @@ msgstr "" "Para conservar una copia local, cancele, cambie la carpeta a Sincronizada y " "espere a que termine la descarga antes de quitarla." -#: crates/pdfs-gui/src/app/pages/devices.rs:937 +#: crates/pdfs-gui/src/app/pages/devices.rs:959 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2425,68 +2436,68 @@ msgstr "" "Los archivos locales permanecen en este disco y simplemente dejan de " "sincronizarse. Elija si quiere eliminar también la copia en Proton Drive." -#: crates/pdfs-gui/src/app/pages/devices.rs:942 +#: crates/pdfs-gui/src/app/pages/devices.rs:964 msgid "Stop Syncing Folder?" msgstr "¿Dejar de sincronizar la carpeta?" -#: crates/pdfs-gui/src/app/pages/devices.rs:947 +#: crates/pdfs-gui/src/app/pages/devices.rs:969 msgid "Also delete from Proton Drive" msgstr "Eliminar también de Proton Drive" -#: crates/pdfs-gui/src/app/pages/devices.rs:949 +#: crates/pdfs-gui/src/app/pages/devices.rs:971 msgid "Deletes the only copy of these files." msgstr "Elimina la única copia de estos archivos." -#: crates/pdfs-gui/src/app/pages/devices.rs:951 +#: crates/pdfs-gui/src/app/pages/devices.rs:973 msgid "The local copy is unaffected." msgstr "La copia local no se ve afectada." -#: crates/pdfs-gui/src/app/pages/devices.rs:958 +#: crates/pdfs-gui/src/app/pages/devices.rs:980 msgid "Stop Syncing" msgstr "Dejar de sincronizar" -#: crates/pdfs-gui/src/app/pages/devices.rs:971 +#: crates/pdfs-gui/src/app/pages/devices.rs:993 msgid "Stopped syncing folder" msgstr "Se dejó de sincronizar la carpeta" -#: crates/pdfs-gui/src/app/pages/devices.rs:972 +#: crates/pdfs-gui/src/app/pages/devices.rs:994 msgid "Couldn't stop syncing the folder" msgstr "No se pudo dejar de sincronizar la carpeta" -#: crates/pdfs-gui/src/app/pages/devices.rs:983 +#: crates/pdfs-gui/src/app/pages/devices.rs:1005 msgid "Rename Computer" msgstr "Renombrar equipo" #. Translators: {name} is the computer's current name. -#: crates/pdfs-gui/src/app/pages/devices.rs:985 +#: crates/pdfs-gui/src/app/pages/devices.rs:1007 #, rust-format msgid "Rename “{name}”." msgstr "Renombrar “{name}”." -#: crates/pdfs-gui/src/app/pages/devices.rs:1010 -#: crates/pdfs-gui/src/app/pages/devices.rs:1022 +#: crates/pdfs-gui/src/app/pages/devices.rs:1032 +#: crates/pdfs-gui/src/app/pages/devices.rs:1044 msgid "Couldn't rename the computer" msgstr "No se pudo renombrar el equipo" -#: crates/pdfs-gui/src/app/pages/devices.rs:1011 +#: crates/pdfs-gui/src/app/pages/devices.rs:1033 msgid "A name is required." msgstr "Se necesita un nombre." -#: crates/pdfs-gui/src/app/pages/devices.rs:1021 +#: crates/pdfs-gui/src/app/pages/devices.rs:1043 msgid "Computer renamed" msgstr "Equipo renombrado" #. Translators: {name} is the computer's name, which the user must type. -#: crates/pdfs-gui/src/app/pages/devices.rs:1034 +#: crates/pdfs-gui/src/app/pages/devices.rs:1056 #, rust-format msgid "Type “{name}” to confirm" msgstr "Escriba “{name}” para confirmar" -#: crates/pdfs-gui/src/app/pages/devices.rs:1039 +#: crates/pdfs-gui/src/app/pages/devices.rs:1061 msgid "Remove Computer?" msgstr "¿Quitar el equipo?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1042 +#: crates/pdfs-gui/src/app/pages/devices.rs:1064 #, rust-format msgid "" "Remove “{name}” from this account?\n" @@ -2500,26 +2511,26 @@ msgstr "" "Todo lo que respaldó en Proton Drive se eliminará con él. Los archivos de " "ese equipo no se tocan, pero esto no se puede deshacer desde aquí." -#: crates/pdfs-gui/src/app/pages/devices.rs:1048 +#: crates/pdfs-gui/src/app/pages/devices.rs:1070 #: crates/pdfs-gui/src/app/pages/shared.rs:1025 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:634 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:876 msgid "Remove" msgstr "Quitar" -#: crates/pdfs-gui/src/app/pages/devices.rs:1065 +#: crates/pdfs-gui/src/app/pages/devices.rs:1087 msgid "Computer removed" msgstr "Equipo quitado" -#: crates/pdfs-gui/src/app/pages/devices.rs:1066 +#: crates/pdfs-gui/src/app/pages/devices.rs:1088 msgid "Couldn't remove the computer" msgstr "No se pudo quitar el equipo" -#: crates/pdfs-gui/src/app/pages/devices.rs:1084 +#: crates/pdfs-gui/src/app/pages/devices.rs:1106 msgid "Continue This Backup Here?" msgstr "¿Continuar esta copia de seguridad aquí?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1087 +#: crates/pdfs-gui/src/app/pages/devices.rs:1109 #, rust-format msgid "" "This computer takes over the backup of “{name}”, for example after a " @@ -2538,25 +2549,25 @@ msgstr "" "no se mueven. Después, use “Restaurar en este equipo” para recuperar lo que " "“{name}” sincronizaba." -#: crates/pdfs-gui/src/app/pages/devices.rs:1092 +#: crates/pdfs-gui/src/app/pages/devices.rs:1114 msgid "Continue Here" msgstr "Continuar aquí" #. Translators: {name} is the name of the computer whose backup this one took over. -#: crates/pdfs-gui/src/app/pages/devices.rs:1099 +#: crates/pdfs-gui/src/app/pages/devices.rs:1121 #, rust-format msgid "Now backing up as “{name}”" msgstr "Ahora se hace copia de seguridad como “{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1108 +#: crates/pdfs-gui/src/app/pages/devices.rs:1130 msgid "Couldn't continue that backup here" msgstr "No se pudo continuar esa copia de seguridad aquí" -#: crates/pdfs-gui/src/app/pages/devices.rs:1124 +#: crates/pdfs-gui/src/app/pages/devices.rs:1146 msgid "Sync With This Computer?" msgstr "¿Sincronizar con este equipo?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1127 +#: crates/pdfs-gui/src/app/pages/devices.rs:1149 #, rust-format msgid "" "This computer and “{name}” keep the same folders in step. A change or a " @@ -2573,25 +2584,25 @@ msgstr "" "Proton Drive. Las carpetas ya sincronizadas aquí se quedan donde están. A " "continuación, elija qué carpetas suyas sincronizar aquí." -#: crates/pdfs-gui/src/app/pages/devices.rs:1132 +#: crates/pdfs-gui/src/app/pages/devices.rs:1154 msgid "Sync Here" msgstr "Sincronizar aquí" #. Translators: {name} is the name of the other computer this one now syncs with. -#: crates/pdfs-gui/src/app/pages/devices.rs:1139 +#: crates/pdfs-gui/src/app/pages/devices.rs:1161 #, rust-format msgid "Now syncing with “{name}”" msgstr "Ahora se sincroniza con “{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1155 +#: crates/pdfs-gui/src/app/pages/devices.rs:1177 msgid "Couldn't sync with that computer" msgstr "No se pudo sincronizar con ese equipo" -#: crates/pdfs-gui/src/app/pages/devices.rs:1187 +#: crates/pdfs-gui/src/app/pages/devices.rs:1209 msgid "Move Backup to This Computer?" msgstr "¿Mover la copia de seguridad a este equipo?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1190 +#: crates/pdfs-gui/src/app/pages/devices.rs:1212 #, rust-format msgid "" "The folders backed up by “{name}” move into this computer's backup in Proton " @@ -2608,12 +2619,12 @@ msgstr "" "carpetas." #. Translators: {name} is the name of the computer whose folders moved to this one. -#: crates/pdfs-gui/src/app/pages/devices.rs:1202 +#: crates/pdfs-gui/src/app/pages/devices.rs:1224 #, rust-format msgid "Moved the backup of “{name}”" msgstr "Se movió la copia de seguridad de “{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1216 +#: crates/pdfs-gui/src/app/pages/devices.rs:1238 msgid "Couldn't move that backup" msgstr "No se pudo mover esa copia de seguridad" @@ -3348,7 +3359,7 @@ msgid "paused" msgstr "en pausa" #. Translators: a folder's files can't be opened right now. -#: crates/pdfs-gui/src/app/pages/locations.rs:1742 +#: crates/pdfs-gui/src/app/pages/locations.rs:1745 msgid "not available" msgstr "no disponible" diff --git a/po/fr.po b/po/fr.po index 8a20308..3ff133c 100644 --- a/po/fr.po +++ b/po/fr.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:26+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: French\n" @@ -249,13 +249,13 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:1917 #: crates/pdfs-gui/src/app/pages/browser.rs:2039 #: crates/pdfs-gui/src/app/pages/browser.rs:2540 -#: crates/pdfs-gui/src/app/pages/devices.rs:804 -#: crates/pdfs-gui/src/app/pages/devices.rs:957 -#: crates/pdfs-gui/src/app/pages/devices.rs:995 -#: crates/pdfs-gui/src/app/pages/devices.rs:1047 -#: crates/pdfs-gui/src/app/pages/devices.rs:1091 -#: crates/pdfs-gui/src/app/pages/devices.rs:1131 -#: crates/pdfs-gui/src/app/pages/devices.rs:1194 +#: crates/pdfs-gui/src/app/pages/devices.rs:826 +#: crates/pdfs-gui/src/app/pages/devices.rs:979 +#: crates/pdfs-gui/src/app/pages/devices.rs:1017 +#: crates/pdfs-gui/src/app/pages/devices.rs:1069 +#: crates/pdfs-gui/src/app/pages/devices.rs:1113 +#: crates/pdfs-gui/src/app/pages/devices.rs:1153 +#: crates/pdfs-gui/src/app/pages/devices.rs:1216 #: crates/pdfs-gui/src/app/pages/locations.rs:850 #: crates/pdfs-gui/src/app/pages/locations.rs:1054 #: crates/pdfs-gui/src/app/pages/locations.rs:1537 @@ -350,7 +350,7 @@ msgstr "Grille / liste" #: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/browser.rs:1896 #: crates/pdfs-gui/src/app/pages/browser.rs:1918 -#: crates/pdfs-gui/src/app/pages/devices.rs:996 +#: crates/pdfs-gui/src/app/pages/devices.rs:1018 #: crates/pdfs-gui/src/app/widgets/details.rs:161 msgid "Rename" msgstr "Renommer" @@ -682,7 +682,7 @@ msgid "Couldn't add a sync folder" msgstr "Impossible d’ajouter un dossier synchronisé" #: crates/pdfs-gui/src/app/pages/activity_text.rs:58 -#: crates/pdfs-gui/src/app/pages/devices.rs:839 +#: crates/pdfs-gui/src/app/pages/devices.rs:861 msgid "Couldn't restore folders" msgstr "Impossible de restaurer les dossiers" @@ -1279,12 +1279,12 @@ msgstr "Impossible de charger les albums" #: crates/pdfs-gui/src/app/pages/browser.rs:1873 #: crates/pdfs-gui/src/app/pages/browser.rs:2500 #: crates/pdfs-gui/src/app/pages/browser.rs:2650 -#: crates/pdfs-gui/src/app/pages/devices.rs:707 -#: crates/pdfs-gui/src/app/pages/devices.rs:747 -#: crates/pdfs-gui/src/app/pages/devices.rs:900 -#: crates/pdfs-gui/src/app/pages/devices.rs:1170 -#: crates/pdfs-gui/src/app/pages/devices.rs:1231 -#: crates/pdfs-gui/src/app/pages/devices.rs:1260 +#: crates/pdfs-gui/src/app/pages/devices.rs:729 +#: crates/pdfs-gui/src/app/pages/devices.rs:769 +#: crates/pdfs-gui/src/app/pages/devices.rs:922 +#: crates/pdfs-gui/src/app/pages/devices.rs:1192 +#: crates/pdfs-gui/src/app/pages/devices.rs:1253 +#: crates/pdfs-gui/src/app/pages/devices.rs:1282 #: crates/pdfs-gui/src/app/pages/duplicates.rs:221 #: crates/pdfs-gui/src/app/pages/locations.rs:577 #: crates/pdfs-gui/src/app/pages/locations.rs:824 @@ -1933,7 +1933,7 @@ msgid "{status}. {message}" msgstr "{status}. {message}" #: crates/pdfs-gui/src/app/pages/browser.rs:1900 -#: crates/pdfs-gui/src/app/pages/devices.rs:989 +#: crates/pdfs-gui/src/app/pages/devices.rs:1011 msgid "New name" msgstr "Nouveau nom" @@ -1958,7 +1958,7 @@ msgstr "Choisissez le dossier de destination." #: crates/pdfs-gui/src/app/pages/browser.rs:1989 #: crates/pdfs-gui/src/app/pages/browser.rs:1994 -#: crates/pdfs-gui/src/app/pages/devices.rs:1195 +#: crates/pdfs-gui/src/app/pages/devices.rs:1217 msgid "Move Here" msgstr "Déplacer ici" @@ -2281,76 +2281,87 @@ msgid_plural "{n} folders backed up" msgstr[0] "{n} dossier sauvegardé" msgstr[1] "{n} dossiers sauvegardés" -#: crates/pdfs-gui/src/app/pages/devices.rs:608 +#: crates/pdfs-gui/src/app/pages/devices.rs:607 +#, rust-format +msgid "{folders} · {n} waiting for its first sync" +msgid_plural "{folders} · {n} waiting for their first sync" +msgstr[0] "{folders} · {n} attend sa première synchronisation" +msgstr[1] "{folders} · {n} attendent leur première synchronisation" + +#: crates/pdfs-gui/src/app/pages/devices.rs:621 #, rust-format msgid "{folders} · {n} needs attention" msgid_plural "{folders} · {n} need attention" msgstr[0] "{folders} · {n} requiert votre attention" msgstr[1] "{folders} · {n} requièrent votre attention" -#: crates/pdfs-gui/src/app/pages/devices.rs:619 +#: crates/pdfs-gui/src/app/pages/devices.rs:640 msgid "syncing…" msgstr "synchronisation…" -#: crates/pdfs-gui/src/app/pages/devices.rs:620 +#: crates/pdfs-gui/src/app/pages/devices.rs:641 msgid "sync error" msgstr "erreur de synchronisation" -#: crates/pdfs-gui/src/app/pages/devices.rs:621 +#: crates/pdfs-gui/src/app/pages/devices.rs:642 msgid "needs attention" msgstr "attention requise" -#: crates/pdfs-gui/src/app/pages/devices.rs:622 +#: crates/pdfs-gui/src/app/pages/devices.rs:643 +msgid "waiting for first sync" +msgstr "en attente de la première synchronisation" + +#: crates/pdfs-gui/src/app/pages/devices.rs:644 msgid "up to date" msgstr "à jour" -#: crates/pdfs-gui/src/app/pages/devices.rs:632 +#: crates/pdfs-gui/src/app/pages/devices.rs:654 msgid "checking for changes…" msgstr "recherche de modifications…" -#: crates/pdfs-gui/src/app/pages/devices.rs:638 +#: crates/pdfs-gui/src/app/pages/devices.rs:660 #, rust-format msgid "checking for changes — {n} item checked" msgid_plural "checking for changes — {n} items checked" msgstr[0] "recherche de modifications — {n} élément vérifié" msgstr[1] "recherche de modifications — {n} éléments vérifiés" -#: crates/pdfs-gui/src/app/pages/devices.rs:645 +#: crates/pdfs-gui/src/app/pages/devices.rs:667 #, rust-format msgid "checking for changes — {done} of {total}" msgstr "recherche de modifications — {done} sur {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:657 +#: crates/pdfs-gui/src/app/pages/devices.rs:679 #, rust-format msgid "syncing {done} of {total}" msgstr "synchronisation de {done} sur {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:663 +#: crates/pdfs-gui/src/app/pages/devices.rs:685 #, rust-format msgid "syncing {file} — {done} of {total}" msgstr "synchronisation de {file} — {done} sur {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:675 +#: crates/pdfs-gui/src/app/pages/devices.rs:697 msgid "Add Folder to Sync" msgstr "Ajouter un dossier à synchroniser" -#: crates/pdfs-gui/src/app/pages/devices.rs:699 +#: crates/pdfs-gui/src/app/pages/devices.rs:721 #: crates/pdfs-gui/src/app/pages/locations.rs:1559 #: crates/pdfs-gui/src/app/pages/locations.rs:1686 msgid "Syncing folder…" msgstr "Synchronisation du dossier…" -#: crates/pdfs-gui/src/app/pages/devices.rs:702 -#: crates/pdfs-gui/src/app/pages/devices.rs:706 +#: crates/pdfs-gui/src/app/pages/devices.rs:724 +#: crates/pdfs-gui/src/app/pages/devices.rs:728 msgid "Couldn't add folder" msgstr "Impossible d’ajouter le dossier" -#: crates/pdfs-gui/src/app/pages/devices.rs:740 -#: crates/pdfs-gui/src/app/pages/devices.rs:746 +#: crates/pdfs-gui/src/app/pages/devices.rs:762 +#: crates/pdfs-gui/src/app/pages/devices.rs:768 msgid "Couldn't list folders to restore" msgstr "Impossible de lister les dossiers à restaurer" -#: crates/pdfs-gui/src/app/pages/devices.rs:772 +#: crates/pdfs-gui/src/app/pages/devices.rs:794 msgid "" "Tick the folders to sync to this machine, and adjust where each one should " "live." @@ -2358,19 +2369,19 @@ msgstr "" "Cochez les dossiers à synchroniser sur cette machine et ajustez " "l’emplacement de chacun." -#: crates/pdfs-gui/src/app/pages/devices.rs:798 +#: crates/pdfs-gui/src/app/pages/devices.rs:820 msgid "Restore Folders" msgstr "Restaurer les dossiers" -#: crates/pdfs-gui/src/app/pages/devices.rs:805 +#: crates/pdfs-gui/src/app/pages/devices.rs:827 msgid "Restore" msgstr "Restaurer" -#: crates/pdfs-gui/src/app/pages/devices.rs:838 +#: crates/pdfs-gui/src/app/pages/devices.rs:860 msgid "Restoring folders…" msgstr "Restauration des dossiers…" -#: crates/pdfs-gui/src/app/pages/devices.rs:850 +#: crates/pdfs-gui/src/app/pages/devices.rs:872 #, rust-format msgid "" "These folders are backed up from “{name}”. Restored folders keep syncing " @@ -2379,35 +2390,35 @@ msgstr "" "Ces dossiers sont sauvegardés depuis « {name} ». Les dossiers restaurés " "restent synchronisés avec la copie de cet ordinateur." -#: crates/pdfs-gui/src/app/pages/devices.rs:853 +#: crates/pdfs-gui/src/app/pages/devices.rs:875 msgid "These folders are backed up under this computer in Proton Drive." msgstr "Ces dossiers sont sauvegardés sous cet ordinateur dans Proton Drive." -#: crates/pdfs-gui/src/app/pages/devices.rs:862 +#: crates/pdfs-gui/src/app/pages/devices.rs:884 #, rust-format msgid "Nothing to restore — “{name}”'s folders are all synced here." msgstr "" "Rien à restaurer — les dossiers de « {name} » sont tous synchronisés ici." -#: crates/pdfs-gui/src/app/pages/devices.rs:865 +#: crates/pdfs-gui/src/app/pages/devices.rs:887 msgid "Nothing to restore — this computer's folders are all synced here." msgstr "" "Rien à restaurer — les dossiers de cet ordinateur sont tous synchronisés ici." -#: crates/pdfs-gui/src/app/pages/devices.rs:889 +#: crates/pdfs-gui/src/app/pages/devices.rs:911 msgid "Folder will switch to online only" msgstr "Le dossier passera en ligne uniquement" -#: crates/pdfs-gui/src/app/pages/devices.rs:891 +#: crates/pdfs-gui/src/app/pages/devices.rs:913 msgid "Folder will download and stay synced" msgstr "Le dossier va être téléchargé et rester synchronisé" -#: crates/pdfs-gui/src/app/pages/devices.rs:895 -#: crates/pdfs-gui/src/app/pages/devices.rs:899 +#: crates/pdfs-gui/src/app/pages/devices.rs:917 +#: crates/pdfs-gui/src/app/pages/devices.rs:921 msgid "Couldn't change mode" msgstr "Impossible de changer de mode" -#: crates/pdfs-gui/src/app/pages/devices.rs:931 +#: crates/pdfs-gui/src/app/pages/devices.rs:953 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2424,7 +2435,7 @@ msgstr "" "arrêtée. Pour conserver une copie locale, annulez, passez le dossier en " "« Synchronisé » et attendez la fin du téléchargement avant de le retirer." -#: crates/pdfs-gui/src/app/pages/devices.rs:937 +#: crates/pdfs-gui/src/app/pages/devices.rs:959 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2438,68 +2449,68 @@ msgstr "" "synchronisés. Choisissez s’il faut aussi supprimer la copie dans Proton " "Drive." -#: crates/pdfs-gui/src/app/pages/devices.rs:942 +#: crates/pdfs-gui/src/app/pages/devices.rs:964 msgid "Stop Syncing Folder?" msgstr "Arrêter la synchronisation du dossier ?" -#: crates/pdfs-gui/src/app/pages/devices.rs:947 +#: crates/pdfs-gui/src/app/pages/devices.rs:969 msgid "Also delete from Proton Drive" msgstr "Supprimer aussi de Proton Drive" -#: crates/pdfs-gui/src/app/pages/devices.rs:949 +#: crates/pdfs-gui/src/app/pages/devices.rs:971 msgid "Deletes the only copy of these files." msgstr "Supprime l’unique copie de ces fichiers." -#: crates/pdfs-gui/src/app/pages/devices.rs:951 +#: crates/pdfs-gui/src/app/pages/devices.rs:973 msgid "The local copy is unaffected." msgstr "La copie locale n’est pas affectée." -#: crates/pdfs-gui/src/app/pages/devices.rs:958 +#: crates/pdfs-gui/src/app/pages/devices.rs:980 msgid "Stop Syncing" msgstr "Arrêter la synchronisation" -#: crates/pdfs-gui/src/app/pages/devices.rs:971 +#: crates/pdfs-gui/src/app/pages/devices.rs:993 msgid "Stopped syncing folder" msgstr "Synchronisation du dossier arrêtée" -#: crates/pdfs-gui/src/app/pages/devices.rs:972 +#: crates/pdfs-gui/src/app/pages/devices.rs:994 msgid "Couldn't stop syncing the folder" msgstr "Impossible d’arrêter la synchronisation du dossier" -#: crates/pdfs-gui/src/app/pages/devices.rs:983 +#: crates/pdfs-gui/src/app/pages/devices.rs:1005 msgid "Rename Computer" msgstr "Renommer l’ordinateur" #. Translators: {name} is the computer's current name. -#: crates/pdfs-gui/src/app/pages/devices.rs:985 +#: crates/pdfs-gui/src/app/pages/devices.rs:1007 #, rust-format msgid "Rename “{name}”." msgstr "Renommer « {name} »." -#: crates/pdfs-gui/src/app/pages/devices.rs:1010 -#: crates/pdfs-gui/src/app/pages/devices.rs:1022 +#: crates/pdfs-gui/src/app/pages/devices.rs:1032 +#: crates/pdfs-gui/src/app/pages/devices.rs:1044 msgid "Couldn't rename the computer" msgstr "Impossible de renommer l’ordinateur" -#: crates/pdfs-gui/src/app/pages/devices.rs:1011 +#: crates/pdfs-gui/src/app/pages/devices.rs:1033 msgid "A name is required." msgstr "Un nom est requis." -#: crates/pdfs-gui/src/app/pages/devices.rs:1021 +#: crates/pdfs-gui/src/app/pages/devices.rs:1043 msgid "Computer renamed" msgstr "Ordinateur renommé" #. Translators: {name} is the computer's name, which the user must type. -#: crates/pdfs-gui/src/app/pages/devices.rs:1034 +#: crates/pdfs-gui/src/app/pages/devices.rs:1056 #, rust-format msgid "Type “{name}” to confirm" msgstr "Saisissez « {name} » pour confirmer" -#: crates/pdfs-gui/src/app/pages/devices.rs:1039 +#: crates/pdfs-gui/src/app/pages/devices.rs:1061 msgid "Remove Computer?" msgstr "Retirer l’ordinateur ?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1042 +#: crates/pdfs-gui/src/app/pages/devices.rs:1064 #, rust-format msgid "" "Remove “{name}” from this account?\n" @@ -2514,26 +2525,26 @@ msgstr "" "fichiers présents sur cet ordinateur ne sont pas touchés — mais cette action " "ne peut pas être annulée d’ici." -#: crates/pdfs-gui/src/app/pages/devices.rs:1048 +#: crates/pdfs-gui/src/app/pages/devices.rs:1070 #: crates/pdfs-gui/src/app/pages/shared.rs:1025 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:634 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:876 msgid "Remove" msgstr "Retirer" -#: crates/pdfs-gui/src/app/pages/devices.rs:1065 +#: crates/pdfs-gui/src/app/pages/devices.rs:1087 msgid "Computer removed" msgstr "Ordinateur retiré" -#: crates/pdfs-gui/src/app/pages/devices.rs:1066 +#: crates/pdfs-gui/src/app/pages/devices.rs:1088 msgid "Couldn't remove the computer" msgstr "Impossible de retirer l’ordinateur" -#: crates/pdfs-gui/src/app/pages/devices.rs:1084 +#: crates/pdfs-gui/src/app/pages/devices.rs:1106 msgid "Continue This Backup Here?" msgstr "Continuer cette sauvegarde ici ?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1087 +#: crates/pdfs-gui/src/app/pages/devices.rs:1109 #, rust-format msgid "" "This computer takes over the backup of “{name}”, for example after a " @@ -2552,25 +2563,25 @@ msgstr "" "ne sont pas déplacés. Utilisez ensuite « Restaurer sur cet ordinateur » pour " "récupérer ce que « {name} » synchronisait." -#: crates/pdfs-gui/src/app/pages/devices.rs:1092 +#: crates/pdfs-gui/src/app/pages/devices.rs:1114 msgid "Continue Here" msgstr "Continuer ici" #. Translators: {name} is the name of the computer whose backup this one took over. -#: crates/pdfs-gui/src/app/pages/devices.rs:1099 +#: crates/pdfs-gui/src/app/pages/devices.rs:1121 #, rust-format msgid "Now backing up as “{name}”" msgstr "Sauvegarde désormais en tant que « {name} »" -#: crates/pdfs-gui/src/app/pages/devices.rs:1108 +#: crates/pdfs-gui/src/app/pages/devices.rs:1130 msgid "Couldn't continue that backup here" msgstr "Impossible de continuer cette sauvegarde ici" -#: crates/pdfs-gui/src/app/pages/devices.rs:1124 +#: crates/pdfs-gui/src/app/pages/devices.rs:1146 msgid "Sync With This Computer?" msgstr "Synchroniser avec cet ordinateur ?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1127 +#: crates/pdfs-gui/src/app/pages/devices.rs:1149 #, rust-format msgid "" "This computer and “{name}” keep the same folders in step. A change or a " @@ -2587,25 +2598,25 @@ msgstr "" "dans Proton Drive. Les dossiers déjà synchronisés ici restent où ils sont. " "Vous choisissez ensuite lesquels de ses dossiers synchroniser ici." -#: crates/pdfs-gui/src/app/pages/devices.rs:1132 +#: crates/pdfs-gui/src/app/pages/devices.rs:1154 msgid "Sync Here" msgstr "Synchroniser ici" #. Translators: {name} is the name of the other computer this one now syncs with. -#: crates/pdfs-gui/src/app/pages/devices.rs:1139 +#: crates/pdfs-gui/src/app/pages/devices.rs:1161 #, rust-format msgid "Now syncing with “{name}”" msgstr "Synchronisé désormais avec « {name} »" -#: crates/pdfs-gui/src/app/pages/devices.rs:1155 +#: crates/pdfs-gui/src/app/pages/devices.rs:1177 msgid "Couldn't sync with that computer" msgstr "Impossible de synchroniser avec cet ordinateur" -#: crates/pdfs-gui/src/app/pages/devices.rs:1187 +#: crates/pdfs-gui/src/app/pages/devices.rs:1209 msgid "Move Backup to This Computer?" msgstr "Déplacer la sauvegarde vers cet ordinateur ?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1190 +#: crates/pdfs-gui/src/app/pages/devices.rs:1212 #, rust-format msgid "" "The folders backed up by “{name}” move into this computer's backup in Proton " @@ -2622,12 +2633,12 @@ msgstr "" "disparaître." #. Translators: {name} is the name of the computer whose folders moved to this one. -#: crates/pdfs-gui/src/app/pages/devices.rs:1202 +#: crates/pdfs-gui/src/app/pages/devices.rs:1224 #, rust-format msgid "Moved the backup of “{name}”" msgstr "Sauvegarde de « {name} » déplacée" -#: crates/pdfs-gui/src/app/pages/devices.rs:1216 +#: crates/pdfs-gui/src/app/pages/devices.rs:1238 msgid "Couldn't move that backup" msgstr "Impossible de déplacer cette sauvegarde" @@ -3374,7 +3385,7 @@ msgid "paused" msgstr "en pause" #. Translators: a folder's files can't be opened right now. -#: crates/pdfs-gui/src/app/pages/locations.rs:1742 +#: crates/pdfs-gui/src/app/pages/locations.rs:1745 msgid "not available" msgstr "non disponible" diff --git a/po/it.po b/po/it.po index c035843..32bff17 100644 --- a/po/it.po +++ b/po/it.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:26+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Italian\n" @@ -245,13 +245,13 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:1917 #: crates/pdfs-gui/src/app/pages/browser.rs:2039 #: crates/pdfs-gui/src/app/pages/browser.rs:2540 -#: crates/pdfs-gui/src/app/pages/devices.rs:804 -#: crates/pdfs-gui/src/app/pages/devices.rs:957 -#: crates/pdfs-gui/src/app/pages/devices.rs:995 -#: crates/pdfs-gui/src/app/pages/devices.rs:1047 -#: crates/pdfs-gui/src/app/pages/devices.rs:1091 -#: crates/pdfs-gui/src/app/pages/devices.rs:1131 -#: crates/pdfs-gui/src/app/pages/devices.rs:1194 +#: crates/pdfs-gui/src/app/pages/devices.rs:826 +#: crates/pdfs-gui/src/app/pages/devices.rs:979 +#: crates/pdfs-gui/src/app/pages/devices.rs:1017 +#: crates/pdfs-gui/src/app/pages/devices.rs:1069 +#: crates/pdfs-gui/src/app/pages/devices.rs:1113 +#: crates/pdfs-gui/src/app/pages/devices.rs:1153 +#: crates/pdfs-gui/src/app/pages/devices.rs:1216 #: crates/pdfs-gui/src/app/pages/locations.rs:850 #: crates/pdfs-gui/src/app/pages/locations.rs:1054 #: crates/pdfs-gui/src/app/pages/locations.rs:1537 @@ -346,7 +346,7 @@ msgstr "Griglia / elenco" #: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/browser.rs:1896 #: crates/pdfs-gui/src/app/pages/browser.rs:1918 -#: crates/pdfs-gui/src/app/pages/devices.rs:996 +#: crates/pdfs-gui/src/app/pages/devices.rs:1018 #: crates/pdfs-gui/src/app/widgets/details.rs:161 msgid "Rename" msgstr "Rinomina" @@ -678,7 +678,7 @@ msgid "Couldn't add a sync folder" msgstr "Impossibile aggiungere una cartella sincronizzata" #: crates/pdfs-gui/src/app/pages/activity_text.rs:58 -#: crates/pdfs-gui/src/app/pages/devices.rs:839 +#: crates/pdfs-gui/src/app/pages/devices.rs:861 msgid "Couldn't restore folders" msgstr "Impossibile ripristinare le cartelle" @@ -1273,12 +1273,12 @@ msgstr "Impossibile caricare gli album" #: crates/pdfs-gui/src/app/pages/browser.rs:1873 #: crates/pdfs-gui/src/app/pages/browser.rs:2500 #: crates/pdfs-gui/src/app/pages/browser.rs:2650 -#: crates/pdfs-gui/src/app/pages/devices.rs:707 -#: crates/pdfs-gui/src/app/pages/devices.rs:747 -#: crates/pdfs-gui/src/app/pages/devices.rs:900 -#: crates/pdfs-gui/src/app/pages/devices.rs:1170 -#: crates/pdfs-gui/src/app/pages/devices.rs:1231 -#: crates/pdfs-gui/src/app/pages/devices.rs:1260 +#: crates/pdfs-gui/src/app/pages/devices.rs:729 +#: crates/pdfs-gui/src/app/pages/devices.rs:769 +#: crates/pdfs-gui/src/app/pages/devices.rs:922 +#: crates/pdfs-gui/src/app/pages/devices.rs:1192 +#: crates/pdfs-gui/src/app/pages/devices.rs:1253 +#: crates/pdfs-gui/src/app/pages/devices.rs:1282 #: crates/pdfs-gui/src/app/pages/duplicates.rs:221 #: crates/pdfs-gui/src/app/pages/locations.rs:577 #: crates/pdfs-gui/src/app/pages/locations.rs:824 @@ -1927,7 +1927,7 @@ msgid "{status}. {message}" msgstr "{status}. {message}" #: crates/pdfs-gui/src/app/pages/browser.rs:1900 -#: crates/pdfs-gui/src/app/pages/devices.rs:989 +#: crates/pdfs-gui/src/app/pages/devices.rs:1011 msgid "New name" msgstr "Nuovo nome" @@ -1952,7 +1952,7 @@ msgstr "Scegli la cartella di destinazione." #: crates/pdfs-gui/src/app/pages/browser.rs:1989 #: crates/pdfs-gui/src/app/pages/browser.rs:1994 -#: crates/pdfs-gui/src/app/pages/devices.rs:1195 +#: crates/pdfs-gui/src/app/pages/devices.rs:1217 msgid "Move Here" msgstr "Sposta qui" @@ -2275,76 +2275,87 @@ msgid_plural "{n} folders backed up" msgstr[0] "{n} cartella sottoposta a backup" msgstr[1] "{n} cartelle sottoposte a backup" -#: crates/pdfs-gui/src/app/pages/devices.rs:608 +#: crates/pdfs-gui/src/app/pages/devices.rs:607 +#, rust-format +msgid "{folders} · {n} waiting for its first sync" +msgid_plural "{folders} · {n} waiting for their first sync" +msgstr[0] "{folders} · {n} attende la prima sincronizzazione" +msgstr[1] "{folders} · {n} attendono la prima sincronizzazione" + +#: crates/pdfs-gui/src/app/pages/devices.rs:621 #, rust-format msgid "{folders} · {n} needs attention" msgid_plural "{folders} · {n} need attention" msgstr[0] "{folders} · {n} richiede attenzione" msgstr[1] "{folders} · {n} richiedono attenzione" -#: crates/pdfs-gui/src/app/pages/devices.rs:619 +#: crates/pdfs-gui/src/app/pages/devices.rs:640 msgid "syncing…" msgstr "sincronizzazione…" -#: crates/pdfs-gui/src/app/pages/devices.rs:620 +#: crates/pdfs-gui/src/app/pages/devices.rs:641 msgid "sync error" msgstr "errore di sincronizzazione" -#: crates/pdfs-gui/src/app/pages/devices.rs:621 +#: crates/pdfs-gui/src/app/pages/devices.rs:642 msgid "needs attention" msgstr "richiede attenzione" -#: crates/pdfs-gui/src/app/pages/devices.rs:622 +#: crates/pdfs-gui/src/app/pages/devices.rs:643 +msgid "waiting for first sync" +msgstr "in attesa della prima sincronizzazione" + +#: crates/pdfs-gui/src/app/pages/devices.rs:644 msgid "up to date" msgstr "aggiornato" -#: crates/pdfs-gui/src/app/pages/devices.rs:632 +#: crates/pdfs-gui/src/app/pages/devices.rs:654 msgid "checking for changes…" msgstr "controllo delle modifiche…" -#: crates/pdfs-gui/src/app/pages/devices.rs:638 +#: crates/pdfs-gui/src/app/pages/devices.rs:660 #, rust-format msgid "checking for changes — {n} item checked" msgid_plural "checking for changes — {n} items checked" msgstr[0] "controllo delle modifiche: {n} elemento controllato" msgstr[1] "controllo delle modifiche: {n} elementi controllati" -#: crates/pdfs-gui/src/app/pages/devices.rs:645 +#: crates/pdfs-gui/src/app/pages/devices.rs:667 #, rust-format msgid "checking for changes — {done} of {total}" msgstr "controllo delle modifiche: {done} di {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:657 +#: crates/pdfs-gui/src/app/pages/devices.rs:679 #, rust-format msgid "syncing {done} of {total}" msgstr "sincronizzazione {done} di {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:663 +#: crates/pdfs-gui/src/app/pages/devices.rs:685 #, rust-format msgid "syncing {file} — {done} of {total}" msgstr "sincronizzazione di {file}: {done} di {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:675 +#: crates/pdfs-gui/src/app/pages/devices.rs:697 msgid "Add Folder to Sync" msgstr "Aggiungi cartella da sincronizzare" -#: crates/pdfs-gui/src/app/pages/devices.rs:699 +#: crates/pdfs-gui/src/app/pages/devices.rs:721 #: crates/pdfs-gui/src/app/pages/locations.rs:1559 #: crates/pdfs-gui/src/app/pages/locations.rs:1686 msgid "Syncing folder…" msgstr "Sincronizzazione della cartella…" -#: crates/pdfs-gui/src/app/pages/devices.rs:702 -#: crates/pdfs-gui/src/app/pages/devices.rs:706 +#: crates/pdfs-gui/src/app/pages/devices.rs:724 +#: crates/pdfs-gui/src/app/pages/devices.rs:728 msgid "Couldn't add folder" msgstr "Impossibile aggiungere la cartella" -#: crates/pdfs-gui/src/app/pages/devices.rs:740 -#: crates/pdfs-gui/src/app/pages/devices.rs:746 +#: crates/pdfs-gui/src/app/pages/devices.rs:762 +#: crates/pdfs-gui/src/app/pages/devices.rs:768 msgid "Couldn't list folders to restore" msgstr "Impossibile elencare le cartelle da ripristinare" -#: crates/pdfs-gui/src/app/pages/devices.rs:772 +#: crates/pdfs-gui/src/app/pages/devices.rs:794 msgid "" "Tick the folders to sync to this machine, and adjust where each one should " "live." @@ -2352,19 +2363,19 @@ msgstr "" "Seleziona le cartelle da sincronizzare su questa macchina e regola dove deve " "trovarsi ciascuna." -#: crates/pdfs-gui/src/app/pages/devices.rs:798 +#: crates/pdfs-gui/src/app/pages/devices.rs:820 msgid "Restore Folders" msgstr "Ripristina cartelle" -#: crates/pdfs-gui/src/app/pages/devices.rs:805 +#: crates/pdfs-gui/src/app/pages/devices.rs:827 msgid "Restore" msgstr "Ripristina" -#: crates/pdfs-gui/src/app/pages/devices.rs:838 +#: crates/pdfs-gui/src/app/pages/devices.rs:860 msgid "Restoring folders…" msgstr "Ripristino delle cartelle…" -#: crates/pdfs-gui/src/app/pages/devices.rs:850 +#: crates/pdfs-gui/src/app/pages/devices.rs:872 #, rust-format msgid "" "These folders are backed up from “{name}”. Restored folders keep syncing " @@ -2373,39 +2384,39 @@ msgstr "" "Queste cartelle sono sottoposte a backup da “{name}”. Le cartelle " "ripristinate continuano a sincronizzarsi con la copia di quel computer." -#: crates/pdfs-gui/src/app/pages/devices.rs:853 +#: crates/pdfs-gui/src/app/pages/devices.rs:875 msgid "These folders are backed up under this computer in Proton Drive." msgstr "" "Queste cartelle sono sottoposte a backup sotto questo computer in Proton " "Drive." -#: crates/pdfs-gui/src/app/pages/devices.rs:862 +#: crates/pdfs-gui/src/app/pages/devices.rs:884 #, rust-format msgid "Nothing to restore — “{name}”'s folders are all synced here." msgstr "" "Niente da ripristinare: le cartelle di “{name}” sono già tutte sincronizzate " "qui." -#: crates/pdfs-gui/src/app/pages/devices.rs:865 +#: crates/pdfs-gui/src/app/pages/devices.rs:887 msgid "Nothing to restore — this computer's folders are all synced here." msgstr "" "Niente da ripristinare: le cartelle di questo computer sono già tutte " "sincronizzate qui." -#: crates/pdfs-gui/src/app/pages/devices.rs:889 +#: crates/pdfs-gui/src/app/pages/devices.rs:911 msgid "Folder will switch to online only" msgstr "La cartella passerà a solo online" -#: crates/pdfs-gui/src/app/pages/devices.rs:891 +#: crates/pdfs-gui/src/app/pages/devices.rs:913 msgid "Folder will download and stay synced" msgstr "La cartella verrà scaricata e resterà sincronizzata" -#: crates/pdfs-gui/src/app/pages/devices.rs:895 -#: crates/pdfs-gui/src/app/pages/devices.rs:899 +#: crates/pdfs-gui/src/app/pages/devices.rs:917 +#: crates/pdfs-gui/src/app/pages/devices.rs:921 msgid "Couldn't change mode" msgstr "Impossibile cambiare modalità" -#: crates/pdfs-gui/src/app/pages/devices.rs:931 +#: crates/pdfs-gui/src/app/pages/devices.rs:953 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2423,7 +2434,7 @@ msgstr "" "cartella a Sincronizzata e attendi il completamento del download prima di " "rimuoverla." -#: crates/pdfs-gui/src/app/pages/devices.rs:937 +#: crates/pdfs-gui/src/app/pages/devices.rs:959 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2436,68 +2447,68 @@ msgstr "" "I file locali restano su questo disco e semplicemente smettono di essere " "sincronizzati. Scegli se eliminare anche la copia in Proton Drive." -#: crates/pdfs-gui/src/app/pages/devices.rs:942 +#: crates/pdfs-gui/src/app/pages/devices.rs:964 msgid "Stop Syncing Folder?" msgstr "Interrompere la sincronizzazione della cartella?" -#: crates/pdfs-gui/src/app/pages/devices.rs:947 +#: crates/pdfs-gui/src/app/pages/devices.rs:969 msgid "Also delete from Proton Drive" msgstr "Elimina anche da Proton Drive" -#: crates/pdfs-gui/src/app/pages/devices.rs:949 +#: crates/pdfs-gui/src/app/pages/devices.rs:971 msgid "Deletes the only copy of these files." msgstr "Elimina l'unica copia di questi file." -#: crates/pdfs-gui/src/app/pages/devices.rs:951 +#: crates/pdfs-gui/src/app/pages/devices.rs:973 msgid "The local copy is unaffected." msgstr "La copia locale non viene modificata." -#: crates/pdfs-gui/src/app/pages/devices.rs:958 +#: crates/pdfs-gui/src/app/pages/devices.rs:980 msgid "Stop Syncing" msgstr "Interrompi sincronizzazione" -#: crates/pdfs-gui/src/app/pages/devices.rs:971 +#: crates/pdfs-gui/src/app/pages/devices.rs:993 msgid "Stopped syncing folder" msgstr "Sincronizzazione della cartella interrotta" -#: crates/pdfs-gui/src/app/pages/devices.rs:972 +#: crates/pdfs-gui/src/app/pages/devices.rs:994 msgid "Couldn't stop syncing the folder" msgstr "Impossibile interrompere la sincronizzazione della cartella" -#: crates/pdfs-gui/src/app/pages/devices.rs:983 +#: crates/pdfs-gui/src/app/pages/devices.rs:1005 msgid "Rename Computer" msgstr "Rinomina computer" #. Translators: {name} is the computer's current name. -#: crates/pdfs-gui/src/app/pages/devices.rs:985 +#: crates/pdfs-gui/src/app/pages/devices.rs:1007 #, rust-format msgid "Rename “{name}”." msgstr "Rinomina “{name}”." -#: crates/pdfs-gui/src/app/pages/devices.rs:1010 -#: crates/pdfs-gui/src/app/pages/devices.rs:1022 +#: crates/pdfs-gui/src/app/pages/devices.rs:1032 +#: crates/pdfs-gui/src/app/pages/devices.rs:1044 msgid "Couldn't rename the computer" msgstr "Impossibile rinominare il computer" -#: crates/pdfs-gui/src/app/pages/devices.rs:1011 +#: crates/pdfs-gui/src/app/pages/devices.rs:1033 msgid "A name is required." msgstr "È richiesto un nome." -#: crates/pdfs-gui/src/app/pages/devices.rs:1021 +#: crates/pdfs-gui/src/app/pages/devices.rs:1043 msgid "Computer renamed" msgstr "Computer rinominato" #. Translators: {name} is the computer's name, which the user must type. -#: crates/pdfs-gui/src/app/pages/devices.rs:1034 +#: crates/pdfs-gui/src/app/pages/devices.rs:1056 #, rust-format msgid "Type “{name}” to confirm" msgstr "Digita “{name}” per confermare" -#: crates/pdfs-gui/src/app/pages/devices.rs:1039 +#: crates/pdfs-gui/src/app/pages/devices.rs:1061 msgid "Remove Computer?" msgstr "Rimuovere il computer?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1042 +#: crates/pdfs-gui/src/app/pages/devices.rs:1064 #, rust-format msgid "" "Remove “{name}” from this account?\n" @@ -2512,26 +2523,26 @@ msgstr "" "insieme a esso. I file su quel computer non vengono toccati, ma questa " "operazione non può essere annullata da qui." -#: crates/pdfs-gui/src/app/pages/devices.rs:1048 +#: crates/pdfs-gui/src/app/pages/devices.rs:1070 #: crates/pdfs-gui/src/app/pages/shared.rs:1025 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:634 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:876 msgid "Remove" msgstr "Rimuovi" -#: crates/pdfs-gui/src/app/pages/devices.rs:1065 +#: crates/pdfs-gui/src/app/pages/devices.rs:1087 msgid "Computer removed" msgstr "Computer rimosso" -#: crates/pdfs-gui/src/app/pages/devices.rs:1066 +#: crates/pdfs-gui/src/app/pages/devices.rs:1088 msgid "Couldn't remove the computer" msgstr "Impossibile rimuovere il computer" -#: crates/pdfs-gui/src/app/pages/devices.rs:1084 +#: crates/pdfs-gui/src/app/pages/devices.rs:1106 msgid "Continue This Backup Here?" msgstr "Continuare questo backup qui?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1087 +#: crates/pdfs-gui/src/app/pages/devices.rs:1109 #, rust-format msgid "" "This computer takes over the backup of “{name}”, for example after a " @@ -2550,25 +2561,25 @@ msgstr "" "qui non vengono spostate. Usa poi “Ripristina su questo computer” per " "recuperare ciò che “{name}” sincronizzava." -#: crates/pdfs-gui/src/app/pages/devices.rs:1092 +#: crates/pdfs-gui/src/app/pages/devices.rs:1114 msgid "Continue Here" msgstr "Continua qui" #. Translators: {name} is the name of the computer whose backup this one took over. -#: crates/pdfs-gui/src/app/pages/devices.rs:1099 +#: crates/pdfs-gui/src/app/pages/devices.rs:1121 #, rust-format msgid "Now backing up as “{name}”" msgstr "Ora il backup avviene come “{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1108 +#: crates/pdfs-gui/src/app/pages/devices.rs:1130 msgid "Couldn't continue that backup here" msgstr "Impossibile continuare quel backup qui" -#: crates/pdfs-gui/src/app/pages/devices.rs:1124 +#: crates/pdfs-gui/src/app/pages/devices.rs:1146 msgid "Sync With This Computer?" msgstr "Sincronizzare con questo computer?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1127 +#: crates/pdfs-gui/src/app/pages/devices.rs:1149 #, rust-format msgid "" "This computer and “{name}” keep the same folders in step. A change or a " @@ -2585,25 +2596,25 @@ msgstr "" "in Proton Drive. Le cartelle già sincronizzate qui restano dove sono. Poi " "scegli quali delle sue cartelle sincronizzare qui." -#: crates/pdfs-gui/src/app/pages/devices.rs:1132 +#: crates/pdfs-gui/src/app/pages/devices.rs:1154 msgid "Sync Here" msgstr "Sincronizza qui" #. Translators: {name} is the name of the other computer this one now syncs with. -#: crates/pdfs-gui/src/app/pages/devices.rs:1139 +#: crates/pdfs-gui/src/app/pages/devices.rs:1161 #, rust-format msgid "Now syncing with “{name}”" msgstr "Ora sincronizzato con “{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1155 +#: crates/pdfs-gui/src/app/pages/devices.rs:1177 msgid "Couldn't sync with that computer" msgstr "Impossibile sincronizzare con quel computer" -#: crates/pdfs-gui/src/app/pages/devices.rs:1187 +#: crates/pdfs-gui/src/app/pages/devices.rs:1209 msgid "Move Backup to This Computer?" msgstr "Spostare il backup su questo computer?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1190 +#: crates/pdfs-gui/src/app/pages/devices.rs:1212 #, rust-format msgid "" "The folders backed up by “{name}” move into this computer's backup in Proton " @@ -2620,12 +2631,12 @@ msgstr "" "cartelle." #. Translators: {name} is the name of the computer whose folders moved to this one. -#: crates/pdfs-gui/src/app/pages/devices.rs:1202 +#: crates/pdfs-gui/src/app/pages/devices.rs:1224 #, rust-format msgid "Moved the backup of “{name}”" msgstr "Backup di “{name}” spostato" -#: crates/pdfs-gui/src/app/pages/devices.rs:1216 +#: crates/pdfs-gui/src/app/pages/devices.rs:1238 msgid "Couldn't move that backup" msgstr "Impossibile spostare quel backup" @@ -3366,7 +3377,7 @@ msgid "paused" msgstr "in pausa" #. Translators: a folder's files can't be opened right now. -#: crates/pdfs-gui/src/app/pages/locations.rs:1742 +#: crates/pdfs-gui/src/app/pages/locations.rs:1745 msgid "not available" msgstr "non disponibile" diff --git a/po/nl.po b/po/nl.po index 1de0e6f..d79763c 100644 --- a/po/nl.po +++ b/po/nl.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:26+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Dutch\n" @@ -246,13 +246,13 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:1917 #: crates/pdfs-gui/src/app/pages/browser.rs:2039 #: crates/pdfs-gui/src/app/pages/browser.rs:2540 -#: crates/pdfs-gui/src/app/pages/devices.rs:804 -#: crates/pdfs-gui/src/app/pages/devices.rs:957 -#: crates/pdfs-gui/src/app/pages/devices.rs:995 -#: crates/pdfs-gui/src/app/pages/devices.rs:1047 -#: crates/pdfs-gui/src/app/pages/devices.rs:1091 -#: crates/pdfs-gui/src/app/pages/devices.rs:1131 -#: crates/pdfs-gui/src/app/pages/devices.rs:1194 +#: crates/pdfs-gui/src/app/pages/devices.rs:826 +#: crates/pdfs-gui/src/app/pages/devices.rs:979 +#: crates/pdfs-gui/src/app/pages/devices.rs:1017 +#: crates/pdfs-gui/src/app/pages/devices.rs:1069 +#: crates/pdfs-gui/src/app/pages/devices.rs:1113 +#: crates/pdfs-gui/src/app/pages/devices.rs:1153 +#: crates/pdfs-gui/src/app/pages/devices.rs:1216 #: crates/pdfs-gui/src/app/pages/locations.rs:850 #: crates/pdfs-gui/src/app/pages/locations.rs:1054 #: crates/pdfs-gui/src/app/pages/locations.rs:1537 @@ -347,7 +347,7 @@ msgstr "Raster / lijst" #: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/browser.rs:1896 #: crates/pdfs-gui/src/app/pages/browser.rs:1918 -#: crates/pdfs-gui/src/app/pages/devices.rs:996 +#: crates/pdfs-gui/src/app/pages/devices.rs:1018 #: crates/pdfs-gui/src/app/widgets/details.rs:161 msgid "Rename" msgstr "Hernoemen" @@ -679,7 +679,7 @@ msgid "Couldn't add a sync folder" msgstr "Kon geen synchronisatiemap toevoegen" #: crates/pdfs-gui/src/app/pages/activity_text.rs:58 -#: crates/pdfs-gui/src/app/pages/devices.rs:839 +#: crates/pdfs-gui/src/app/pages/devices.rs:861 msgid "Couldn't restore folders" msgstr "Kon mappen niet terugzetten" @@ -1269,12 +1269,12 @@ msgstr "Kon albums niet laden" #: crates/pdfs-gui/src/app/pages/browser.rs:1873 #: crates/pdfs-gui/src/app/pages/browser.rs:2500 #: crates/pdfs-gui/src/app/pages/browser.rs:2650 -#: crates/pdfs-gui/src/app/pages/devices.rs:707 -#: crates/pdfs-gui/src/app/pages/devices.rs:747 -#: crates/pdfs-gui/src/app/pages/devices.rs:900 -#: crates/pdfs-gui/src/app/pages/devices.rs:1170 -#: crates/pdfs-gui/src/app/pages/devices.rs:1231 -#: crates/pdfs-gui/src/app/pages/devices.rs:1260 +#: crates/pdfs-gui/src/app/pages/devices.rs:729 +#: crates/pdfs-gui/src/app/pages/devices.rs:769 +#: crates/pdfs-gui/src/app/pages/devices.rs:922 +#: crates/pdfs-gui/src/app/pages/devices.rs:1192 +#: crates/pdfs-gui/src/app/pages/devices.rs:1253 +#: crates/pdfs-gui/src/app/pages/devices.rs:1282 #: crates/pdfs-gui/src/app/pages/duplicates.rs:221 #: crates/pdfs-gui/src/app/pages/locations.rs:577 #: crates/pdfs-gui/src/app/pages/locations.rs:824 @@ -1921,7 +1921,7 @@ msgid "{status}. {message}" msgstr "{status}. {message}" #: crates/pdfs-gui/src/app/pages/browser.rs:1900 -#: crates/pdfs-gui/src/app/pages/devices.rs:989 +#: crates/pdfs-gui/src/app/pages/devices.rs:1011 msgid "New name" msgstr "Nieuwe naam" @@ -1946,7 +1946,7 @@ msgstr "Kies de map waarnaar je wilt verplaatsen." #: crates/pdfs-gui/src/app/pages/browser.rs:1989 #: crates/pdfs-gui/src/app/pages/browser.rs:1994 -#: crates/pdfs-gui/src/app/pages/devices.rs:1195 +#: crates/pdfs-gui/src/app/pages/devices.rs:1217 msgid "Move Here" msgstr "Hierheen verplaatsen" @@ -2269,76 +2269,87 @@ msgid_plural "{n} folders backed up" msgstr[0] "{n} map in back-up" msgstr[1] "{n} mappen in back-up" -#: crates/pdfs-gui/src/app/pages/devices.rs:608 +#: crates/pdfs-gui/src/app/pages/devices.rs:607 +#, rust-format +msgid "{folders} · {n} waiting for its first sync" +msgid_plural "{folders} · {n} waiting for their first sync" +msgstr[0] "{folders} · {n} wacht op de eerste synchronisatie" +msgstr[1] "{folders} · {n} wachten op de eerste synchronisatie" + +#: crates/pdfs-gui/src/app/pages/devices.rs:621 #, rust-format msgid "{folders} · {n} needs attention" msgid_plural "{folders} · {n} need attention" msgstr[0] "{folders} · {n} vereist aandacht" msgstr[1] "{folders} · {n} vereisen aandacht" -#: crates/pdfs-gui/src/app/pages/devices.rs:619 +#: crates/pdfs-gui/src/app/pages/devices.rs:640 msgid "syncing…" msgstr "synchroniseren…" -#: crates/pdfs-gui/src/app/pages/devices.rs:620 +#: crates/pdfs-gui/src/app/pages/devices.rs:641 msgid "sync error" msgstr "synchronisatiefout" -#: crates/pdfs-gui/src/app/pages/devices.rs:621 +#: crates/pdfs-gui/src/app/pages/devices.rs:642 msgid "needs attention" msgstr "vereist aandacht" -#: crates/pdfs-gui/src/app/pages/devices.rs:622 +#: crates/pdfs-gui/src/app/pages/devices.rs:643 +msgid "waiting for first sync" +msgstr "wacht op eerste synchronisatie" + +#: crates/pdfs-gui/src/app/pages/devices.rs:644 msgid "up to date" msgstr "bijgewerkt" -#: crates/pdfs-gui/src/app/pages/devices.rs:632 +#: crates/pdfs-gui/src/app/pages/devices.rs:654 msgid "checking for changes…" msgstr "controleren op wijzigingen…" -#: crates/pdfs-gui/src/app/pages/devices.rs:638 +#: crates/pdfs-gui/src/app/pages/devices.rs:660 #, rust-format msgid "checking for changes — {n} item checked" msgid_plural "checking for changes — {n} items checked" msgstr[0] "controleren op wijzigingen — {n} item gecontroleerd" msgstr[1] "controleren op wijzigingen — {n} items gecontroleerd" -#: crates/pdfs-gui/src/app/pages/devices.rs:645 +#: crates/pdfs-gui/src/app/pages/devices.rs:667 #, rust-format msgid "checking for changes — {done} of {total}" msgstr "controleren op wijzigingen — {done} van {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:657 +#: crates/pdfs-gui/src/app/pages/devices.rs:679 #, rust-format msgid "syncing {done} of {total}" msgstr "synchroniseren: {done} van {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:663 +#: crates/pdfs-gui/src/app/pages/devices.rs:685 #, rust-format msgid "syncing {file} — {done} of {total}" msgstr "{file} synchroniseren — {done} van {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:675 +#: crates/pdfs-gui/src/app/pages/devices.rs:697 msgid "Add Folder to Sync" msgstr "Map toevoegen aan synchronisatie" -#: crates/pdfs-gui/src/app/pages/devices.rs:699 +#: crates/pdfs-gui/src/app/pages/devices.rs:721 #: crates/pdfs-gui/src/app/pages/locations.rs:1559 #: crates/pdfs-gui/src/app/pages/locations.rs:1686 msgid "Syncing folder…" msgstr "Map synchroniseren…" -#: crates/pdfs-gui/src/app/pages/devices.rs:702 -#: crates/pdfs-gui/src/app/pages/devices.rs:706 +#: crates/pdfs-gui/src/app/pages/devices.rs:724 +#: crates/pdfs-gui/src/app/pages/devices.rs:728 msgid "Couldn't add folder" msgstr "Kon map niet toevoegen" -#: crates/pdfs-gui/src/app/pages/devices.rs:740 -#: crates/pdfs-gui/src/app/pages/devices.rs:746 +#: crates/pdfs-gui/src/app/pages/devices.rs:762 +#: crates/pdfs-gui/src/app/pages/devices.rs:768 msgid "Couldn't list folders to restore" msgstr "Kon mappen om terug te zetten niet weergeven" -#: crates/pdfs-gui/src/app/pages/devices.rs:772 +#: crates/pdfs-gui/src/app/pages/devices.rs:794 msgid "" "Tick the folders to sync to this machine, and adjust where each one should " "live." @@ -2346,19 +2357,19 @@ msgstr "" "Vink de mappen aan die je met dit apparaat wilt synchroniseren en pas aan " "waar elke map moet komen." -#: crates/pdfs-gui/src/app/pages/devices.rs:798 +#: crates/pdfs-gui/src/app/pages/devices.rs:820 msgid "Restore Folders" msgstr "Mappen herstellen" -#: crates/pdfs-gui/src/app/pages/devices.rs:805 +#: crates/pdfs-gui/src/app/pages/devices.rs:827 msgid "Restore" msgstr "Terugzetten" -#: crates/pdfs-gui/src/app/pages/devices.rs:838 +#: crates/pdfs-gui/src/app/pages/devices.rs:860 msgid "Restoring folders…" msgstr "Mappen terugzetten…" -#: crates/pdfs-gui/src/app/pages/devices.rs:850 +#: crates/pdfs-gui/src/app/pages/devices.rs:872 #, rust-format msgid "" "These folders are backed up from “{name}”. Restored folders keep syncing " @@ -2367,38 +2378,38 @@ msgstr "" "Van deze mappen is een back-up gemaakt vanaf “{name}”. Teruggezette mappen " "blijven synchroniseren met de kopie van die computer." -#: crates/pdfs-gui/src/app/pages/devices.rs:853 +#: crates/pdfs-gui/src/app/pages/devices.rs:875 msgid "These folders are backed up under this computer in Proton Drive." msgstr "" "Van deze mappen is een back-up gemaakt onder deze computer in Proton Drive." -#: crates/pdfs-gui/src/app/pages/devices.rs:862 +#: crates/pdfs-gui/src/app/pages/devices.rs:884 #, rust-format msgid "Nothing to restore — “{name}”'s folders are all synced here." msgstr "" "Niets om terug te zetten — de mappen van “{name}” worden hier al allemaal " "gesynchroniseerd." -#: crates/pdfs-gui/src/app/pages/devices.rs:865 +#: crates/pdfs-gui/src/app/pages/devices.rs:887 msgid "Nothing to restore — this computer's folders are all synced here." msgstr "" "Niets om terug te zetten — de mappen van deze computer worden hier al " "allemaal gesynchroniseerd." -#: crates/pdfs-gui/src/app/pages/devices.rs:889 +#: crates/pdfs-gui/src/app/pages/devices.rs:911 msgid "Folder will switch to online only" msgstr "Map wordt alleen online" -#: crates/pdfs-gui/src/app/pages/devices.rs:891 +#: crates/pdfs-gui/src/app/pages/devices.rs:913 msgid "Folder will download and stay synced" msgstr "Map wordt gedownload en blijft gesynchroniseerd" -#: crates/pdfs-gui/src/app/pages/devices.rs:895 -#: crates/pdfs-gui/src/app/pages/devices.rs:899 +#: crates/pdfs-gui/src/app/pages/devices.rs:917 +#: crates/pdfs-gui/src/app/pages/devices.rs:921 msgid "Couldn't change mode" msgstr "Kon modus niet wijzigen" -#: crates/pdfs-gui/src/app/pages/devices.rs:931 +#: crates/pdfs-gui/src/app/pages/devices.rs:953 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2415,7 +2426,7 @@ msgstr "" "kopie houden, annuleer dan, zet de map op Gesynchroniseerd en wacht tot de " "download klaar is voordat je hem verwijdert." -#: crates/pdfs-gui/src/app/pages/devices.rs:937 +#: crates/pdfs-gui/src/app/pages/devices.rs:959 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2428,68 +2439,68 @@ msgstr "" "De lokale bestanden blijven op deze schijf en worden gewoon niet meer " "gesynchroniseerd. Kies of je ook de kopie in Proton Drive wilt verwijderen." -#: crates/pdfs-gui/src/app/pages/devices.rs:942 +#: crates/pdfs-gui/src/app/pages/devices.rs:964 msgid "Stop Syncing Folder?" msgstr "Synchroniseren van map stoppen?" -#: crates/pdfs-gui/src/app/pages/devices.rs:947 +#: crates/pdfs-gui/src/app/pages/devices.rs:969 msgid "Also delete from Proton Drive" msgstr "Ook uit Proton Drive verwijderen" -#: crates/pdfs-gui/src/app/pages/devices.rs:949 +#: crates/pdfs-gui/src/app/pages/devices.rs:971 msgid "Deletes the only copy of these files." msgstr "Verwijdert de enige kopie van deze bestanden." -#: crates/pdfs-gui/src/app/pages/devices.rs:951 +#: crates/pdfs-gui/src/app/pages/devices.rs:973 msgid "The local copy is unaffected." msgstr "De lokale kopie blijft ongewijzigd." -#: crates/pdfs-gui/src/app/pages/devices.rs:958 +#: crates/pdfs-gui/src/app/pages/devices.rs:980 msgid "Stop Syncing" msgstr "Synchroniseren stoppen" -#: crates/pdfs-gui/src/app/pages/devices.rs:971 +#: crates/pdfs-gui/src/app/pages/devices.rs:993 msgid "Stopped syncing folder" msgstr "Synchroniseren van map gestopt" -#: crates/pdfs-gui/src/app/pages/devices.rs:972 +#: crates/pdfs-gui/src/app/pages/devices.rs:994 msgid "Couldn't stop syncing the folder" msgstr "Kon het synchroniseren van de map niet stoppen" -#: crates/pdfs-gui/src/app/pages/devices.rs:983 +#: crates/pdfs-gui/src/app/pages/devices.rs:1005 msgid "Rename Computer" msgstr "Computer hernoemen" #. Translators: {name} is the computer's current name. -#: crates/pdfs-gui/src/app/pages/devices.rs:985 +#: crates/pdfs-gui/src/app/pages/devices.rs:1007 #, rust-format msgid "Rename “{name}”." msgstr "“{name}” hernoemen." -#: crates/pdfs-gui/src/app/pages/devices.rs:1010 -#: crates/pdfs-gui/src/app/pages/devices.rs:1022 +#: crates/pdfs-gui/src/app/pages/devices.rs:1032 +#: crates/pdfs-gui/src/app/pages/devices.rs:1044 msgid "Couldn't rename the computer" msgstr "Kon de computer niet hernoemen" -#: crates/pdfs-gui/src/app/pages/devices.rs:1011 +#: crates/pdfs-gui/src/app/pages/devices.rs:1033 msgid "A name is required." msgstr "Een naam is vereist." -#: crates/pdfs-gui/src/app/pages/devices.rs:1021 +#: crates/pdfs-gui/src/app/pages/devices.rs:1043 msgid "Computer renamed" msgstr "Computer hernoemd" #. Translators: {name} is the computer's name, which the user must type. -#: crates/pdfs-gui/src/app/pages/devices.rs:1034 +#: crates/pdfs-gui/src/app/pages/devices.rs:1056 #, rust-format msgid "Type “{name}” to confirm" msgstr "Typ “{name}” om te bevestigen" -#: crates/pdfs-gui/src/app/pages/devices.rs:1039 +#: crates/pdfs-gui/src/app/pages/devices.rs:1061 msgid "Remove Computer?" msgstr "Computer verwijderen?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1042 +#: crates/pdfs-gui/src/app/pages/devices.rs:1064 #, rust-format msgid "" "Remove “{name}” from this account?\n" @@ -2504,26 +2515,26 @@ msgstr "" "mee verwijderd. De bestanden op die computer zelf blijven onaangeroerd — " "maar dit kan hier niet ongedaan worden gemaakt." -#: crates/pdfs-gui/src/app/pages/devices.rs:1048 +#: crates/pdfs-gui/src/app/pages/devices.rs:1070 #: crates/pdfs-gui/src/app/pages/shared.rs:1025 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:634 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:876 msgid "Remove" msgstr "Verwijderen" -#: crates/pdfs-gui/src/app/pages/devices.rs:1065 +#: crates/pdfs-gui/src/app/pages/devices.rs:1087 msgid "Computer removed" msgstr "Computer verwijderd" -#: crates/pdfs-gui/src/app/pages/devices.rs:1066 +#: crates/pdfs-gui/src/app/pages/devices.rs:1088 msgid "Couldn't remove the computer" msgstr "Kon de computer niet verwijderen" -#: crates/pdfs-gui/src/app/pages/devices.rs:1084 +#: crates/pdfs-gui/src/app/pages/devices.rs:1106 msgid "Continue This Backup Here?" msgstr "Deze back-up hier voortzetten?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1087 +#: crates/pdfs-gui/src/app/pages/devices.rs:1109 #, rust-format msgid "" "This computer takes over the backup of “{name}”, for example after a " @@ -2542,25 +2553,25 @@ msgstr "" "gesynchroniseerd, worden niet verplaatst. Gebruik daarna “Terugzetten op " "deze computer” om terug te halen wat “{name}” synchroniseerde." -#: crates/pdfs-gui/src/app/pages/devices.rs:1092 +#: crates/pdfs-gui/src/app/pages/devices.rs:1114 msgid "Continue Here" msgstr "Hier voortzetten" #. Translators: {name} is the name of the computer whose backup this one took over. -#: crates/pdfs-gui/src/app/pages/devices.rs:1099 +#: crates/pdfs-gui/src/app/pages/devices.rs:1121 #, rust-format msgid "Now backing up as “{name}”" msgstr "Back-up nu als “{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1108 +#: crates/pdfs-gui/src/app/pages/devices.rs:1130 msgid "Couldn't continue that backup here" msgstr "Kon die back-up hier niet voortzetten" -#: crates/pdfs-gui/src/app/pages/devices.rs:1124 +#: crates/pdfs-gui/src/app/pages/devices.rs:1146 msgid "Sync With This Computer?" msgstr "Synchroniseren met deze computer?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1127 +#: crates/pdfs-gui/src/app/pages/devices.rs:1149 #, rust-format msgid "" "This computer and “{name}” keep the same folders in step. A change or a " @@ -2577,25 +2588,25 @@ msgstr "" "Proton Drive. Mappen die hier al worden gesynchroniseerd, blijven waar ze " "zijn. Daarna kiest u welke van zijn mappen u hier synchroniseert." -#: crates/pdfs-gui/src/app/pages/devices.rs:1132 +#: crates/pdfs-gui/src/app/pages/devices.rs:1154 msgid "Sync Here" msgstr "Hier synchroniseren" #. Translators: {name} is the name of the other computer this one now syncs with. -#: crates/pdfs-gui/src/app/pages/devices.rs:1139 +#: crates/pdfs-gui/src/app/pages/devices.rs:1161 #, rust-format msgid "Now syncing with “{name}”" msgstr "Nu gesynchroniseerd met “{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1155 +#: crates/pdfs-gui/src/app/pages/devices.rs:1177 msgid "Couldn't sync with that computer" msgstr "Kon niet synchroniseren met die computer" -#: crates/pdfs-gui/src/app/pages/devices.rs:1187 +#: crates/pdfs-gui/src/app/pages/devices.rs:1209 msgid "Move Backup to This Computer?" msgstr "Back-up naar deze computer verplaatsen?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1190 +#: crates/pdfs-gui/src/app/pages/devices.rs:1212 #, rust-format msgid "" "The folders backed up by “{name}” move into this computer's backup in Proton " @@ -2611,12 +2622,12 @@ msgstr "" "Stop Proton Drive eerst op “{name}”. Anders ziet het zijn mappen verdwijnen." #. Translators: {name} is the name of the computer whose folders moved to this one. -#: crates/pdfs-gui/src/app/pages/devices.rs:1202 +#: crates/pdfs-gui/src/app/pages/devices.rs:1224 #, rust-format msgid "Moved the backup of “{name}”" msgstr "Back-up van “{name}” verplaatst" -#: crates/pdfs-gui/src/app/pages/devices.rs:1216 +#: crates/pdfs-gui/src/app/pages/devices.rs:1238 msgid "Couldn't move that backup" msgstr "Kon die back-up niet verplaatsen" @@ -3358,7 +3369,7 @@ msgid "paused" msgstr "gepauzeerd" #. Translators: a folder's files can't be opened right now. -#: crates/pdfs-gui/src/app/pages/locations.rs:1742 +#: crates/pdfs-gui/src/app/pages/locations.rs:1745 msgid "not available" msgstr "niet beschikbaar" diff --git a/po/pdfs.pot b/po/pdfs.pot index 90860e8..4014cfa 100644 --- a/po/pdfs.pot +++ b/po/pdfs.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 3.6.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:28+0100\n" +"POT-Creation-Date: 2026-10-31 10:26+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -222,13 +222,13 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:1917 #: crates/pdfs-gui/src/app/pages/browser.rs:2039 #: crates/pdfs-gui/src/app/pages/browser.rs:2540 -#: crates/pdfs-gui/src/app/pages/devices.rs:804 -#: crates/pdfs-gui/src/app/pages/devices.rs:957 -#: crates/pdfs-gui/src/app/pages/devices.rs:995 -#: crates/pdfs-gui/src/app/pages/devices.rs:1047 -#: crates/pdfs-gui/src/app/pages/devices.rs:1091 -#: crates/pdfs-gui/src/app/pages/devices.rs:1131 -#: crates/pdfs-gui/src/app/pages/devices.rs:1194 +#: crates/pdfs-gui/src/app/pages/devices.rs:826 +#: crates/pdfs-gui/src/app/pages/devices.rs:979 +#: crates/pdfs-gui/src/app/pages/devices.rs:1017 +#: crates/pdfs-gui/src/app/pages/devices.rs:1069 +#: crates/pdfs-gui/src/app/pages/devices.rs:1113 +#: crates/pdfs-gui/src/app/pages/devices.rs:1153 +#: crates/pdfs-gui/src/app/pages/devices.rs:1216 #: crates/pdfs-gui/src/app/pages/locations.rs:850 #: crates/pdfs-gui/src/app/pages/locations.rs:1054 #: crates/pdfs-gui/src/app/pages/locations.rs:1537 @@ -323,7 +323,7 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/browser.rs:1896 #: crates/pdfs-gui/src/app/pages/browser.rs:1918 -#: crates/pdfs-gui/src/app/pages/devices.rs:996 +#: crates/pdfs-gui/src/app/pages/devices.rs:1018 #: crates/pdfs-gui/src/app/widgets/details.rs:161 msgid "Rename" msgstr "" @@ -649,7 +649,7 @@ msgid "Couldn't add a sync folder" msgstr "" #: crates/pdfs-gui/src/app/pages/activity_text.rs:58 -#: crates/pdfs-gui/src/app/pages/devices.rs:839 +#: crates/pdfs-gui/src/app/pages/devices.rs:861 msgid "Couldn't restore folders" msgstr "" @@ -1227,12 +1227,12 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:1873 #: crates/pdfs-gui/src/app/pages/browser.rs:2500 #: crates/pdfs-gui/src/app/pages/browser.rs:2650 -#: crates/pdfs-gui/src/app/pages/devices.rs:707 -#: crates/pdfs-gui/src/app/pages/devices.rs:747 -#: crates/pdfs-gui/src/app/pages/devices.rs:900 -#: crates/pdfs-gui/src/app/pages/devices.rs:1170 -#: crates/pdfs-gui/src/app/pages/devices.rs:1231 -#: crates/pdfs-gui/src/app/pages/devices.rs:1260 +#: crates/pdfs-gui/src/app/pages/devices.rs:729 +#: crates/pdfs-gui/src/app/pages/devices.rs:769 +#: crates/pdfs-gui/src/app/pages/devices.rs:922 +#: crates/pdfs-gui/src/app/pages/devices.rs:1192 +#: crates/pdfs-gui/src/app/pages/devices.rs:1253 +#: crates/pdfs-gui/src/app/pages/devices.rs:1282 #: crates/pdfs-gui/src/app/pages/duplicates.rs:221 #: crates/pdfs-gui/src/app/pages/locations.rs:577 #: crates/pdfs-gui/src/app/pages/locations.rs:824 @@ -1875,7 +1875,7 @@ msgid "{status}. {message}" msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:1900 -#: crates/pdfs-gui/src/app/pages/devices.rs:989 +#: crates/pdfs-gui/src/app/pages/devices.rs:1011 msgid "New name" msgstr "" @@ -1900,7 +1900,7 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:1989 #: crates/pdfs-gui/src/app/pages/browser.rs:1994 -#: crates/pdfs-gui/src/app/pages/devices.rs:1195 +#: crates/pdfs-gui/src/app/pages/devices.rs:1217 msgid "Move Here" msgstr "" @@ -2221,127 +2221,138 @@ msgid_plural "{n} folders backed up" msgstr[0] "" msgstr[1] "" -#: crates/pdfs-gui/src/app/pages/devices.rs:608 +#: crates/pdfs-gui/src/app/pages/devices.rs:607 +#, rust-format +msgid "{folders} · {n} waiting for its first sync" +msgid_plural "{folders} · {n} waiting for their first sync" +msgstr[0] "" +msgstr[1] "" + +#: crates/pdfs-gui/src/app/pages/devices.rs:621 #, rust-format msgid "{folders} · {n} needs attention" msgid_plural "{folders} · {n} need attention" msgstr[0] "" msgstr[1] "" -#: crates/pdfs-gui/src/app/pages/devices.rs:619 +#: crates/pdfs-gui/src/app/pages/devices.rs:640 msgid "syncing…" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:620 +#: crates/pdfs-gui/src/app/pages/devices.rs:641 msgid "sync error" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:621 +#: crates/pdfs-gui/src/app/pages/devices.rs:642 msgid "needs attention" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:622 +#: crates/pdfs-gui/src/app/pages/devices.rs:643 +msgid "waiting for first sync" +msgstr "" + +#: crates/pdfs-gui/src/app/pages/devices.rs:644 msgid "up to date" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:632 +#: crates/pdfs-gui/src/app/pages/devices.rs:654 msgid "checking for changes…" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:638 +#: crates/pdfs-gui/src/app/pages/devices.rs:660 #, rust-format msgid "checking for changes — {n} item checked" msgid_plural "checking for changes — {n} items checked" msgstr[0] "" msgstr[1] "" -#: crates/pdfs-gui/src/app/pages/devices.rs:645 +#: crates/pdfs-gui/src/app/pages/devices.rs:667 #, rust-format msgid "checking for changes — {done} of {total}" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:657 +#: crates/pdfs-gui/src/app/pages/devices.rs:679 #, rust-format msgid "syncing {done} of {total}" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:663 +#: crates/pdfs-gui/src/app/pages/devices.rs:685 #, rust-format msgid "syncing {file} — {done} of {total}" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:675 +#: crates/pdfs-gui/src/app/pages/devices.rs:697 msgid "Add Folder to Sync" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:699 +#: crates/pdfs-gui/src/app/pages/devices.rs:721 #: crates/pdfs-gui/src/app/pages/locations.rs:1559 #: crates/pdfs-gui/src/app/pages/locations.rs:1686 msgid "Syncing folder…" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:702 -#: crates/pdfs-gui/src/app/pages/devices.rs:706 +#: crates/pdfs-gui/src/app/pages/devices.rs:724 +#: crates/pdfs-gui/src/app/pages/devices.rs:728 msgid "Couldn't add folder" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:740 -#: crates/pdfs-gui/src/app/pages/devices.rs:746 +#: crates/pdfs-gui/src/app/pages/devices.rs:762 +#: crates/pdfs-gui/src/app/pages/devices.rs:768 msgid "Couldn't list folders to restore" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:772 +#: crates/pdfs-gui/src/app/pages/devices.rs:794 msgid "" "Tick the folders to sync to this machine, and adjust where each one should " "live." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:798 +#: crates/pdfs-gui/src/app/pages/devices.rs:820 msgid "Restore Folders" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:805 +#: crates/pdfs-gui/src/app/pages/devices.rs:827 msgid "Restore" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:838 +#: crates/pdfs-gui/src/app/pages/devices.rs:860 msgid "Restoring folders…" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:850 +#: crates/pdfs-gui/src/app/pages/devices.rs:872 #, rust-format msgid "" "These folders are backed up from “{name}”. Restored folders keep syncing " "with that computer's copy." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:853 +#: crates/pdfs-gui/src/app/pages/devices.rs:875 msgid "These folders are backed up under this computer in Proton Drive." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:862 +#: crates/pdfs-gui/src/app/pages/devices.rs:884 #, rust-format msgid "Nothing to restore — “{name}”'s folders are all synced here." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:865 +#: crates/pdfs-gui/src/app/pages/devices.rs:887 msgid "Nothing to restore — this computer's folders are all synced here." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:889 +#: crates/pdfs-gui/src/app/pages/devices.rs:911 msgid "Folder will switch to online only" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:891 +#: crates/pdfs-gui/src/app/pages/devices.rs:913 msgid "Folder will download and stay synced" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:895 -#: crates/pdfs-gui/src/app/pages/devices.rs:899 +#: crates/pdfs-gui/src/app/pages/devices.rs:917 +#: crates/pdfs-gui/src/app/pages/devices.rs:921 msgid "Couldn't change mode" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:931 +#: crates/pdfs-gui/src/app/pages/devices.rs:953 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2352,7 +2363,7 @@ msgid "" "finish before removing it." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:937 +#: crates/pdfs-gui/src/app/pages/devices.rs:959 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2361,68 +2372,68 @@ msgid "" "whether to also delete the copy in Proton Drive." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:942 +#: crates/pdfs-gui/src/app/pages/devices.rs:964 msgid "Stop Syncing Folder?" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:947 +#: crates/pdfs-gui/src/app/pages/devices.rs:969 msgid "Also delete from Proton Drive" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:949 +#: crates/pdfs-gui/src/app/pages/devices.rs:971 msgid "Deletes the only copy of these files." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:951 +#: crates/pdfs-gui/src/app/pages/devices.rs:973 msgid "The local copy is unaffected." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:958 +#: crates/pdfs-gui/src/app/pages/devices.rs:980 msgid "Stop Syncing" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:971 +#: crates/pdfs-gui/src/app/pages/devices.rs:993 msgid "Stopped syncing folder" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:972 +#: crates/pdfs-gui/src/app/pages/devices.rs:994 msgid "Couldn't stop syncing the folder" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:983 +#: crates/pdfs-gui/src/app/pages/devices.rs:1005 msgid "Rename Computer" msgstr "" #. Translators: {name} is the computer's current name. -#: crates/pdfs-gui/src/app/pages/devices.rs:985 +#: crates/pdfs-gui/src/app/pages/devices.rs:1007 #, rust-format msgid "Rename “{name}”." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1010 -#: crates/pdfs-gui/src/app/pages/devices.rs:1022 +#: crates/pdfs-gui/src/app/pages/devices.rs:1032 +#: crates/pdfs-gui/src/app/pages/devices.rs:1044 msgid "Couldn't rename the computer" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1011 +#: crates/pdfs-gui/src/app/pages/devices.rs:1033 msgid "A name is required." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1021 +#: crates/pdfs-gui/src/app/pages/devices.rs:1043 msgid "Computer renamed" msgstr "" #. Translators: {name} is the computer's name, which the user must type. -#: crates/pdfs-gui/src/app/pages/devices.rs:1034 +#: crates/pdfs-gui/src/app/pages/devices.rs:1056 #, rust-format msgid "Type “{name}” to confirm" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1039 +#: crates/pdfs-gui/src/app/pages/devices.rs:1061 msgid "Remove Computer?" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1042 +#: crates/pdfs-gui/src/app/pages/devices.rs:1064 #, rust-format msgid "" "Remove “{name}” from this account?\n" @@ -2432,26 +2443,26 @@ msgid "" "here." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1048 +#: crates/pdfs-gui/src/app/pages/devices.rs:1070 #: crates/pdfs-gui/src/app/pages/shared.rs:1025 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:634 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:876 msgid "Remove" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1065 +#: crates/pdfs-gui/src/app/pages/devices.rs:1087 msgid "Computer removed" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1066 +#: crates/pdfs-gui/src/app/pages/devices.rs:1088 msgid "Couldn't remove the computer" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1084 +#: crates/pdfs-gui/src/app/pages/devices.rs:1106 msgid "Continue This Backup Here?" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1087 +#: crates/pdfs-gui/src/app/pages/devices.rs:1109 #, rust-format msgid "" "This computer takes over the backup of “{name}”, for example after a " @@ -2463,25 +2474,25 @@ msgid "" "syncing." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1092 +#: crates/pdfs-gui/src/app/pages/devices.rs:1114 msgid "Continue Here" msgstr "" #. Translators: {name} is the name of the computer whose backup this one took over. -#: crates/pdfs-gui/src/app/pages/devices.rs:1099 +#: crates/pdfs-gui/src/app/pages/devices.rs:1121 #, rust-format msgid "Now backing up as “{name}”" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1108 +#: crates/pdfs-gui/src/app/pages/devices.rs:1130 msgid "Couldn't continue that backup here" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1124 +#: crates/pdfs-gui/src/app/pages/devices.rs:1146 msgid "Sync With This Computer?" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1127 +#: crates/pdfs-gui/src/app/pages/devices.rs:1149 #, rust-format msgid "" "This computer and “{name}” keep the same folders in step. A change or a " @@ -2492,25 +2503,25 @@ msgid "" "its folders to sync here." msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1132 +#: crates/pdfs-gui/src/app/pages/devices.rs:1154 msgid "Sync Here" msgstr "" #. Translators: {name} is the name of the other computer this one now syncs with. -#: crates/pdfs-gui/src/app/pages/devices.rs:1139 +#: crates/pdfs-gui/src/app/pages/devices.rs:1161 #, rust-format msgid "Now syncing with “{name}”" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1155 +#: crates/pdfs-gui/src/app/pages/devices.rs:1177 msgid "Couldn't sync with that computer" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1187 +#: crates/pdfs-gui/src/app/pages/devices.rs:1209 msgid "Move Backup to This Computer?" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1190 +#: crates/pdfs-gui/src/app/pages/devices.rs:1212 #, rust-format msgid "" "The folders backed up by “{name}” move into this computer's backup in Proton " @@ -2521,12 +2532,12 @@ msgid "" msgstr "" #. Translators: {name} is the name of the computer whose folders moved to this one. -#: crates/pdfs-gui/src/app/pages/devices.rs:1202 +#: crates/pdfs-gui/src/app/pages/devices.rs:1224 #, rust-format msgid "Moved the backup of “{name}”" msgstr "" -#: crates/pdfs-gui/src/app/pages/devices.rs:1216 +#: crates/pdfs-gui/src/app/pages/devices.rs:1238 msgid "Couldn't move that backup" msgstr "" @@ -3210,7 +3221,7 @@ msgid "paused" msgstr "" #. Translators: a folder's files can't be opened right now. -#: crates/pdfs-gui/src/app/pages/locations.rs:1742 +#: crates/pdfs-gui/src/app/pages/locations.rs:1745 msgid "not available" msgstr "" diff --git a/po/pl.po b/po/pl.po index 8acb1fb..558008f 100644 --- a/po/pl.po +++ b/po/pl.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:26+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Polish\n" @@ -250,13 +250,13 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:1917 #: crates/pdfs-gui/src/app/pages/browser.rs:2039 #: crates/pdfs-gui/src/app/pages/browser.rs:2540 -#: crates/pdfs-gui/src/app/pages/devices.rs:804 -#: crates/pdfs-gui/src/app/pages/devices.rs:957 -#: crates/pdfs-gui/src/app/pages/devices.rs:995 -#: crates/pdfs-gui/src/app/pages/devices.rs:1047 -#: crates/pdfs-gui/src/app/pages/devices.rs:1091 -#: crates/pdfs-gui/src/app/pages/devices.rs:1131 -#: crates/pdfs-gui/src/app/pages/devices.rs:1194 +#: crates/pdfs-gui/src/app/pages/devices.rs:826 +#: crates/pdfs-gui/src/app/pages/devices.rs:979 +#: crates/pdfs-gui/src/app/pages/devices.rs:1017 +#: crates/pdfs-gui/src/app/pages/devices.rs:1069 +#: crates/pdfs-gui/src/app/pages/devices.rs:1113 +#: crates/pdfs-gui/src/app/pages/devices.rs:1153 +#: crates/pdfs-gui/src/app/pages/devices.rs:1216 #: crates/pdfs-gui/src/app/pages/locations.rs:850 #: crates/pdfs-gui/src/app/pages/locations.rs:1054 #: crates/pdfs-gui/src/app/pages/locations.rs:1537 @@ -351,7 +351,7 @@ msgstr "Siatka/lista" #: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/browser.rs:1896 #: crates/pdfs-gui/src/app/pages/browser.rs:1918 -#: crates/pdfs-gui/src/app/pages/devices.rs:996 +#: crates/pdfs-gui/src/app/pages/devices.rs:1018 #: crates/pdfs-gui/src/app/widgets/details.rs:161 msgid "Rename" msgstr "Zmień nazwę" @@ -696,7 +696,7 @@ msgid "Couldn't add a sync folder" msgstr "Nie można dodać katalogu synchronizacji" #: crates/pdfs-gui/src/app/pages/activity_text.rs:58 -#: crates/pdfs-gui/src/app/pages/devices.rs:839 +#: crates/pdfs-gui/src/app/pages/devices.rs:861 msgid "Couldn't restore folders" msgstr "Nie można przywrócić katalogów" @@ -1300,12 +1300,12 @@ msgstr "Nie można wczytać albumów" #: crates/pdfs-gui/src/app/pages/browser.rs:1873 #: crates/pdfs-gui/src/app/pages/browser.rs:2500 #: crates/pdfs-gui/src/app/pages/browser.rs:2650 -#: crates/pdfs-gui/src/app/pages/devices.rs:707 -#: crates/pdfs-gui/src/app/pages/devices.rs:747 -#: crates/pdfs-gui/src/app/pages/devices.rs:900 -#: crates/pdfs-gui/src/app/pages/devices.rs:1170 -#: crates/pdfs-gui/src/app/pages/devices.rs:1231 -#: crates/pdfs-gui/src/app/pages/devices.rs:1260 +#: crates/pdfs-gui/src/app/pages/devices.rs:729 +#: crates/pdfs-gui/src/app/pages/devices.rs:769 +#: crates/pdfs-gui/src/app/pages/devices.rs:922 +#: crates/pdfs-gui/src/app/pages/devices.rs:1192 +#: crates/pdfs-gui/src/app/pages/devices.rs:1253 +#: crates/pdfs-gui/src/app/pages/devices.rs:1282 #: crates/pdfs-gui/src/app/pages/duplicates.rs:221 #: crates/pdfs-gui/src/app/pages/locations.rs:577 #: crates/pdfs-gui/src/app/pages/locations.rs:824 @@ -1969,7 +1969,7 @@ msgid "{status}. {message}" msgstr "{status}. {message}" #: crates/pdfs-gui/src/app/pages/browser.rs:1900 -#: crates/pdfs-gui/src/app/pages/devices.rs:989 +#: crates/pdfs-gui/src/app/pages/devices.rs:1011 msgid "New name" msgstr "Nowa nazwa" @@ -1994,7 +1994,7 @@ msgstr "Proszę wybrać katalog docelowy." #: crates/pdfs-gui/src/app/pages/browser.rs:1989 #: crates/pdfs-gui/src/app/pages/browser.rs:1994 -#: crates/pdfs-gui/src/app/pages/devices.rs:1195 +#: crates/pdfs-gui/src/app/pages/devices.rs:1217 msgid "Move Here" msgstr "Przenieś tutaj" @@ -2327,7 +2327,15 @@ msgstr[0] "{n} katalog z kopią zapasową" msgstr[1] "{n} katalogi z kopią zapasową" msgstr[2] "{n} katalogów z kopią zapasową" -#: crates/pdfs-gui/src/app/pages/devices.rs:608 +#: crates/pdfs-gui/src/app/pages/devices.rs:607 +#, rust-format +msgid "{folders} · {n} waiting for its first sync" +msgid_plural "{folders} · {n} waiting for their first sync" +msgstr[0] "{folders} · {n} czeka na pierwszą synchronizację" +msgstr[1] "{folders} · {n} czekają na pierwszą synchronizację" +msgstr[2] "{folders} · {n} czeka na pierwszą synchronizację" + +#: crates/pdfs-gui/src/app/pages/devices.rs:621 #, rust-format msgid "{folders} · {n} needs attention" msgid_plural "{folders} · {n} need attention" @@ -2335,27 +2343,31 @@ msgstr[0] "{folders} · {n} wymaga uwagi" msgstr[1] "{folders} · {n} wymagają uwagi" msgstr[2] "{folders} · {n} wymaga uwagi" -#: crates/pdfs-gui/src/app/pages/devices.rs:619 +#: crates/pdfs-gui/src/app/pages/devices.rs:640 msgid "syncing…" msgstr "synchronizowanie…" -#: crates/pdfs-gui/src/app/pages/devices.rs:620 +#: crates/pdfs-gui/src/app/pages/devices.rs:641 msgid "sync error" msgstr "błąd synchronizacji" -#: crates/pdfs-gui/src/app/pages/devices.rs:621 +#: crates/pdfs-gui/src/app/pages/devices.rs:642 msgid "needs attention" msgstr "wymaga uwagi" -#: crates/pdfs-gui/src/app/pages/devices.rs:622 +#: crates/pdfs-gui/src/app/pages/devices.rs:643 +msgid "waiting for first sync" +msgstr "oczekiwanie na pierwszą synchronizację" + +#: crates/pdfs-gui/src/app/pages/devices.rs:644 msgid "up to date" msgstr "aktualne" -#: crates/pdfs-gui/src/app/pages/devices.rs:632 +#: crates/pdfs-gui/src/app/pages/devices.rs:654 msgid "checking for changes…" msgstr "sprawdzanie zmian…" -#: crates/pdfs-gui/src/app/pages/devices.rs:638 +#: crates/pdfs-gui/src/app/pages/devices.rs:660 #, rust-format msgid "checking for changes — {n} item checked" msgid_plural "checking for changes — {n} items checked" @@ -2363,42 +2375,42 @@ msgstr[0] "sprawdzanie zmian — sprawdzono {n} element" msgstr[1] "sprawdzanie zmian — sprawdzono {n} elementy" msgstr[2] "sprawdzanie zmian — sprawdzono {n} elementów" -#: crates/pdfs-gui/src/app/pages/devices.rs:645 +#: crates/pdfs-gui/src/app/pages/devices.rs:667 #, rust-format msgid "checking for changes — {done} of {total}" msgstr "sprawdzanie zmian — {done} z {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:657 +#: crates/pdfs-gui/src/app/pages/devices.rs:679 #, rust-format msgid "syncing {done} of {total}" msgstr "synchronizowanie {done} z {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:663 +#: crates/pdfs-gui/src/app/pages/devices.rs:685 #, rust-format msgid "syncing {file} — {done} of {total}" msgstr "synchronizowanie {file} — {done} z {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:675 +#: crates/pdfs-gui/src/app/pages/devices.rs:697 msgid "Add Folder to Sync" msgstr "Dodaj katalog do synchronizacji" -#: crates/pdfs-gui/src/app/pages/devices.rs:699 +#: crates/pdfs-gui/src/app/pages/devices.rs:721 #: crates/pdfs-gui/src/app/pages/locations.rs:1559 #: crates/pdfs-gui/src/app/pages/locations.rs:1686 msgid "Syncing folder…" msgstr "Synchronizowanie katalogu…" -#: crates/pdfs-gui/src/app/pages/devices.rs:702 -#: crates/pdfs-gui/src/app/pages/devices.rs:706 +#: crates/pdfs-gui/src/app/pages/devices.rs:724 +#: crates/pdfs-gui/src/app/pages/devices.rs:728 msgid "Couldn't add folder" msgstr "Nie można dodać katalogu" -#: crates/pdfs-gui/src/app/pages/devices.rs:740 -#: crates/pdfs-gui/src/app/pages/devices.rs:746 +#: crates/pdfs-gui/src/app/pages/devices.rs:762 +#: crates/pdfs-gui/src/app/pages/devices.rs:768 msgid "Couldn't list folders to restore" msgstr "Nie można wyświetlić listy katalogów do przywrócenia" -#: crates/pdfs-gui/src/app/pages/devices.rs:772 +#: crates/pdfs-gui/src/app/pages/devices.rs:794 msgid "" "Tick the folders to sync to this machine, and adjust where each one should " "live." @@ -2406,19 +2418,19 @@ msgstr "" "Należy zaznaczyć katalogi do synchronizacji z tym komputerem i dostosować " "położenie każdego z nich." -#: crates/pdfs-gui/src/app/pages/devices.rs:798 +#: crates/pdfs-gui/src/app/pages/devices.rs:820 msgid "Restore Folders" msgstr "Przywróć katalogi" -#: crates/pdfs-gui/src/app/pages/devices.rs:805 +#: crates/pdfs-gui/src/app/pages/devices.rs:827 msgid "Restore" msgstr "Przywróć" -#: crates/pdfs-gui/src/app/pages/devices.rs:838 +#: crates/pdfs-gui/src/app/pages/devices.rs:860 msgid "Restoring folders…" msgstr "Przywracanie katalogów…" -#: crates/pdfs-gui/src/app/pages/devices.rs:850 +#: crates/pdfs-gui/src/app/pages/devices.rs:872 #, rust-format msgid "" "These folders are backed up from “{name}”. Restored folders keep syncing " @@ -2427,37 +2439,37 @@ msgstr "" "Te katalogi mają kopię zapasową z komputera „{name}”. Przywrócone katalogi " "nadal będą synchronizowane z kopią tego komputera." -#: crates/pdfs-gui/src/app/pages/devices.rs:853 +#: crates/pdfs-gui/src/app/pages/devices.rs:875 msgid "These folders are backed up under this computer in Proton Drive." msgstr "Te katalogi mają kopię zapasową w Proton Drive pod tym komputerem." -#: crates/pdfs-gui/src/app/pages/devices.rs:862 +#: crates/pdfs-gui/src/app/pages/devices.rs:884 #, rust-format msgid "Nothing to restore — “{name}”'s folders are all synced here." msgstr "" "Nie ma nic do przywrócenia — wszystkie katalogi komputera „{name}” są tutaj " "synchronizowane." -#: crates/pdfs-gui/src/app/pages/devices.rs:865 +#: crates/pdfs-gui/src/app/pages/devices.rs:887 msgid "Nothing to restore — this computer's folders are all synced here." msgstr "" "Nie ma nic do przywrócenia — wszystkie katalogi tego komputera są tutaj " "synchronizowane." -#: crates/pdfs-gui/src/app/pages/devices.rs:889 +#: crates/pdfs-gui/src/app/pages/devices.rs:911 msgid "Folder will switch to online only" msgstr "Katalog przejdzie w tryb tylko online" -#: crates/pdfs-gui/src/app/pages/devices.rs:891 +#: crates/pdfs-gui/src/app/pages/devices.rs:913 msgid "Folder will download and stay synced" msgstr "Katalog zostanie pobrany i będzie synchronizowany" -#: crates/pdfs-gui/src/app/pages/devices.rs:895 -#: crates/pdfs-gui/src/app/pages/devices.rs:899 +#: crates/pdfs-gui/src/app/pages/devices.rs:917 +#: crates/pdfs-gui/src/app/pages/devices.rs:921 msgid "Couldn't change mode" msgstr "Nie można zmienić trybu" -#: crates/pdfs-gui/src/app/pages/devices.rs:931 +#: crates/pdfs-gui/src/app/pages/devices.rs:953 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2474,7 +2486,7 @@ msgstr "" "pusty. Aby zachować lokalną kopię, należy anulować, przełączyć katalog na " "tryb Synchronizowany i poczekać na ukończenie pobierania przed usunięciem." -#: crates/pdfs-gui/src/app/pages/devices.rs:937 +#: crates/pdfs-gui/src/app/pages/devices.rs:959 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2487,68 +2499,68 @@ msgstr "" "Lokalne pliki pozostaną na tym dysku i po prostu przestaną być " "synchronizowane. Można też usunąć kopię w Proton Drive." -#: crates/pdfs-gui/src/app/pages/devices.rs:942 +#: crates/pdfs-gui/src/app/pages/devices.rs:964 msgid "Stop Syncing Folder?" msgstr "Zatrzymać synchronizację katalogu?" -#: crates/pdfs-gui/src/app/pages/devices.rs:947 +#: crates/pdfs-gui/src/app/pages/devices.rs:969 msgid "Also delete from Proton Drive" msgstr "Usuń także z Proton Drive" -#: crates/pdfs-gui/src/app/pages/devices.rs:949 +#: crates/pdfs-gui/src/app/pages/devices.rs:971 msgid "Deletes the only copy of these files." msgstr "Usuwa jedyną kopię tych plików." -#: crates/pdfs-gui/src/app/pages/devices.rs:951 +#: crates/pdfs-gui/src/app/pages/devices.rs:973 msgid "The local copy is unaffected." msgstr "Lokalna kopia pozostanie bez zmian." -#: crates/pdfs-gui/src/app/pages/devices.rs:958 +#: crates/pdfs-gui/src/app/pages/devices.rs:980 msgid "Stop Syncing" msgstr "Zatrzymaj synchronizację" -#: crates/pdfs-gui/src/app/pages/devices.rs:971 +#: crates/pdfs-gui/src/app/pages/devices.rs:993 msgid "Stopped syncing folder" msgstr "Zatrzymano synchronizację katalogu" -#: crates/pdfs-gui/src/app/pages/devices.rs:972 +#: crates/pdfs-gui/src/app/pages/devices.rs:994 msgid "Couldn't stop syncing the folder" msgstr "Nie można zatrzymać synchronizacji katalogu" -#: crates/pdfs-gui/src/app/pages/devices.rs:983 +#: crates/pdfs-gui/src/app/pages/devices.rs:1005 msgid "Rename Computer" msgstr "Zmiana nazwy komputera" #. Translators: {name} is the computer's current name. -#: crates/pdfs-gui/src/app/pages/devices.rs:985 +#: crates/pdfs-gui/src/app/pages/devices.rs:1007 #, rust-format msgid "Rename “{name}”." msgstr "Proszę podać nową nazwę komputera „{name}”." -#: crates/pdfs-gui/src/app/pages/devices.rs:1010 -#: crates/pdfs-gui/src/app/pages/devices.rs:1022 +#: crates/pdfs-gui/src/app/pages/devices.rs:1032 +#: crates/pdfs-gui/src/app/pages/devices.rs:1044 msgid "Couldn't rename the computer" msgstr "Nie można zmienić nazwy komputera" -#: crates/pdfs-gui/src/app/pages/devices.rs:1011 +#: crates/pdfs-gui/src/app/pages/devices.rs:1033 msgid "A name is required." msgstr "Nazwa jest wymagana." -#: crates/pdfs-gui/src/app/pages/devices.rs:1021 +#: crates/pdfs-gui/src/app/pages/devices.rs:1043 msgid "Computer renamed" msgstr "Zmieniono nazwę komputera" #. Translators: {name} is the computer's name, which the user must type. -#: crates/pdfs-gui/src/app/pages/devices.rs:1034 +#: crates/pdfs-gui/src/app/pages/devices.rs:1056 #, rust-format msgid "Type “{name}” to confirm" msgstr "Proszę wpisać „{name}”, aby potwierdzić" -#: crates/pdfs-gui/src/app/pages/devices.rs:1039 +#: crates/pdfs-gui/src/app/pages/devices.rs:1061 msgid "Remove Computer?" msgstr "Usunąć komputer?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1042 +#: crates/pdfs-gui/src/app/pages/devices.rs:1064 #, rust-format msgid "" "Remove “{name}” from this account?\n" @@ -2563,26 +2575,26 @@ msgstr "" "z nim. Pliki na samym komputerze pozostaną nietknięte — ale tej operacji nie " "można tutaj cofnąć." -#: crates/pdfs-gui/src/app/pages/devices.rs:1048 +#: crates/pdfs-gui/src/app/pages/devices.rs:1070 #: crates/pdfs-gui/src/app/pages/shared.rs:1025 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:634 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:876 msgid "Remove" msgstr "Usuń" -#: crates/pdfs-gui/src/app/pages/devices.rs:1065 +#: crates/pdfs-gui/src/app/pages/devices.rs:1087 msgid "Computer removed" msgstr "Usunięto komputer" -#: crates/pdfs-gui/src/app/pages/devices.rs:1066 +#: crates/pdfs-gui/src/app/pages/devices.rs:1088 msgid "Couldn't remove the computer" msgstr "Nie można usunąć komputera" -#: crates/pdfs-gui/src/app/pages/devices.rs:1084 +#: crates/pdfs-gui/src/app/pages/devices.rs:1106 msgid "Continue This Backup Here?" msgstr "Kontynuować tę kopię zapasową tutaj?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1087 +#: crates/pdfs-gui/src/app/pages/devices.rs:1109 #, rust-format msgid "" "This computer takes over the backup of “{name}”, for example after a " @@ -2601,25 +2613,25 @@ msgstr "" "przeniesione. Aby odzyskać to, co synchronizował „{name}”, proszę potem użyć " "opcji „Przywróć na tym komputerze”." -#: crates/pdfs-gui/src/app/pages/devices.rs:1092 +#: crates/pdfs-gui/src/app/pages/devices.rs:1114 msgid "Continue Here" msgstr "Kontynuuj tutaj" #. Translators: {name} is the name of the computer whose backup this one took over. -#: crates/pdfs-gui/src/app/pages/devices.rs:1099 +#: crates/pdfs-gui/src/app/pages/devices.rs:1121 #, rust-format msgid "Now backing up as “{name}”" msgstr "Kopia zapasowa jest teraz tworzona jako „{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1108 +#: crates/pdfs-gui/src/app/pages/devices.rs:1130 msgid "Couldn't continue that backup here" msgstr "Nie można kontynuować tej kopii zapasowej tutaj" -#: crates/pdfs-gui/src/app/pages/devices.rs:1124 +#: crates/pdfs-gui/src/app/pages/devices.rs:1146 msgid "Sync With This Computer?" msgstr "Synchronizować z tym komputerem?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1127 +#: crates/pdfs-gui/src/app/pages/devices.rs:1149 #, rust-format msgid "" "This computer and “{name}” keep the same folders in step. A change or a " @@ -2636,25 +2648,25 @@ msgstr "" "Drive. Katalogi już tu synchronizowane zostaną tam, gdzie są. Następnie " "proszę wybrać, które z jego katalogów synchronizować tutaj." -#: crates/pdfs-gui/src/app/pages/devices.rs:1132 +#: crates/pdfs-gui/src/app/pages/devices.rs:1154 msgid "Sync Here" msgstr "Synchronizuj tutaj" #. Translators: {name} is the name of the other computer this one now syncs with. -#: crates/pdfs-gui/src/app/pages/devices.rs:1139 +#: crates/pdfs-gui/src/app/pages/devices.rs:1161 #, rust-format msgid "Now syncing with “{name}”" msgstr "Teraz synchronizowane z „{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1155 +#: crates/pdfs-gui/src/app/pages/devices.rs:1177 msgid "Couldn't sync with that computer" msgstr "Nie można synchronizować z tym komputerem" -#: crates/pdfs-gui/src/app/pages/devices.rs:1187 +#: crates/pdfs-gui/src/app/pages/devices.rs:1209 msgid "Move Backup to This Computer?" msgstr "Przenieść kopię zapasową na ten komputer?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1190 +#: crates/pdfs-gui/src/app/pages/devices.rs:1212 #, rust-format msgid "" "The folders backed up by “{name}” move into this computer's backup in Proton " @@ -2672,12 +2684,12 @@ msgstr "" "jego foldery znikają." #. Translators: {name} is the name of the computer whose folders moved to this one. -#: crates/pdfs-gui/src/app/pages/devices.rs:1202 +#: crates/pdfs-gui/src/app/pages/devices.rs:1224 #, rust-format msgid "Moved the backup of “{name}”" msgstr "Przeniesiono kopię zapasową „{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1216 +#: crates/pdfs-gui/src/app/pages/devices.rs:1238 msgid "Couldn't move that backup" msgstr "Nie można przenieść tej kopii zapasowej" @@ -3423,7 +3435,7 @@ msgid "paused" msgstr "wstrzymany" #. Translators: a folder's files can't be opened right now. -#: crates/pdfs-gui/src/app/pages/locations.rs:1742 +#: crates/pdfs-gui/src/app/pages/locations.rs:1745 msgid "not available" msgstr "niedostępne" diff --git a/po/pt_BR.po b/po/pt_BR.po index 56b0d45..b61fcea 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:26+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Portuguese (Brazil)\n" @@ -245,13 +245,13 @@ msgstr "" #: crates/pdfs-gui/src/app/pages/browser.rs:1917 #: crates/pdfs-gui/src/app/pages/browser.rs:2039 #: crates/pdfs-gui/src/app/pages/browser.rs:2540 -#: crates/pdfs-gui/src/app/pages/devices.rs:804 -#: crates/pdfs-gui/src/app/pages/devices.rs:957 -#: crates/pdfs-gui/src/app/pages/devices.rs:995 -#: crates/pdfs-gui/src/app/pages/devices.rs:1047 -#: crates/pdfs-gui/src/app/pages/devices.rs:1091 -#: crates/pdfs-gui/src/app/pages/devices.rs:1131 -#: crates/pdfs-gui/src/app/pages/devices.rs:1194 +#: crates/pdfs-gui/src/app/pages/devices.rs:826 +#: crates/pdfs-gui/src/app/pages/devices.rs:979 +#: crates/pdfs-gui/src/app/pages/devices.rs:1017 +#: crates/pdfs-gui/src/app/pages/devices.rs:1069 +#: crates/pdfs-gui/src/app/pages/devices.rs:1113 +#: crates/pdfs-gui/src/app/pages/devices.rs:1153 +#: crates/pdfs-gui/src/app/pages/devices.rs:1216 #: crates/pdfs-gui/src/app/pages/locations.rs:850 #: crates/pdfs-gui/src/app/pages/locations.rs:1054 #: crates/pdfs-gui/src/app/pages/locations.rs:1537 @@ -346,7 +346,7 @@ msgstr "Grade / lista" #: crates/pdfs-gui/src/app/pages/albums.rs:298 #: crates/pdfs-gui/src/app/pages/browser.rs:1896 #: crates/pdfs-gui/src/app/pages/browser.rs:1918 -#: crates/pdfs-gui/src/app/pages/devices.rs:996 +#: crates/pdfs-gui/src/app/pages/devices.rs:1018 #: crates/pdfs-gui/src/app/widgets/details.rs:161 msgid "Rename" msgstr "Renomear" @@ -678,7 +678,7 @@ msgid "Couldn't add a sync folder" msgstr "Não foi possível adicionar uma pasta sincronizada" #: crates/pdfs-gui/src/app/pages/activity_text.rs:58 -#: crates/pdfs-gui/src/app/pages/devices.rs:839 +#: crates/pdfs-gui/src/app/pages/devices.rs:861 msgid "Couldn't restore folders" msgstr "Não foi possível restaurar as pastas" @@ -1270,12 +1270,12 @@ msgstr "Não foi possível carregar os álbuns" #: crates/pdfs-gui/src/app/pages/browser.rs:1873 #: crates/pdfs-gui/src/app/pages/browser.rs:2500 #: crates/pdfs-gui/src/app/pages/browser.rs:2650 -#: crates/pdfs-gui/src/app/pages/devices.rs:707 -#: crates/pdfs-gui/src/app/pages/devices.rs:747 -#: crates/pdfs-gui/src/app/pages/devices.rs:900 -#: crates/pdfs-gui/src/app/pages/devices.rs:1170 -#: crates/pdfs-gui/src/app/pages/devices.rs:1231 -#: crates/pdfs-gui/src/app/pages/devices.rs:1260 +#: crates/pdfs-gui/src/app/pages/devices.rs:729 +#: crates/pdfs-gui/src/app/pages/devices.rs:769 +#: crates/pdfs-gui/src/app/pages/devices.rs:922 +#: crates/pdfs-gui/src/app/pages/devices.rs:1192 +#: crates/pdfs-gui/src/app/pages/devices.rs:1253 +#: crates/pdfs-gui/src/app/pages/devices.rs:1282 #: crates/pdfs-gui/src/app/pages/duplicates.rs:221 #: crates/pdfs-gui/src/app/pages/locations.rs:577 #: crates/pdfs-gui/src/app/pages/locations.rs:824 @@ -1921,7 +1921,7 @@ msgid "{status}. {message}" msgstr "{status}. {message}" #: crates/pdfs-gui/src/app/pages/browser.rs:1900 -#: crates/pdfs-gui/src/app/pages/devices.rs:989 +#: crates/pdfs-gui/src/app/pages/devices.rs:1011 msgid "New name" msgstr "Novo nome" @@ -1946,7 +1946,7 @@ msgstr "Escolha a pasta de destino." #: crates/pdfs-gui/src/app/pages/browser.rs:1989 #: crates/pdfs-gui/src/app/pages/browser.rs:1994 -#: crates/pdfs-gui/src/app/pages/devices.rs:1195 +#: crates/pdfs-gui/src/app/pages/devices.rs:1217 msgid "Move Here" msgstr "Mover para cá" @@ -2268,76 +2268,87 @@ msgid_plural "{n} folders backed up" msgstr[0] "{n} pasta com backup" msgstr[1] "{n} pastas com backup" -#: crates/pdfs-gui/src/app/pages/devices.rs:608 +#: crates/pdfs-gui/src/app/pages/devices.rs:607 +#, rust-format +msgid "{folders} · {n} waiting for its first sync" +msgid_plural "{folders} · {n} waiting for their first sync" +msgstr[0] "{folders} · {n} aguarda a primeira sincronização" +msgstr[1] "{folders} · {n} aguardam a primeira sincronização" + +#: crates/pdfs-gui/src/app/pages/devices.rs:621 #, rust-format msgid "{folders} · {n} needs attention" msgid_plural "{folders} · {n} need attention" msgstr[0] "{folders} · {n} requer atenção" msgstr[1] "{folders} · {n} requerem atenção" -#: crates/pdfs-gui/src/app/pages/devices.rs:619 +#: crates/pdfs-gui/src/app/pages/devices.rs:640 msgid "syncing…" msgstr "sincronizando…" -#: crates/pdfs-gui/src/app/pages/devices.rs:620 +#: crates/pdfs-gui/src/app/pages/devices.rs:641 msgid "sync error" msgstr "erro de sincronização" -#: crates/pdfs-gui/src/app/pages/devices.rs:621 +#: crates/pdfs-gui/src/app/pages/devices.rs:642 msgid "needs attention" msgstr "requer atenção" -#: crates/pdfs-gui/src/app/pages/devices.rs:622 +#: crates/pdfs-gui/src/app/pages/devices.rs:643 +msgid "waiting for first sync" +msgstr "aguardando a primeira sincronização" + +#: crates/pdfs-gui/src/app/pages/devices.rs:644 msgid "up to date" msgstr "atualizado" -#: crates/pdfs-gui/src/app/pages/devices.rs:632 +#: crates/pdfs-gui/src/app/pages/devices.rs:654 msgid "checking for changes…" msgstr "verificando alterações…" -#: crates/pdfs-gui/src/app/pages/devices.rs:638 +#: crates/pdfs-gui/src/app/pages/devices.rs:660 #, rust-format msgid "checking for changes — {n} item checked" msgid_plural "checking for changes — {n} items checked" msgstr[0] "verificando alterações — {n} item verificado" msgstr[1] "verificando alterações — {n} itens verificados" -#: crates/pdfs-gui/src/app/pages/devices.rs:645 +#: crates/pdfs-gui/src/app/pages/devices.rs:667 #, rust-format msgid "checking for changes — {done} of {total}" msgstr "verificando alterações — {done} de {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:657 +#: crates/pdfs-gui/src/app/pages/devices.rs:679 #, rust-format msgid "syncing {done} of {total}" msgstr "sincronizando {done} de {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:663 +#: crates/pdfs-gui/src/app/pages/devices.rs:685 #, rust-format msgid "syncing {file} — {done} of {total}" msgstr "sincronizando {file} — {done} de {total}" -#: crates/pdfs-gui/src/app/pages/devices.rs:675 +#: crates/pdfs-gui/src/app/pages/devices.rs:697 msgid "Add Folder to Sync" msgstr "Adicionar pasta à sincronização" -#: crates/pdfs-gui/src/app/pages/devices.rs:699 +#: crates/pdfs-gui/src/app/pages/devices.rs:721 #: crates/pdfs-gui/src/app/pages/locations.rs:1559 #: crates/pdfs-gui/src/app/pages/locations.rs:1686 msgid "Syncing folder…" msgstr "Sincronizando pasta…" -#: crates/pdfs-gui/src/app/pages/devices.rs:702 -#: crates/pdfs-gui/src/app/pages/devices.rs:706 +#: crates/pdfs-gui/src/app/pages/devices.rs:724 +#: crates/pdfs-gui/src/app/pages/devices.rs:728 msgid "Couldn't add folder" msgstr "Não foi possível adicionar a pasta" -#: crates/pdfs-gui/src/app/pages/devices.rs:740 -#: crates/pdfs-gui/src/app/pages/devices.rs:746 +#: crates/pdfs-gui/src/app/pages/devices.rs:762 +#: crates/pdfs-gui/src/app/pages/devices.rs:768 msgid "Couldn't list folders to restore" msgstr "Não foi possível listar as pastas para restaurar" -#: crates/pdfs-gui/src/app/pages/devices.rs:772 +#: crates/pdfs-gui/src/app/pages/devices.rs:794 msgid "" "Tick the folders to sync to this machine, and adjust where each one should " "live." @@ -2345,19 +2356,19 @@ msgstr "" "Marque as pastas a sincronizar com esta máquina e ajuste onde cada uma deve " "ficar." -#: crates/pdfs-gui/src/app/pages/devices.rs:798 +#: crates/pdfs-gui/src/app/pages/devices.rs:820 msgid "Restore Folders" msgstr "Restaurar pastas" -#: crates/pdfs-gui/src/app/pages/devices.rs:805 +#: crates/pdfs-gui/src/app/pages/devices.rs:827 msgid "Restore" msgstr "Restaurar" -#: crates/pdfs-gui/src/app/pages/devices.rs:838 +#: crates/pdfs-gui/src/app/pages/devices.rs:860 msgid "Restoring folders…" msgstr "Restaurando pastas…" -#: crates/pdfs-gui/src/app/pages/devices.rs:850 +#: crates/pdfs-gui/src/app/pages/devices.rs:872 #, rust-format msgid "" "These folders are backed up from “{name}”. Restored folders keep syncing " @@ -2366,36 +2377,36 @@ msgstr "" "Estas pastas têm backup a partir de “{name}”. As pastas restauradas " "continuam sincronizando com a cópia daquele computador." -#: crates/pdfs-gui/src/app/pages/devices.rs:853 +#: crates/pdfs-gui/src/app/pages/devices.rs:875 msgid "These folders are backed up under this computer in Proton Drive." msgstr "Estas pastas têm backup sob este computador no Proton Drive." -#: crates/pdfs-gui/src/app/pages/devices.rs:862 +#: crates/pdfs-gui/src/app/pages/devices.rs:884 #, rust-format msgid "Nothing to restore — “{name}”'s folders are all synced here." msgstr "" "Nada a restaurar — as pastas de “{name}” já estão todas sincronizadas aqui." -#: crates/pdfs-gui/src/app/pages/devices.rs:865 +#: crates/pdfs-gui/src/app/pages/devices.rs:887 msgid "Nothing to restore — this computer's folders are all synced here." msgstr "" "Nada a restaurar — as pastas deste computador já estão todas sincronizadas " "aqui." -#: crates/pdfs-gui/src/app/pages/devices.rs:889 +#: crates/pdfs-gui/src/app/pages/devices.rs:911 msgid "Folder will switch to online only" msgstr "A pasta passará a ser somente online" -#: crates/pdfs-gui/src/app/pages/devices.rs:891 +#: crates/pdfs-gui/src/app/pages/devices.rs:913 msgid "Folder will download and stay synced" msgstr "A pasta será baixada e permanecerá sincronizada" -#: crates/pdfs-gui/src/app/pages/devices.rs:895 -#: crates/pdfs-gui/src/app/pages/devices.rs:899 +#: crates/pdfs-gui/src/app/pages/devices.rs:917 +#: crates/pdfs-gui/src/app/pages/devices.rs:921 msgid "Couldn't change mode" msgstr "Não foi possível alterar o modo" -#: crates/pdfs-gui/src/app/pages/devices.rs:931 +#: crates/pdfs-gui/src/app/pages/devices.rs:953 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2412,7 +2423,7 @@ msgstr "" "uma cópia local, cancele, mude a pasta para Sincronizada e aguarde o " "download terminar antes de removê-la." -#: crates/pdfs-gui/src/app/pages/devices.rs:937 +#: crates/pdfs-gui/src/app/pages/devices.rs:959 #, rust-format msgid "" "Stop syncing “{path}”?\n" @@ -2425,68 +2436,68 @@ msgstr "" "Os arquivos locais permanecem neste disco e simplesmente deixam de ser " "sincronizados. Escolha se também deseja excluir a cópia no Proton Drive." -#: crates/pdfs-gui/src/app/pages/devices.rs:942 +#: crates/pdfs-gui/src/app/pages/devices.rs:964 msgid "Stop Syncing Folder?" msgstr "Parar de sincronizar a pasta?" -#: crates/pdfs-gui/src/app/pages/devices.rs:947 +#: crates/pdfs-gui/src/app/pages/devices.rs:969 msgid "Also delete from Proton Drive" msgstr "Também excluir do Proton Drive" -#: crates/pdfs-gui/src/app/pages/devices.rs:949 +#: crates/pdfs-gui/src/app/pages/devices.rs:971 msgid "Deletes the only copy of these files." msgstr "Exclui a única cópia destes arquivos." -#: crates/pdfs-gui/src/app/pages/devices.rs:951 +#: crates/pdfs-gui/src/app/pages/devices.rs:973 msgid "The local copy is unaffected." msgstr "A cópia local não é afetada." -#: crates/pdfs-gui/src/app/pages/devices.rs:958 +#: crates/pdfs-gui/src/app/pages/devices.rs:980 msgid "Stop Syncing" msgstr "Parar de sincronizar" -#: crates/pdfs-gui/src/app/pages/devices.rs:971 +#: crates/pdfs-gui/src/app/pages/devices.rs:993 msgid "Stopped syncing folder" msgstr "A pasta deixou de ser sincronizada" -#: crates/pdfs-gui/src/app/pages/devices.rs:972 +#: crates/pdfs-gui/src/app/pages/devices.rs:994 msgid "Couldn't stop syncing the folder" msgstr "Não foi possível parar de sincronizar a pasta" -#: crates/pdfs-gui/src/app/pages/devices.rs:983 +#: crates/pdfs-gui/src/app/pages/devices.rs:1005 msgid "Rename Computer" msgstr "Renomear computador" #. Translators: {name} is the computer's current name. -#: crates/pdfs-gui/src/app/pages/devices.rs:985 +#: crates/pdfs-gui/src/app/pages/devices.rs:1007 #, rust-format msgid "Rename “{name}”." msgstr "Renomear “{name}”." -#: crates/pdfs-gui/src/app/pages/devices.rs:1010 -#: crates/pdfs-gui/src/app/pages/devices.rs:1022 +#: crates/pdfs-gui/src/app/pages/devices.rs:1032 +#: crates/pdfs-gui/src/app/pages/devices.rs:1044 msgid "Couldn't rename the computer" msgstr "Não foi possível renomear o computador" -#: crates/pdfs-gui/src/app/pages/devices.rs:1011 +#: crates/pdfs-gui/src/app/pages/devices.rs:1033 msgid "A name is required." msgstr "Um nome é obrigatório." -#: crates/pdfs-gui/src/app/pages/devices.rs:1021 +#: crates/pdfs-gui/src/app/pages/devices.rs:1043 msgid "Computer renamed" msgstr "Computador renomeado" #. Translators: {name} is the computer's name, which the user must type. -#: crates/pdfs-gui/src/app/pages/devices.rs:1034 +#: crates/pdfs-gui/src/app/pages/devices.rs:1056 #, rust-format msgid "Type “{name}” to confirm" msgstr "Digite “{name}” para confirmar" -#: crates/pdfs-gui/src/app/pages/devices.rs:1039 +#: crates/pdfs-gui/src/app/pages/devices.rs:1061 msgid "Remove Computer?" msgstr "Remover computador?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1042 +#: crates/pdfs-gui/src/app/pages/devices.rs:1064 #, rust-format msgid "" "Remove “{name}” from this account?\n" @@ -2501,26 +2512,26 @@ msgstr "" "naquele próprio computador não são tocados — mas isso não pode ser desfeito " "daqui." -#: crates/pdfs-gui/src/app/pages/devices.rs:1048 +#: crates/pdfs-gui/src/app/pages/devices.rs:1070 #: crates/pdfs-gui/src/app/pages/shared.rs:1025 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:634 #: crates/pdfs-gui/src/app/widgets/share_dialog.rs:876 msgid "Remove" msgstr "Remover" -#: crates/pdfs-gui/src/app/pages/devices.rs:1065 +#: crates/pdfs-gui/src/app/pages/devices.rs:1087 msgid "Computer removed" msgstr "Computador removido" -#: crates/pdfs-gui/src/app/pages/devices.rs:1066 +#: crates/pdfs-gui/src/app/pages/devices.rs:1088 msgid "Couldn't remove the computer" msgstr "Não foi possível remover o computador" -#: crates/pdfs-gui/src/app/pages/devices.rs:1084 +#: crates/pdfs-gui/src/app/pages/devices.rs:1106 msgid "Continue This Backup Here?" msgstr "Continuar este backup aqui?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1087 +#: crates/pdfs-gui/src/app/pages/devices.rs:1109 #, rust-format msgid "" "This computer takes over the backup of “{name}”, for example after a " @@ -2539,25 +2550,25 @@ msgstr "" "movidas. Depois, use “Restaurar neste computador” para recuperar o que " "“{name}” sincronizava." -#: crates/pdfs-gui/src/app/pages/devices.rs:1092 +#: crates/pdfs-gui/src/app/pages/devices.rs:1114 msgid "Continue Here" msgstr "Continuar aqui" #. Translators: {name} is the name of the computer whose backup this one took over. -#: crates/pdfs-gui/src/app/pages/devices.rs:1099 +#: crates/pdfs-gui/src/app/pages/devices.rs:1121 #, rust-format msgid "Now backing up as “{name}”" msgstr "Agora fazendo backup como “{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1108 +#: crates/pdfs-gui/src/app/pages/devices.rs:1130 msgid "Couldn't continue that backup here" msgstr "Não foi possível continuar esse backup aqui" -#: crates/pdfs-gui/src/app/pages/devices.rs:1124 +#: crates/pdfs-gui/src/app/pages/devices.rs:1146 msgid "Sync With This Computer?" msgstr "Sincronizar com este computador?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1127 +#: crates/pdfs-gui/src/app/pages/devices.rs:1149 #, rust-format msgid "" "This computer and “{name}” keep the same folders in step. A change or a " @@ -2574,25 +2585,25 @@ msgstr "" "Drive. As pastas já sincronizadas aqui ficam onde estão. Em seguida, você " "escolhe quais pastas dele sincronizar aqui." -#: crates/pdfs-gui/src/app/pages/devices.rs:1132 +#: crates/pdfs-gui/src/app/pages/devices.rs:1154 msgid "Sync Here" msgstr "Sincronizar aqui" #. Translators: {name} is the name of the other computer this one now syncs with. -#: crates/pdfs-gui/src/app/pages/devices.rs:1139 +#: crates/pdfs-gui/src/app/pages/devices.rs:1161 #, rust-format msgid "Now syncing with “{name}”" msgstr "Agora sincronizando com “{name}”" -#: crates/pdfs-gui/src/app/pages/devices.rs:1155 +#: crates/pdfs-gui/src/app/pages/devices.rs:1177 msgid "Couldn't sync with that computer" msgstr "Não foi possível sincronizar com esse computador" -#: crates/pdfs-gui/src/app/pages/devices.rs:1187 +#: crates/pdfs-gui/src/app/pages/devices.rs:1209 msgid "Move Backup to This Computer?" msgstr "Mover o backup para este computador?" -#: crates/pdfs-gui/src/app/pages/devices.rs:1190 +#: crates/pdfs-gui/src/app/pages/devices.rs:1212 #, rust-format msgid "" "The folders backed up by “{name}” move into this computer's backup in Proton " @@ -2609,12 +2620,12 @@ msgstr "" "pastas sumirem." #. Translators: {name} is the name of the computer whose folders moved to this one. -#: crates/pdfs-gui/src/app/pages/devices.rs:1202 +#: crates/pdfs-gui/src/app/pages/devices.rs:1224 #, rust-format msgid "Moved the backup of “{name}”" msgstr "Backup de “{name}” movido" -#: crates/pdfs-gui/src/app/pages/devices.rs:1216 +#: crates/pdfs-gui/src/app/pages/devices.rs:1238 msgid "Couldn't move that backup" msgstr "Não foi possível mover esse backup" @@ -3351,7 +3362,7 @@ msgid "paused" msgstr "pausada" #. Translators: a folder's files can't be opened right now. -#: crates/pdfs-gui/src/app/pages/locations.rs:1742 +#: crates/pdfs-gui/src/app/pages/locations.rs:1745 msgid "not available" msgstr "indisponível" From e9577f953fb15745496094d3c144f817adb87a9a Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 31 Oct 2026 10:49:01 +0100 Subject: [PATCH 06/12] fix(cli,core,fuse,gui): say why the mount keeps failing, and back off A mount that failed on every try retried every 5 s forever, and the reason was only in the journal: the app and the tray said Connecting... for days (B263, issue #40). The retry now doubles to a 2-minute ceiling, and the last failure is kept in mount-failure.json for the app, the tray, pdfs diagnose and systemctl status. The early returns in mount left the workers they had started running beside the next attempt (B264). They are now stopped on every error path, without telling systemd the daemon is stopping. --- crates/pdfs-cli/src/main.rs | 80 +++++++++++++++++++++++-- crates/pdfs-core/src/lib.rs | 1 + crates/pdfs-core/src/mountfail.rs | 75 +++++++++++++++++++++++ crates/pdfs-fuse/src/lib.rs | 2 +- crates/pdfs-fuse/src/mount.rs | 99 ++++++++++++++++++++++--------- crates/pdfs-fuse/src/systemd.rs | 10 +++- crates/pdfs-gui/src/app/main.rs | 36 ++++++++--- crates/pdfs-gui/src/main.rs | 18 ++++++ docs/BUGS.md | 38 +++++++++--- po/de.po | 74 ++++++++++++++--------- po/en_GB.po | 74 ++++++++++++++--------- po/es.po | 74 ++++++++++++++--------- po/fr.po | 74 ++++++++++++++--------- po/it.po | 74 ++++++++++++++--------- po/nl.po | 74 ++++++++++++++--------- po/pdfs.pot | 74 ++++++++++++++--------- po/pl.po | 74 ++++++++++++++--------- po/pt_BR.po | 74 ++++++++++++++--------- 18 files changed, 714 insertions(+), 311 deletions(-) create mode 100644 crates/pdfs-core/src/mountfail.rs diff --git a/crates/pdfs-cli/src/main.rs b/crates/pdfs-cli/src/main.rs index 75f6e3b..3076f68 100644 --- a/crates/pdfs-cli/src/main.rs +++ b/crates/pdfs-cli/src/main.rs @@ -2375,12 +2375,21 @@ where /// an external unmount triggers a remount; errors back off and retry. This is /// the entry point for the systemd user service. fn cmd_daemon(mountpoint: Option) -> Result<()> { + let state_dir = AppDirs::new()?.state_dir(); + // A failure an earlier daemon recorded is not this one's. + pdfs_core::mountfail::clear(&state_dir); + let mut failure: Option = None; loop { // Wait until a session is stored. The GUI enables this service on login, // but the service may also start at boot before the user has logged in. wait_for_session(auth::ready, pdfs_fuse::idle_sleep)?; - match mount_once(mountpoint.clone()) { + let outcome = mount_once(mountpoint.clone()); + if outcome.is_ok() { + // The mount came up, so the next failure starts a new run of them. + failure = None; + } + match outcome { Ok(pdfs_fuse::MountOutcome::Shutdown) => { tracing::info!("daemon stopping"); return Ok(()); @@ -2395,13 +2404,51 @@ fn cmd_daemon(mountpoint: Option) -> Result<()> { Err(e) => { // `{:#}` so the whole anyhow chain lands in the log — the top // context alone ("mount failed") never says *why*. - tracing::error!(error = format!("{e:#}"), "mount failed; retrying in 5s"); - pdfs_fuse::idle_sleep(std::time::Duration::from_secs(5)); + let error = format!("{e:#}"); + let now = unix_now(); + let attempts = failure.as_ref().map_or(1, |f| f.attempts.saturating_add(1)); + let delay = mount_retry_delay(attempts); + tracing::error!( + error, + attempts, + retry_secs = delay.as_secs(), + "mount failed; retrying" + ); + let record = pdfs_core::mountfail::MountFailure { + error: error.clone(), + since: failure.as_ref().map_or(now, |f| f.since), + attempts, + next_attempt: now + delay.as_secs() as i64, + }; + if let Err(e) = pdfs_core::mountfail::record(&state_dir, &record) { + tracing::warn!(error = %e, "could not record the mount failure"); + } + pdfs_fuse::notify_status(&format!("Mount failing: {error}")); + failure = Some(record); + pdfs_fuse::idle_sleep(delay); } } } } +/// How long to wait before mount attempt `attempts + 1`, after `attempts` +/// failures in a row. +/// +/// Doubles from 5 s to a 2-minute ceiling. A flat 5 s retry sent about four +/// requests to Proton per attempt, tens of thousands a day from an account +/// whose mount fails on a key, which no retry fixes (`docs/BUGS.md` B263). The +/// ceiling stays at minutes because a cold start without a network fails the +/// same way, and must mount soon after the network returns. +fn mount_retry_delay(attempts: u32) -> std::time::Duration { + const BASE_SECS: u64 = 5; + const MAX_SECS: u64 = 120; + + let factor = 1u64 + .checked_shl(attempts.saturating_sub(1)) + .unwrap_or(u64::MAX); + std::time::Duration::from_secs(BASE_SECS.saturating_mul(factor).min(MAX_SECS)) +} + /// One mount run: resume the session, mount, and block until the mount ends /// (clean shutdown or external unmount). Returns the [`MountOutcome`] so the /// daemon loop can decide whether to exit or remount. @@ -3857,7 +3904,23 @@ fn cmd_diagnose() -> Result<()> { " daemon responding", format!("stopped or unreachable: {e:#}"), ); - println!(" (start it with `systemctl --user start proton-drive.service`)"); + match pdfs_core::mountfail::read(&dirs.state_dir()) { + // A daemon whose mount keeps failing never opens the socket, + // so this is the only place its reason shows (B263). + Some(failure) => report.finding( + DiagnoseLevel::Fail, + " mount", + format!( + "failing for {}, {} attempt(s): {}", + human_secs(unix_now().saturating_sub(failure.since).max(0) as u64), + failure.attempts, + failure.error + ), + ), + None => println!( + " (start it with `systemctl --user start proton-drive.service`)" + ), + } } } @@ -4017,6 +4080,15 @@ mod diagnose_tests { #[cfg(test)] mod login_wait_tests { use super::*; + + /// The mount retry backs off from 5 s to a ceiling of minutes, rather than + /// asking Proton every 5 s forever (`docs/BUGS.md` B263). + #[test] + fn a_failing_mount_backs_off_to_a_ceiling() { + let secs: Vec = (1..=8).map(|n| mount_retry_delay(n).as_secs()).collect(); + assert_eq!(secs, [5, 10, 20, 40, 80, 120, 120, 120]); + assert_eq!(mount_retry_delay(u32::MAX).as_secs(), 120); + } use std::time::Duration; #[test] diff --git a/crates/pdfs-core/src/lib.rs b/crates/pdfs-core/src/lib.rs index ed19c4f..0f79ec4 100644 --- a/crates/pdfs-core/src/lib.rs +++ b/crates/pdfs-core/src/lib.rs @@ -15,6 +15,7 @@ pub mod kernelkey; pub mod keycheck; pub mod localindex; pub mod menu; +pub mod mountfail; pub mod mounts; pub mod opener; pub mod places; diff --git a/crates/pdfs-core/src/mountfail.rs b/crates/pdfs-core/src/mountfail.rs new file mode 100644 index 0000000..130a155 --- /dev/null +++ b/crates/pdfs-core/src/mountfail.rs @@ -0,0 +1,75 @@ +//! Why the daemon's mount keeps failing, for the front ends to read. +//! +//! The control socket opens only once a mount has succeeded, so a daemon whose +//! mount fails on every try had no way to say why. The app and the tray showed +//! "Connecting…" for days while the reason sat in the journal (`docs/BUGS.md` +//! B263). The daemon now writes its last failure to a small file in the state +//! directory, next to the control socket, and removes it once a mount is up. + +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; + +/// The file's name inside the state directory. +const FILE_NAME: &str = "mount-failure.json"; + +/// The last failed mount attempt. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct MountFailure { + /// The error, in English, as the journal has it. + pub error: String, + /// Unix seconds of the first failure in this run of failures. + pub since: i64, + /// Failed attempts in a row. + pub attempts: u32, + /// Unix seconds of the next attempt. + pub next_attempt: i64, +} + +/// Where the failure is kept for the state directory `state_dir`. +pub fn path(state_dir: &Path) -> PathBuf { + state_dir.join(FILE_NAME) +} + +/// Record `failure`, replacing the one before it. Written to a temporary file +/// and renamed, so a reader never sees half of it. +pub fn record(state_dir: &Path, failure: &MountFailure) -> std::io::Result<()> { + let json = serde_json::to_vec(failure).map_err(std::io::Error::other)?; + let tmp = state_dir.join(format!("{FILE_NAME}.tmp")); + std::fs::write(&tmp, json)?; + std::fs::rename(&tmp, path(state_dir)) +} + +/// Forget the failure: a mount is up, or a new daemon has started. +pub fn clear(state_dir: &Path) { + let _ = std::fs::remove_file(path(state_dir)); +} + +/// The recorded failure, if there is one and it can be read. +pub fn read(state_dir: &Path) -> Option { + let bytes = std::fs::read(path(state_dir)).ok()?; + serde_json::from_slice(&bytes).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_recorded_failure_reads_back_until_cleared() { + let dir = std::env::temp_dir().join(format!("pdfs-mountfail-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + assert_eq!(read(&dir), None); + let failure = MountFailure { + error: "fetch My Files root: missing key".into(), + since: 1_700_000_000, + attempts: 3, + next_attempt: 1_700_000_040, + }; + record(&dir, &failure).unwrap(); + assert_eq!(read(&dir), Some(failure)); + clear(&dir); + assert_eq!(read(&dir), None); + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/crates/pdfs-fuse/src/lib.rs b/crates/pdfs-fuse/src/lib.rs index d9bbb0e..75dc15b 100644 --- a/crates/pdfs-fuse/src/lib.rs +++ b/crates/pdfs-fuse/src/lib.rs @@ -124,7 +124,7 @@ use mount::{ }; use reads::{BlockFlight, BlockRing, PREFETCH_BUDGET, Prefetch, ReaderSlot, STREAM_BYPASS_MIN}; use state::{Entry, Intervals, PendingRevision, State, StateGuard, WriteHandle}; -pub use systemd::idle_sleep; +pub use systemd::{idle_sleep, status as notify_status}; use tracing::{debug, error, info, warn}; use transfers::{CountingWriter, JobGuard, OwnedCountingReader, TransferRegistry}; use r#virtual::{ diff --git a/crates/pdfs-fuse/src/mount.rs b/crates/pdfs-fuse/src/mount.rs index 1aeccdc..a35c3a5 100644 --- a/crates/pdfs-fuse/src/mount.rs +++ b/crates/pdfs-fuse/src/mount.rs @@ -540,12 +540,11 @@ pub(crate) fn mount_with( // and sweeps all operating on a mount that no longer exists (`docs/BUGS.md` B44). let mut workers: Vec> = Vec::new(); for worker in 0..DRAIN_WORKERS { - let core = core.clone(); - workers.push( - std::thread::Builder::new() - .name(format!("pdfs-drain-{worker}")) - .spawn(move || core.run_pending_drain(worker == 0))?, - ); + let drain_core = core.clone(); + let spawned = std::thread::Builder::new() + .name(format!("pdfs-drain-{worker}")) + .spawn(move || drain_core.run_pending_drain(worker == 0)); + keep_or_abandon(&core, control_socket, &mut workers, spawned)?; } // Start the folder-sync engine. It watches every mirror folder, polls the @@ -565,24 +564,22 @@ pub(crate) fn mount_with( info!("conflict sweep disabled by configuration"); } else { info!(mode = ?sweep_mode, "conflict sweep enabled"); - let core = core.clone(); - workers.push( - std::thread::Builder::new() - .name("pdfs-conflict-sweep".into()) - .spawn(move || core.run_conflict_sweep_loop())?, - ); + let sweep_core = core.clone(); + let spawned = std::thread::Builder::new() + .name("pdfs-conflict-sweep".into()) + .spawn(move || sweep_core.run_conflict_sweep_loop()); + keep_or_abandon(&core, control_socket, &mut workers, spawned)?; } // Watch the link for the life of the mount: a mount from the cache waits for // the network to come back, and an online one can lose it at any moment and // must notice it coming back just the same. { - let core = core.clone(); - workers.push( - std::thread::Builder::new() - .name("pdfs-online-probe".into()) - .spawn(move || core.run_online_probe())?, - ); + let probe_core = core.clone(); + let spawned = std::thread::Builder::new() + .name("pdfs-online-probe".into()) + .spawn(move || probe_core.run_online_probe()); + keep_or_abandon(&core, control_socket, &mut workers, spawned)?; } // Keep the launcher prompt's "This computer" index fresh. Its own thread: @@ -593,11 +590,10 @@ pub(crate) fn mount_with( let transfers = core.transfers.clone(); let mountpoint = mountpoint.to_path_buf(); let shutdown = core.shutdown.clone(); - workers.push( - std::thread::Builder::new() - .name("pdfs-localindex".into()) - .spawn(move || run_local_index(db, indexing, transfers, mountpoint, shutdown))?, - ); + let spawned = std::thread::Builder::new() + .name("pdfs-localindex".into()) + .spawn(move || run_local_index(db, indexing, transfers, mountpoint, shutdown)); + keep_or_abandon(&core, control_socket, &mut workers, spawned)?; } // Bind the control socket before the FUSE session takes over the thread. A @@ -606,12 +602,18 @@ pub(crate) fn mount_with( let old_umask = unsafe { libc::umask(0o77) }; let listener_res = UnixListener::bind(control_socket); unsafe { libc::umask(old_umask) }; - let listener = listener_res?; + let listener = match listener_res { + Ok(listener) => listener, + Err(error) => { + abandon_workers(&core, control_socket, workers); + return Err(error); + } + }; // Owner-only before anything can connect: a peer on this socket commands the // daemon's authenticated session without a credential of its own (B6). if let Err(e) = pdfs_core::config::restrict_socket(control_socket) { error!(error = %e, "cannot restrict control socket permissions; refusing to serve"); - let _ = std::fs::remove_file(control_socket); + abandon_workers(&core, control_socket, workers); return Err(std::io::Error::other(format!( "control socket permissions: {e}" ))); @@ -622,7 +624,7 @@ pub(crate) fn mount_with( let bg = match spawn_session(&core, mountpoint, root) { Ok(session) => session, Err(error) => { - let _ = std::fs::remove_file(control_socket); + abandon_workers(&core, control_socket, workers); return Err(error); } }; @@ -642,7 +644,7 @@ pub(crate) fn mount_with( Err(error) => { // A failure here abandons the mount, so the background threads // started above have to be told to stop with it. - stop_workers(&core, control_socket, workers); + abandon_workers(&core, control_socket, workers); let secondaries = core.mounts.lock().close_and_drain(); for (id, mount) in secondaries { if let Err(unmount_error) = mount.teardown() { @@ -692,8 +694,12 @@ pub(crate) fn mount_with( } } // The mount is live and the control socket answers: this is the moment the - // unit is actually usable, which is what `Type=notify` reports. + // unit is actually usable, which is what `Type=notify` reports. Whatever + // failure an earlier attempt recorded for the front ends is over. systemd::ready(); + if let Some(state_dir) = control_socket.parent() { + pdfs_core::mountfail::clear(state_dir); + } // Re-establish on-demand mounts only after the primary session is live. // Keep the worker so shutdown can close publication, wait for any in-flight @@ -859,6 +865,43 @@ fn stop_workers(core: &Core, control_socket: &Path, workers: Vec>) { + halt_workers(core, control_socket, workers); +} + +/// Keep a worker the mount just spawned, or, if the spawn failed, stop the +/// ones already running and hand the error back. +fn keep_or_abandon( + core: &Core, + control_socket: &Path, + workers: &mut Vec>, + spawned: std::io::Result>, +) -> std::io::Result<()> { + match spawned { + Ok(handle) => { + workers.push(handle); + Ok(()) + } + Err(error) => { + abandon_workers(core, control_socket, std::mem::take(workers)); + Err(error) + } + } +} + +/// Signal every worker to stop, then join them up to [`WORKER_JOIN_DEADLINE`]. +fn halt_workers(core: &Core, control_socket: &Path, workers: Vec>) { core.shutdown.stop(); core.wake_drain(); let _ = core.sync_tx.send(sync::SyncMsg::Stop); diff --git a/crates/pdfs-fuse/src/systemd.rs b/crates/pdfs-fuse/src/systemd.rs index 22683bc..f54091e 100644 --- a/crates/pdfs-fuse/src/systemd.rs +++ b/crates/pdfs-fuse/src/systemd.rs @@ -64,11 +64,19 @@ fn send_abstract(socket: &UnixDatagram, name: &str, message: &str) -> bool { /// so `systemctl --user start` returns when the mount actually works rather than /// when the process exists. pub(crate) fn ready() { - if notify("READY=1\n") { + if notify("READY=1\nSTATUS=Mounted\n") { debug!("notified systemd: ready"); } } +/// Set the one-line status `systemctl --user status` shows for the unit, such +/// as why the mount keeps failing (`docs/BUGS.md` B263). +pub fn status(text: &str) { + // A newline would end the `STATUS=` line and start another variable. + let line = text.replace('\n', " "); + notify(&format!("STATUS={line}\n")); +} + /// Tell the manager the daemon is still healthy. pub(crate) fn watchdog_ping() { let _ = notify("WATCHDOG=1\n"); diff --git a/crates/pdfs-gui/src/app/main.rs b/crates/pdfs-gui/src/app/main.rs index 5db14d0..a30fa50 100644 --- a/crates/pdfs-gui/src/app/main.rs +++ b/crates/pdfs-gui/src/app/main.rs @@ -1797,13 +1797,35 @@ pub(crate) fn service_unreachable( ); return; } - status( - &ui, - "network-idle-symbolic", - &gettext("Connecting…"), - &gettext("Waiting for the Proton Drive service to start."), - false, - ); + // Running but not answering may be a mount that fails on every try, + // which never opens the socket. Its reason is in a file the daemon + // keeps for this (B263); "Connecting…" alone looked like a slow start + // for as long as it lasted. + let state_dir = ui.dirs.state_dir(); + let failure = gio::spawn_blocking(move || pdfs_core::mountfail::read(&state_dir)) + .await + .ok() + .flatten(); + match failure { + Some(failure) => status( + &ui, + "dialog-warning-symbolic", + &gettext("Can't open your Drive"), + // Translators: {error} is an error message, in English. + &gettext_f( + "The Proton Drive service keeps failing to start and tries again on its own. The last error was: {error}", + &[("error", &failure.error)], + ), + true, + ), + None => status( + &ui, + "network-idle-symbolic", + &gettext("Connecting…"), + &gettext("Waiting for the Proton Drive service to start."), + false, + ), + } glib::timeout_add_local_once(CONNECT_RETRY_INTERVAL, move || { if ui.stack.visible_child_name().as_deref() == Some(page) { reload(&ui); diff --git a/crates/pdfs-gui/src/main.rs b/crates/pdfs-gui/src/main.rs index 083466e..8471f79 100644 --- a/crates/pdfs-gui/src/main.rs +++ b/crates/pdfs-gui/src/main.rs @@ -194,6 +194,24 @@ fn poll_state(socket: &Path, default_mountpoint: &Path) -> DriveState { ), Phase::Attention, ), + // A daemon whose mount fails on every try never opens the + // socket; the app says why (B263). + Ok(s) + if socket + .parent() + .and_then(pdfs_core::mountfail::read) + .is_some() => + { + ( + // Translators: {username} is the Proton account name. The + // service keeps failing to start; the app shows the error. + gettext_f( + "Signed in as {username} — can't open your Drive", + &[("username", &s.username)], + ), + Phase::Attention, + ) + } Ok(s) => ( // Translators: {username} is the Proton account name. gettext_f( diff --git a/docs/BUGS.md b/docs/BUGS.md index 319b4ca..63536a2 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -77,8 +77,6 @@ original wording. | [B267](#b267--a-key-failure-at-a-start-from-a-warm-cache-shows-as-offline-for-good) | A key failure at a start from a warm cache shows as offline for good | Not fixed | | [B266](#b266--pin-unpin-search-and-node-loads-report-a-database-error-as-offline) | Pin, unpin, search and node loads report a database error as "offline" | Not fixed | | [B265](#b265--address-keys-whose-token-is-for-an-inactive-account-key-never-open) | Address keys whose token is for an inactive account key never open | Needs an SDK change; not reproduced | -| [B264](#b264--a-mount-that-fails-after-starting-its-workers-leaves-them-running) | A mount that fails after starting its workers leaves them running | Not fixed | -| [B263](#b263--a-mount-that-can-never-succeed-retries-every-5-s-and-tells-only-the-journal) | A mount that can never succeed retries every 5 s and tells only the journal | Not fixed | | [B262](#b262--watching-a-large-synced-folder-holds-up-the-sync-engine-and-a-failed-watch-is-never-retried) | Watching a large synced folder holds up the sync engine, and a failed watch is never retried | Not fixed | | [B261](#b261--a-folder-that-has-never-synced-reads-up-to-date) | A folder that has never synced reads "up to date" | Not fixed | | [B260](#b260--the-poll-pushes-a-new-folders-first-sync-behind-every-other-folder) | The poll pushes a new folder's first sync behind every other folder | Not fixed | @@ -96,6 +94,7 @@ original wording. | Entry | Title | What is left | |---|---|---| +| [B263](#b263--a-mount-that-can-never-succeed-retries-every-5-s-and-tells-only-the-journal) | A mount that can never succeed retries every 5 s and tells only the journal | Errors no retry fixes back off like network errors | | [B199](#b199--a-stopped-share-stays-on-the-shared-by-me-page-for-minutes) | A stopped share stays on the Shared by me page for minutes | The share itself is not deleted; the SDK has no call for it | | [B88](#b88--pdfs-pin-rename-move-delete-and-sharing-still-cannot-name-a-path-under-a-secondary-mount) | `pdfs pin`, `rename`, `move`, `delete` and sharing still cannot name a path under a secondary mount | `pin`, `unpin`, opening a file, thumbnails, uploads, sharing and versions | | [B72](#b72--pdfs-trash-never-returns) | `pdfs trash` never returns | The slowness of the first refresh is unmeasured | @@ -116,6 +115,7 @@ a live account since. | Entry | Title | |---|---| +| [B264](#b264--a-mount-that-fails-after-starting-its-workers-leaves-them-running) | A mount that fails after starting its workers leaves them running | | [B256](#b256--pdfs-copy-right-after-a-save-copies-the-old-version) | `pdfs copy` right after a save copies the old version | | [B255](#b255--a-synced-folder-whose-files-are-all-gone-here-is-stuck-for-good) | A synced folder whose files are all gone here is stuck for good | | [B254](#b254--an-edit-in-the-same-second-as-a-new-upload-never-arrives) | An edit in the same second as a new upload never arrives | @@ -567,7 +567,7 @@ tokens, and only the active ones for signing. Skip and log an address that canno ### B264 — A mount that fails after starting its workers leaves them running -**Status:** Open +**Status:** Fixed, not verified live **Found:** 2026-10-31, code audit after issue #40. **Where:** `run_mount` in `crates/pdfs-fuse/src/mount.rs` @@ -581,14 +581,20 @@ permission check, a `spawn_session` error, and a `?` on a later thread spawn) sk the `flock` held, every later attempt fails with "another daemon is using cache.db" instead of the real error. That part is inferred from the code and not observed. -**Fix.** Not done. Bind the socket and check the mountpoint before any worker starts, and stop -the workers on every error path. +**Fix.** Every early return in `mount` after the first worker starts stops the workers +(`abandon_workers`, `keep_or_abandon` in `crates/pdfs-fuse/src/mount.rs`). Unlike a normal stop +it does not send `STOPPING=1`, because the daemon is about to retry and systemd would +otherwise wait for it to exit and then kill it. The control-thread failure path, which already +stopped its workers, no longer sends it either. + +**Test:** none of its own; the simulations (`sim::daemon`, 55 passed) cover the normal start +and stop. --- ### B263 — A mount that can never succeed retries every 5 s and tells only the journal -**Status:** Open +**Status:** Partly fixed. Errors that no retry fixes are not told apart from network errors yet. **Found:** 2026-10-31, issue #40: the app showed "Connecting… waiting for the Proton Drive service" for days, `pdfs diagnose` only reported the control socket missing, and the reporter uninstalled. @@ -605,9 +611,23 @@ four requests, so a broken account sends tens of thousands a day, which may also Proton's sign-in blocks (#39). The control socket is bound only after the mount succeeds, and the error is written nowhere a front end can read it. -**Fix.** Not done. Back off exponentially, treat key, `KeysLocked` and 4xx errors as permanent -with a long back-off, and write the last error where the app, the tray and `diagnose` can -show it (a state file, and `STATUS=` to systemd). +**Fix.** +- The retry doubles from 5 s to a 2-minute ceiling (`mount_retry_delay` in + `crates/pdfs-cli/src/main.rs`), about 720 attempts a day instead of 17,000. The ceiling stays + at minutes because a cold start without a network fails the same way and has to mount soon + after the network returns. +- Each failure is written to `mount-failure.json` in the state directory + (`crates/pdfs-core/src/mountfail.rs`), with the error, since when it fails and the attempt + count. A mount that comes up removes it, and so does a new daemon. +- The app shows "Can't open your Drive" with the error and a Retry button instead of + "Connecting…". The tray says "Signed in as … — can't open your Drive". `pdfs diagnose` reports + `[FAIL] mount: failing for …`. `systemctl --user status` shows the error as the unit's status. +- Not done: key, `KeysLocked` and 4xx errors still retry like network errors, only less often. + +**Test:** `a_failing_mount_backs_off_to_a_ceiling` in `pdfs-cli`; +`a_recorded_failure_reads_back_until_cleared` in `pdfs-core`. On 2026-10-31 a second +`pdfs daemon` beside the running one failed on the database lock three times in 18 s (at 0, 5 +and 15 s) and recorded `attempts: 3` with the lock error. --- diff --git a/po/de.po b/po/de.po index 7ac1044..5f4b380 100644 --- a/po/de.po +++ b/po/de.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:37+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: German\n" @@ -424,14 +424,25 @@ msgstr "Nicht verbunden" msgid "The Proton Drive service isn't running." msgstr "Der Proton-Drive-Dienst läuft nicht." -#: crates/pdfs-gui/src/app/main.rs:1803 +#: crates/pdfs-gui/src/app/main.rs:1813 +msgid "Can't open your Drive" +msgstr "Proton Drive kann nicht geöffnet werden" + +#: crates/pdfs-gui/src/app/main.rs:1816 +#, rust-format +msgid "" +"The Proton Drive service keeps failing to start and tries again on its own. " +"The last error was: {error}" +msgstr "Der Proton-Drive-Dienst kann wiederholt nicht starten und versucht es selbstständig erneut. Letzter Fehler: {error}" + +#: crates/pdfs-gui/src/app/main.rs:1824 #: crates/pdfs-gui/src/app/pages/status.rs:355 #: crates/pdfs-gui/src/hud/mod.rs:269 crates/pdfs-gui/src/hud/mod.rs:579 -#: crates/pdfs-gui/src/main.rs:475 crates/pdfs-gui/src/main.rs:517 +#: crates/pdfs-gui/src/main.rs:493 crates/pdfs-gui/src/main.rs:535 msgid "Connecting…" msgstr "Verbindung wird hergestellt …" -#: crates/pdfs-gui/src/app/main.rs:1804 +#: crates/pdfs-gui/src/app/main.rs:1825 msgid "Waiting for the Proton Drive service to start." msgstr "Warten auf den Start des Proton-Drive-Dienstes." @@ -2908,7 +2919,7 @@ msgid "Sync paused until you resume" msgstr "Synchronisierung bis zur Fortsetzung pausiert" #: crates/pdfs-gui/src/app/pages/locations.rs:570 -#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:290 +#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:308 msgid "Sync paused" msgstr "Synchronisierung pausiert" @@ -5136,7 +5147,7 @@ msgstr "Wird {time} fortgesetzt" msgid "Until you resume" msgstr "Bis zur manuellen Fortsetzung" -#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:292 +#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:310 #, rust-format msgid "{n} change needs attention" msgid_plural "{n} changes need attention" @@ -6371,52 +6382,57 @@ msgstr "Proton Drive reagiert nicht" msgid "Signed in as {username} — locked" msgstr "Angemeldet als {username} – gesperrt" -#: crates/pdfs-gui/src/main.rs:200 +#: crates/pdfs-gui/src/main.rs:209 +#, rust-format +msgid "Signed in as {username} — can't open your Drive" +msgstr "Angemeldet als {username} – Proton Drive kann nicht geöffnet werden" + +#: crates/pdfs-gui/src/main.rs:218 #, rust-format msgid "Signed in as {username} — not connected" msgstr "Angemeldet als {username} – nicht verbunden" -#: crates/pdfs-gui/src/main.rs:205 +#: crates/pdfs-gui/src/main.rs:223 msgid "Not signed in" msgstr "Nicht angemeldet" #. Translators: {error} is an error message, in English. -#: crates/pdfs-gui/src/main.rs:208 +#: crates/pdfs-gui/src/main.rs:226 #, rust-format msgid "Error: {error}" msgstr "Fehler: {error}" -#: crates/pdfs-gui/src/main.rs:230 +#: crates/pdfs-gui/src/main.rs:248 msgid "Proton Drive sent an unexpected reply" msgstr "Proton Drive hat unerwartet geantwortet" -#: crates/pdfs-gui/src/main.rs:284 +#: crates/pdfs-gui/src/main.rs:302 msgid "Session expired — sign in again" msgstr "Sitzung abgelaufen – erneut anmelden" -#: crates/pdfs-gui/src/main.rs:286 +#: crates/pdfs-gui/src/main.rs:304 msgid "Sign-in not saved — unlock your keyring" msgstr "Anmeldung nicht gespeichert – Schlüsselbund entsperren" -#: crates/pdfs-gui/src/main.rs:289 +#: crates/pdfs-gui/src/main.rs:307 msgid "Update not finished — restart Proton Drive" msgstr "Update nicht abgeschlossen – Proton Drive neu starten" -#: crates/pdfs-gui/src/main.rs:299 +#: crates/pdfs-gui/src/main.rs:317 #, rust-format msgid "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgstr[0] "Aktuell in {mountpoint} ({n} angeheftet)" msgstr[1] "Aktuell in {mountpoint} ({n} angeheftet)" -#: crates/pdfs-gui/src/main.rs:306 +#: crates/pdfs-gui/src/main.rs:324 #, rust-format msgid "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)" msgstr[0] "Wird synchronisiert – {queued} ({n} angeheftet)" msgstr[1] "Wird synchronisiert – {queued} ({n} angeheftet)" -#: crates/pdfs-gui/src/main.rs:313 +#: crates/pdfs-gui/src/main.rs:331 #, rust-format msgid "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)" @@ -6424,60 +6440,60 @@ msgstr[0] "Offline – nur zwischengespeicherte Dateien ({n} angeheftet)" msgstr[1] "Offline – nur zwischengespeicherte Dateien ({n} angeheftet)" #. Translators: {queued} is a queue summary such as "3 uploads queued". -#: crates/pdfs-gui/src/main.rs:319 +#: crates/pdfs-gui/src/main.rs:337 #, rust-format msgid "Offline — {queued}" msgstr "Offline – {queued}" #. Translators: a menu item that opens the list of failing changes. -#: crates/pdfs-gui/src/main.rs:358 +#: crates/pdfs-gui/src/main.rs:376 #, rust-format msgid "{n} issue — View" msgid_plural "{n} issues — View" msgstr[0] "{n} Problem – Anzeigen" msgstr[1] "{n} Probleme – Anzeigen" -#: crates/pdfs-gui/src/main.rs:465 +#: crates/pdfs-gui/src/main.rs:483 msgid "Sign In…" msgstr "Anmelden …" -#: crates/pdfs-gui/src/main.rs:466 +#: crates/pdfs-gui/src/main.rs:484 msgid "Sign In Again…" msgstr "Erneut anmelden …" -#: crates/pdfs-gui/src/main.rs:469 +#: crates/pdfs-gui/src/main.rs:487 msgid "Open Proton Drive" msgstr "Proton Drive öffnen" -#: crates/pdfs-gui/src/main.rs:473 +#: crates/pdfs-gui/src/main.rs:491 msgid "Restart Proton Drive" msgstr "Proton Drive neu starten" -#: crates/pdfs-gui/src/main.rs:482 +#: crates/pdfs-gui/src/main.rs:500 msgid "Open Folder" msgstr "Ordner öffnen" -#: crates/pdfs-gui/src/main.rs:494 +#: crates/pdfs-gui/src/main.rs:512 msgid "Resume Sync" msgstr "Synchronisierung fortsetzen" -#: crates/pdfs-gui/src/main.rs:496 +#: crates/pdfs-gui/src/main.rs:514 msgid "Pause Sync" msgstr "Synchronisierung pausieren" -#: crates/pdfs-gui/src/main.rs:515 +#: crates/pdfs-gui/src/main.rs:533 msgid "Connect" msgstr "Verbinden" -#: crates/pdfs-gui/src/main.rs:522 +#: crates/pdfs-gui/src/main.rs:540 msgid "Hide Tray Icon" msgstr "Symbol im Benachrichtigungsfeld ausblenden" -#: crates/pdfs-gui/src/main.rs:526 +#: crates/pdfs-gui/src/main.rs:544 msgid "Stop Proton Drive…" msgstr "Proton Drive beenden …" -#: crates/pdfs-gui/src/main.rs:530 +#: crates/pdfs-gui/src/main.rs:548 msgid "Quit" msgstr "Beenden" diff --git a/po/en_GB.po b/po/en_GB.po index 5a2e3c1..33a2a9c 100644 --- a/po/en_GB.po +++ b/po/en_GB.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:37+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: English (United Kingdom)\n" @@ -420,14 +420,25 @@ msgstr "Not connected" msgid "The Proton Drive service isn't running." msgstr "The Proton Drive service isn't running." -#: crates/pdfs-gui/src/app/main.rs:1803 +#: crates/pdfs-gui/src/app/main.rs:1813 +msgid "Can't open your Drive" +msgstr "Can't open your Drive" + +#: crates/pdfs-gui/src/app/main.rs:1816 +#, rust-format +msgid "" +"The Proton Drive service keeps failing to start and tries again on its own. " +"The last error was: {error}" +msgstr "The Proton Drive service keeps failing to start and tries again on its own. The last error was: {error}" + +#: crates/pdfs-gui/src/app/main.rs:1824 #: crates/pdfs-gui/src/app/pages/status.rs:355 #: crates/pdfs-gui/src/hud/mod.rs:269 crates/pdfs-gui/src/hud/mod.rs:579 -#: crates/pdfs-gui/src/main.rs:475 crates/pdfs-gui/src/main.rs:517 +#: crates/pdfs-gui/src/main.rs:493 crates/pdfs-gui/src/main.rs:535 msgid "Connecting…" msgstr "Connecting…" -#: crates/pdfs-gui/src/app/main.rs:1804 +#: crates/pdfs-gui/src/app/main.rs:1825 msgid "Waiting for the Proton Drive service to start." msgstr "Waiting for the Proton Drive service to start." @@ -2869,7 +2880,7 @@ msgid "Sync paused until you resume" msgstr "Sync paused until you resume" #: crates/pdfs-gui/src/app/pages/locations.rs:570 -#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:290 +#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:308 msgid "Sync paused" msgstr "Sync paused" @@ -5035,7 +5046,7 @@ msgstr "Resumes {time}" msgid "Until you resume" msgstr "Until you resume" -#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:292 +#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:310 #, rust-format msgid "{n} change needs attention" msgid_plural "{n} changes need attention" @@ -6246,52 +6257,57 @@ msgstr "Proton Drive isn't responding" msgid "Signed in as {username} — locked" msgstr "Signed in as {username} — locked" -#: crates/pdfs-gui/src/main.rs:200 +#: crates/pdfs-gui/src/main.rs:209 +#, rust-format +msgid "Signed in as {username} — can't open your Drive" +msgstr "Signed in as {username} — can't open your Drive" + +#: crates/pdfs-gui/src/main.rs:218 #, rust-format msgid "Signed in as {username} — not connected" msgstr "Signed in as {username} — not connected" -#: crates/pdfs-gui/src/main.rs:205 +#: crates/pdfs-gui/src/main.rs:223 msgid "Not signed in" msgstr "Not signed in" #. Translators: {error} is an error message, in English. -#: crates/pdfs-gui/src/main.rs:208 +#: crates/pdfs-gui/src/main.rs:226 #, rust-format msgid "Error: {error}" msgstr "Error: {error}" -#: crates/pdfs-gui/src/main.rs:230 +#: crates/pdfs-gui/src/main.rs:248 msgid "Proton Drive sent an unexpected reply" msgstr "Proton Drive sent an unexpected reply" -#: crates/pdfs-gui/src/main.rs:284 +#: crates/pdfs-gui/src/main.rs:302 msgid "Session expired — sign in again" msgstr "Session expired — sign in again" -#: crates/pdfs-gui/src/main.rs:286 +#: crates/pdfs-gui/src/main.rs:304 msgid "Sign-in not saved — unlock your keyring" msgstr "Sign-in not saved — unlock your keyring" -#: crates/pdfs-gui/src/main.rs:289 +#: crates/pdfs-gui/src/main.rs:307 msgid "Update not finished — restart Proton Drive" msgstr "Update not finished — restart Proton Drive" -#: crates/pdfs-gui/src/main.rs:299 +#: crates/pdfs-gui/src/main.rs:317 #, rust-format msgid "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgstr[0] "Up to date in {mountpoint} ({n} pinned)" msgstr[1] "Up to date in {mountpoint} ({n} pinned)" -#: crates/pdfs-gui/src/main.rs:306 +#: crates/pdfs-gui/src/main.rs:324 #, rust-format msgid "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)" msgstr[0] "Syncing — {queued} ({n} pinned)" msgstr[1] "Syncing — {queued} ({n} pinned)" -#: crates/pdfs-gui/src/main.rs:313 +#: crates/pdfs-gui/src/main.rs:331 #, rust-format msgid "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)" @@ -6299,60 +6315,60 @@ msgstr[0] "Offline — cached files only ({n} pinned)" msgstr[1] "Offline — cached files only ({n} pinned)" #. Translators: {queued} is a queue summary such as "3 uploads queued". -#: crates/pdfs-gui/src/main.rs:319 +#: crates/pdfs-gui/src/main.rs:337 #, rust-format msgid "Offline — {queued}" msgstr "Offline — {queued}" #. Translators: a menu item that opens the list of failing changes. -#: crates/pdfs-gui/src/main.rs:358 +#: crates/pdfs-gui/src/main.rs:376 #, rust-format msgid "{n} issue — View" msgid_plural "{n} issues — View" msgstr[0] "{n} issue — View" msgstr[1] "{n} issues — View" -#: crates/pdfs-gui/src/main.rs:465 +#: crates/pdfs-gui/src/main.rs:483 msgid "Sign In…" msgstr "Sign In…" -#: crates/pdfs-gui/src/main.rs:466 +#: crates/pdfs-gui/src/main.rs:484 msgid "Sign In Again…" msgstr "Sign In Again…" -#: crates/pdfs-gui/src/main.rs:469 +#: crates/pdfs-gui/src/main.rs:487 msgid "Open Proton Drive" msgstr "Open Proton Drive" -#: crates/pdfs-gui/src/main.rs:473 +#: crates/pdfs-gui/src/main.rs:491 msgid "Restart Proton Drive" msgstr "Restart Proton Drive" -#: crates/pdfs-gui/src/main.rs:482 +#: crates/pdfs-gui/src/main.rs:500 msgid "Open Folder" msgstr "Open Folder" -#: crates/pdfs-gui/src/main.rs:494 +#: crates/pdfs-gui/src/main.rs:512 msgid "Resume Sync" msgstr "Resume Sync" -#: crates/pdfs-gui/src/main.rs:496 +#: crates/pdfs-gui/src/main.rs:514 msgid "Pause Sync" msgstr "Pause Sync" -#: crates/pdfs-gui/src/main.rs:515 +#: crates/pdfs-gui/src/main.rs:533 msgid "Connect" msgstr "Connect" -#: crates/pdfs-gui/src/main.rs:522 +#: crates/pdfs-gui/src/main.rs:540 msgid "Hide Tray Icon" msgstr "Hide Tray Icon" -#: crates/pdfs-gui/src/main.rs:526 +#: crates/pdfs-gui/src/main.rs:544 msgid "Stop Proton Drive…" msgstr "Stop Proton Drive…" -#: crates/pdfs-gui/src/main.rs:530 +#: crates/pdfs-gui/src/main.rs:548 msgid "Quit" msgstr "Quit" diff --git a/po/es.po b/po/es.po index 9ecbbaf..25156aa 100644 --- a/po/es.po +++ b/po/es.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:37+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Spanish\n" @@ -422,14 +422,25 @@ msgstr "Sin conexión" msgid "The Proton Drive service isn't running." msgstr "El servicio de Proton Drive no está en ejecución." -#: crates/pdfs-gui/src/app/main.rs:1803 +#: crates/pdfs-gui/src/app/main.rs:1813 +msgid "Can't open your Drive" +msgstr "No se puede abrir Proton Drive" + +#: crates/pdfs-gui/src/app/main.rs:1816 +#, rust-format +msgid "" +"The Proton Drive service keeps failing to start and tries again on its own. " +"The last error was: {error}" +msgstr "El servicio de Proton Drive no consigue iniciarse y vuelve a intentarlo por sí solo. Último error: {error}" + +#: crates/pdfs-gui/src/app/main.rs:1824 #: crates/pdfs-gui/src/app/pages/status.rs:355 #: crates/pdfs-gui/src/hud/mod.rs:269 crates/pdfs-gui/src/hud/mod.rs:579 -#: crates/pdfs-gui/src/main.rs:475 crates/pdfs-gui/src/main.rs:517 +#: crates/pdfs-gui/src/main.rs:493 crates/pdfs-gui/src/main.rs:535 msgid "Connecting…" msgstr "Conectando…" -#: crates/pdfs-gui/src/app/main.rs:1804 +#: crates/pdfs-gui/src/app/main.rs:1825 msgid "Waiting for the Proton Drive service to start." msgstr "Esperando a que se inicie el servicio de Proton Drive." @@ -2891,7 +2902,7 @@ msgid "Sync paused until you resume" msgstr "Sincronización en pausa hasta que la reanude" #: crates/pdfs-gui/src/app/pages/locations.rs:570 -#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:290 +#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:308 msgid "Sync paused" msgstr "Sincronización en pausa" @@ -5095,7 +5106,7 @@ msgstr "Se reanuda {time}" msgid "Until you resume" msgstr "Hasta que la reanude" -#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:292 +#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:310 #, rust-format msgid "{n} change needs attention" msgid_plural "{n} changes need attention" @@ -6327,52 +6338,57 @@ msgstr "Proton Drive no responde" msgid "Signed in as {username} — locked" msgstr "Sesión iniciada como {username} — bloqueado" -#: crates/pdfs-gui/src/main.rs:200 +#: crates/pdfs-gui/src/main.rs:209 +#, rust-format +msgid "Signed in as {username} — can't open your Drive" +msgstr "Sesión iniciada como {username} — no se puede abrir Proton Drive" + +#: crates/pdfs-gui/src/main.rs:218 #, rust-format msgid "Signed in as {username} — not connected" msgstr "Sesión iniciada como {username} — sin conexión" -#: crates/pdfs-gui/src/main.rs:205 +#: crates/pdfs-gui/src/main.rs:223 msgid "Not signed in" msgstr "Sesión no iniciada" #. Translators: {error} is an error message, in English. -#: crates/pdfs-gui/src/main.rs:208 +#: crates/pdfs-gui/src/main.rs:226 #, rust-format msgid "Error: {error}" msgstr "Error: {error}" -#: crates/pdfs-gui/src/main.rs:230 +#: crates/pdfs-gui/src/main.rs:248 msgid "Proton Drive sent an unexpected reply" msgstr "Proton Drive envió una respuesta inesperada" -#: crates/pdfs-gui/src/main.rs:284 +#: crates/pdfs-gui/src/main.rs:302 msgid "Session expired — sign in again" msgstr "Sesión caducada: vuelva a iniciar sesión" -#: crates/pdfs-gui/src/main.rs:286 +#: crates/pdfs-gui/src/main.rs:304 msgid "Sign-in not saved — unlock your keyring" msgstr "Inicio de sesión no guardado: desbloquee el depósito de claves" -#: crates/pdfs-gui/src/main.rs:289 +#: crates/pdfs-gui/src/main.rs:307 msgid "Update not finished — restart Proton Drive" msgstr "Actualización sin terminar: reinicie Proton Drive" -#: crates/pdfs-gui/src/main.rs:299 +#: crates/pdfs-gui/src/main.rs:317 #, rust-format msgid "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgstr[0] "Actualizado en {mountpoint} ({n} fijado)" msgstr[1] "Actualizado en {mountpoint} ({n} fijados)" -#: crates/pdfs-gui/src/main.rs:306 +#: crates/pdfs-gui/src/main.rs:324 #, rust-format msgid "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)" msgstr[0] "Sincronizando: {queued} ({n} anclado)" msgstr[1] "Sincronizando: {queued} ({n} anclados)" -#: crates/pdfs-gui/src/main.rs:313 +#: crates/pdfs-gui/src/main.rs:331 #, rust-format msgid "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)" @@ -6380,60 +6396,60 @@ msgstr[0] "Sin conexión: solo archivos en caché ({n} anclado)" msgstr[1] "Sin conexión: solo archivos en caché ({n} anclados)" #. Translators: {queued} is a queue summary such as "3 uploads queued". -#: crates/pdfs-gui/src/main.rs:319 +#: crates/pdfs-gui/src/main.rs:337 #, rust-format msgid "Offline — {queued}" msgstr "Sin conexión: {queued}" #. Translators: a menu item that opens the list of failing changes. -#: crates/pdfs-gui/src/main.rs:358 +#: crates/pdfs-gui/src/main.rs:376 #, rust-format msgid "{n} issue — View" msgid_plural "{n} issues — View" msgstr[0] "{n} problema: ver" msgstr[1] "{n} problemas: ver" -#: crates/pdfs-gui/src/main.rs:465 +#: crates/pdfs-gui/src/main.rs:483 msgid "Sign In…" msgstr "Iniciar sesión…" -#: crates/pdfs-gui/src/main.rs:466 +#: crates/pdfs-gui/src/main.rs:484 msgid "Sign In Again…" msgstr "Volver a iniciar sesión…" -#: crates/pdfs-gui/src/main.rs:469 +#: crates/pdfs-gui/src/main.rs:487 msgid "Open Proton Drive" msgstr "Abrir Proton Drive" -#: crates/pdfs-gui/src/main.rs:473 +#: crates/pdfs-gui/src/main.rs:491 msgid "Restart Proton Drive" msgstr "Reiniciar Proton Drive" -#: crates/pdfs-gui/src/main.rs:482 +#: crates/pdfs-gui/src/main.rs:500 msgid "Open Folder" msgstr "Abrir carpeta" -#: crates/pdfs-gui/src/main.rs:494 +#: crates/pdfs-gui/src/main.rs:512 msgid "Resume Sync" msgstr "Reanudar la sincronización" -#: crates/pdfs-gui/src/main.rs:496 +#: crates/pdfs-gui/src/main.rs:514 msgid "Pause Sync" msgstr "Pausar la sincronización" -#: crates/pdfs-gui/src/main.rs:515 +#: crates/pdfs-gui/src/main.rs:533 msgid "Connect" msgstr "Conectar" -#: crates/pdfs-gui/src/main.rs:522 +#: crates/pdfs-gui/src/main.rs:540 msgid "Hide Tray Icon" msgstr "Ocultar el icono de la bandeja" -#: crates/pdfs-gui/src/main.rs:526 +#: crates/pdfs-gui/src/main.rs:544 msgid "Stop Proton Drive…" msgstr "Detener Proton Drive…" -#: crates/pdfs-gui/src/main.rs:530 +#: crates/pdfs-gui/src/main.rs:548 msgid "Quit" msgstr "Salir" diff --git a/po/fr.po b/po/fr.po index 8a20308..80269d4 100644 --- a/po/fr.po +++ b/po/fr.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:37+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: French\n" @@ -424,14 +424,25 @@ msgstr "Non connecté" msgid "The Proton Drive service isn't running." msgstr "Le service Proton Drive n’est pas en cours d’exécution." -#: crates/pdfs-gui/src/app/main.rs:1803 +#: crates/pdfs-gui/src/app/main.rs:1813 +msgid "Can't open your Drive" +msgstr "Impossible d’ouvrir Proton Drive" + +#: crates/pdfs-gui/src/app/main.rs:1816 +#, rust-format +msgid "" +"The Proton Drive service keeps failing to start and tries again on its own. " +"The last error was: {error}" +msgstr "Le service Proton Drive n’arrive pas à démarrer et réessaie de lui-même. Dernière erreur : {error}" + +#: crates/pdfs-gui/src/app/main.rs:1824 #: crates/pdfs-gui/src/app/pages/status.rs:355 #: crates/pdfs-gui/src/hud/mod.rs:269 crates/pdfs-gui/src/hud/mod.rs:579 -#: crates/pdfs-gui/src/main.rs:475 crates/pdfs-gui/src/main.rs:517 +#: crates/pdfs-gui/src/main.rs:493 crates/pdfs-gui/src/main.rs:535 msgid "Connecting…" msgstr "Connexion…" -#: crates/pdfs-gui/src/app/main.rs:1804 +#: crates/pdfs-gui/src/app/main.rs:1825 msgid "Waiting for the Proton Drive service to start." msgstr "En attente du démarrage du service Proton Drive." @@ -2908,7 +2919,7 @@ msgid "Sync paused until you resume" msgstr "Synchronisation en pause jusqu’à la reprise" #: crates/pdfs-gui/src/app/pages/locations.rs:570 -#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:290 +#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:308 msgid "Sync paused" msgstr "Synchronisation en pause" @@ -5138,7 +5149,7 @@ msgstr "Reprise {time}" msgid "Until you resume" msgstr "Jusqu’à la reprise" -#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:292 +#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:310 #, rust-format msgid "{n} change needs attention" msgid_plural "{n} changes need attention" @@ -6375,52 +6386,57 @@ msgstr "Proton Drive ne répond pas" msgid "Signed in as {username} — locked" msgstr "Connecté en tant que {username} — verrouillé" -#: crates/pdfs-gui/src/main.rs:200 +#: crates/pdfs-gui/src/main.rs:209 +#, rust-format +msgid "Signed in as {username} — can't open your Drive" +msgstr "Connecté en tant que {username} — impossible d’ouvrir Proton Drive" + +#: crates/pdfs-gui/src/main.rs:218 #, rust-format msgid "Signed in as {username} — not connected" msgstr "Connecté en tant que {username} — non connecté" -#: crates/pdfs-gui/src/main.rs:205 +#: crates/pdfs-gui/src/main.rs:223 msgid "Not signed in" msgstr "Non connecté" #. Translators: {error} is an error message, in English. -#: crates/pdfs-gui/src/main.rs:208 +#: crates/pdfs-gui/src/main.rs:226 #, rust-format msgid "Error: {error}" msgstr "Erreur : {error}" -#: crates/pdfs-gui/src/main.rs:230 +#: crates/pdfs-gui/src/main.rs:248 msgid "Proton Drive sent an unexpected reply" msgstr "Proton Drive a envoyé une réponse inattendue" -#: crates/pdfs-gui/src/main.rs:284 +#: crates/pdfs-gui/src/main.rs:302 msgid "Session expired — sign in again" msgstr "Session expirée — reconnectez-vous" -#: crates/pdfs-gui/src/main.rs:286 +#: crates/pdfs-gui/src/main.rs:304 msgid "Sign-in not saved — unlock your keyring" msgstr "Connexion non enregistrée — déverrouillez votre trousseau" -#: crates/pdfs-gui/src/main.rs:289 +#: crates/pdfs-gui/src/main.rs:307 msgid "Update not finished — restart Proton Drive" msgstr "Mise à jour inachevée — redémarrez Proton Drive" -#: crates/pdfs-gui/src/main.rs:299 +#: crates/pdfs-gui/src/main.rs:317 #, rust-format msgid "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgstr[0] "À jour dans {mountpoint} ({n} épinglé)" msgstr[1] "À jour dans {mountpoint} ({n} épinglés)" -#: crates/pdfs-gui/src/main.rs:306 +#: crates/pdfs-gui/src/main.rs:324 #, rust-format msgid "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)" msgstr[0] "Synchronisation — {queued} ({n} épinglé)" msgstr[1] "Synchronisation — {queued} ({n} épinglés)" -#: crates/pdfs-gui/src/main.rs:313 +#: crates/pdfs-gui/src/main.rs:331 #, rust-format msgid "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)" @@ -6428,60 +6444,60 @@ msgstr[0] "Hors ligne — fichiers en cache uniquement ({n} épinglé)" msgstr[1] "Hors ligne — fichiers en cache uniquement ({n} épinglés)" #. Translators: {queued} is a queue summary such as "3 uploads queued". -#: crates/pdfs-gui/src/main.rs:319 +#: crates/pdfs-gui/src/main.rs:337 #, rust-format msgid "Offline — {queued}" msgstr "Hors ligne — {queued}" #. Translators: a menu item that opens the list of failing changes. -#: crates/pdfs-gui/src/main.rs:358 +#: crates/pdfs-gui/src/main.rs:376 #, rust-format msgid "{n} issue — View" msgid_plural "{n} issues — View" msgstr[0] "{n} problème — Afficher" msgstr[1] "{n} problèmes — Afficher" -#: crates/pdfs-gui/src/main.rs:465 +#: crates/pdfs-gui/src/main.rs:483 msgid "Sign In…" msgstr "Se connecter…" -#: crates/pdfs-gui/src/main.rs:466 +#: crates/pdfs-gui/src/main.rs:484 msgid "Sign In Again…" msgstr "Se reconnecter…" -#: crates/pdfs-gui/src/main.rs:469 +#: crates/pdfs-gui/src/main.rs:487 msgid "Open Proton Drive" msgstr "Ouvrir Proton Drive" -#: crates/pdfs-gui/src/main.rs:473 +#: crates/pdfs-gui/src/main.rs:491 msgid "Restart Proton Drive" msgstr "Redémarrer Proton Drive" -#: crates/pdfs-gui/src/main.rs:482 +#: crates/pdfs-gui/src/main.rs:500 msgid "Open Folder" msgstr "Ouvrir le dossier" -#: crates/pdfs-gui/src/main.rs:494 +#: crates/pdfs-gui/src/main.rs:512 msgid "Resume Sync" msgstr "Reprendre la synchronisation" -#: crates/pdfs-gui/src/main.rs:496 +#: crates/pdfs-gui/src/main.rs:514 msgid "Pause Sync" msgstr "Mettre en pause la synchronisation" -#: crates/pdfs-gui/src/main.rs:515 +#: crates/pdfs-gui/src/main.rs:533 msgid "Connect" msgstr "Connecter" -#: crates/pdfs-gui/src/main.rs:522 +#: crates/pdfs-gui/src/main.rs:540 msgid "Hide Tray Icon" msgstr "Masquer l’icône de la zone de notification" -#: crates/pdfs-gui/src/main.rs:526 +#: crates/pdfs-gui/src/main.rs:544 msgid "Stop Proton Drive…" msgstr "Arrêter Proton Drive…" -#: crates/pdfs-gui/src/main.rs:530 +#: crates/pdfs-gui/src/main.rs:548 msgid "Quit" msgstr "Quitter" diff --git a/po/it.po b/po/it.po index c035843..8487641 100644 --- a/po/it.po +++ b/po/it.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:37+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Italian\n" @@ -420,14 +420,25 @@ msgstr "Non connesso" msgid "The Proton Drive service isn't running." msgstr "Il servizio di Proton Drive non è in esecuzione." -#: crates/pdfs-gui/src/app/main.rs:1803 +#: crates/pdfs-gui/src/app/main.rs:1813 +msgid "Can't open your Drive" +msgstr "Impossibile aprire Proton Drive" + +#: crates/pdfs-gui/src/app/main.rs:1816 +#, rust-format +msgid "" +"The Proton Drive service keeps failing to start and tries again on its own. " +"The last error was: {error}" +msgstr "Il servizio di Proton Drive non riesce ad avviarsi e riprova da solo. Ultimo errore: {error}" + +#: crates/pdfs-gui/src/app/main.rs:1824 #: crates/pdfs-gui/src/app/pages/status.rs:355 #: crates/pdfs-gui/src/hud/mod.rs:269 crates/pdfs-gui/src/hud/mod.rs:579 -#: crates/pdfs-gui/src/main.rs:475 crates/pdfs-gui/src/main.rs:517 +#: crates/pdfs-gui/src/main.rs:493 crates/pdfs-gui/src/main.rs:535 msgid "Connecting…" msgstr "Connessione…" -#: crates/pdfs-gui/src/app/main.rs:1804 +#: crates/pdfs-gui/src/app/main.rs:1825 msgid "Waiting for the Proton Drive service to start." msgstr "In attesa dell’avvio del servizio di Proton Drive." @@ -2906,7 +2917,7 @@ msgid "Sync paused until you resume" msgstr "Sincronizzazione in pausa finché non riprendi" #: crates/pdfs-gui/src/app/pages/locations.rs:570 -#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:290 +#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:308 msgid "Sync paused" msgstr "Sincronizzazione in pausa" @@ -5114,7 +5125,7 @@ msgstr "Riprende {time}" msgid "Until you resume" msgstr "Finché non riprendi" -#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:292 +#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:310 #, rust-format msgid "{n} change needs attention" msgid_plural "{n} changes need attention" @@ -6351,52 +6362,57 @@ msgstr "Proton Drive non risponde" msgid "Signed in as {username} — locked" msgstr "Accesso effettuato come {username} — bloccato" -#: crates/pdfs-gui/src/main.rs:200 +#: crates/pdfs-gui/src/main.rs:209 +#, rust-format +msgid "Signed in as {username} — can't open your Drive" +msgstr "Accesso effettuato come {username} — impossibile aprire Proton Drive" + +#: crates/pdfs-gui/src/main.rs:218 #, rust-format msgid "Signed in as {username} — not connected" msgstr "Accesso effettuato come {username} — non connesso" -#: crates/pdfs-gui/src/main.rs:205 +#: crates/pdfs-gui/src/main.rs:223 msgid "Not signed in" msgstr "Accesso non eseguito" #. Translators: {error} is an error message, in English. -#: crates/pdfs-gui/src/main.rs:208 +#: crates/pdfs-gui/src/main.rs:226 #, rust-format msgid "Error: {error}" msgstr "Errore: {error}" -#: crates/pdfs-gui/src/main.rs:230 +#: crates/pdfs-gui/src/main.rs:248 msgid "Proton Drive sent an unexpected reply" msgstr "Proton Drive ha inviato una risposta inattesa" -#: crates/pdfs-gui/src/main.rs:284 +#: crates/pdfs-gui/src/main.rs:302 msgid "Session expired — sign in again" msgstr "Sessione scaduta: accedi di nuovo" -#: crates/pdfs-gui/src/main.rs:286 +#: crates/pdfs-gui/src/main.rs:304 msgid "Sign-in not saved — unlock your keyring" msgstr "Accesso non salvato: sblocca il portachiavi" -#: crates/pdfs-gui/src/main.rs:289 +#: crates/pdfs-gui/src/main.rs:307 msgid "Update not finished — restart Proton Drive" msgstr "Aggiornamento non completato: riavvia Proton Drive" -#: crates/pdfs-gui/src/main.rs:299 +#: crates/pdfs-gui/src/main.rs:317 #, rust-format msgid "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgstr[0] "Aggiornato in {mountpoint} ({n} fissato)" msgstr[1] "Aggiornato in {mountpoint} ({n} fissati)" -#: crates/pdfs-gui/src/main.rs:306 +#: crates/pdfs-gui/src/main.rs:324 #, rust-format msgid "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)" msgstr[0] "Sincronizzazione: {queued} ({n} fissato)" msgstr[1] "Sincronizzazione: {queued} ({n} fissati)" -#: crates/pdfs-gui/src/main.rs:313 +#: crates/pdfs-gui/src/main.rs:331 #, rust-format msgid "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)" @@ -6404,60 +6420,60 @@ msgstr[0] "Offline: solo file in cache ({n} fissato)" msgstr[1] "Offline: solo file in cache ({n} fissati)" #. Translators: {queued} is a queue summary such as "3 uploads queued". -#: crates/pdfs-gui/src/main.rs:319 +#: crates/pdfs-gui/src/main.rs:337 #, rust-format msgid "Offline — {queued}" msgstr "Offline: {queued}" #. Translators: a menu item that opens the list of failing changes. -#: crates/pdfs-gui/src/main.rs:358 +#: crates/pdfs-gui/src/main.rs:376 #, rust-format msgid "{n} issue — View" msgid_plural "{n} issues — View" msgstr[0] "{n} problema: visualizza" msgstr[1] "{n} problemi: visualizza" -#: crates/pdfs-gui/src/main.rs:465 +#: crates/pdfs-gui/src/main.rs:483 msgid "Sign In…" msgstr "Accedi…" -#: crates/pdfs-gui/src/main.rs:466 +#: crates/pdfs-gui/src/main.rs:484 msgid "Sign In Again…" msgstr "Accedi di nuovo…" -#: crates/pdfs-gui/src/main.rs:469 +#: crates/pdfs-gui/src/main.rs:487 msgid "Open Proton Drive" msgstr "Apri Proton Drive" -#: crates/pdfs-gui/src/main.rs:473 +#: crates/pdfs-gui/src/main.rs:491 msgid "Restart Proton Drive" msgstr "Riavvia Proton Drive" -#: crates/pdfs-gui/src/main.rs:482 +#: crates/pdfs-gui/src/main.rs:500 msgid "Open Folder" msgstr "Apri cartella" -#: crates/pdfs-gui/src/main.rs:494 +#: crates/pdfs-gui/src/main.rs:512 msgid "Resume Sync" msgstr "Riprendi sincronizzazione" -#: crates/pdfs-gui/src/main.rs:496 +#: crates/pdfs-gui/src/main.rs:514 msgid "Pause Sync" msgstr "Metti in pausa sincronizzazione" -#: crates/pdfs-gui/src/main.rs:515 +#: crates/pdfs-gui/src/main.rs:533 msgid "Connect" msgstr "Connetti" -#: crates/pdfs-gui/src/main.rs:522 +#: crates/pdfs-gui/src/main.rs:540 msgid "Hide Tray Icon" msgstr "Nascondi icona nell'area di notifica" -#: crates/pdfs-gui/src/main.rs:526 +#: crates/pdfs-gui/src/main.rs:544 msgid "Stop Proton Drive…" msgstr "Arresta Proton Drive…" -#: crates/pdfs-gui/src/main.rs:530 +#: crates/pdfs-gui/src/main.rs:548 msgid "Quit" msgstr "Esci" diff --git a/po/nl.po b/po/nl.po index 1de0e6f..26a0fbc 100644 --- a/po/nl.po +++ b/po/nl.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:37+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Dutch\n" @@ -421,14 +421,25 @@ msgstr "Niet verbonden" msgid "The Proton Drive service isn't running." msgstr "De Proton Drive-service draait niet." -#: crates/pdfs-gui/src/app/main.rs:1803 +#: crates/pdfs-gui/src/app/main.rs:1813 +msgid "Can't open your Drive" +msgstr "Kan Proton Drive niet openen" + +#: crates/pdfs-gui/src/app/main.rs:1816 +#, rust-format +msgid "" +"The Proton Drive service keeps failing to start and tries again on its own. " +"The last error was: {error}" +msgstr "De Proton Drive-service kan steeds niet starten en probeert het zelf opnieuw. Laatste fout: {error}" + +#: crates/pdfs-gui/src/app/main.rs:1824 #: crates/pdfs-gui/src/app/pages/status.rs:355 #: crates/pdfs-gui/src/hud/mod.rs:269 crates/pdfs-gui/src/hud/mod.rs:579 -#: crates/pdfs-gui/src/main.rs:475 crates/pdfs-gui/src/main.rs:517 +#: crates/pdfs-gui/src/main.rs:493 crates/pdfs-gui/src/main.rs:535 msgid "Connecting…" msgstr "Verbinden…" -#: crates/pdfs-gui/src/app/main.rs:1804 +#: crates/pdfs-gui/src/app/main.rs:1825 msgid "Waiting for the Proton Drive service to start." msgstr "Wachten tot de Proton Drive-service is gestart." @@ -2896,7 +2907,7 @@ msgid "Sync paused until you resume" msgstr "Synchronisatie gepauzeerd totdat je hervat" #: crates/pdfs-gui/src/app/pages/locations.rs:570 -#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:290 +#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:308 msgid "Sync paused" msgstr "Synchronisatie gepauzeerd" @@ -5108,7 +5119,7 @@ msgstr "Hervat {time}" msgid "Until you resume" msgstr "Totdat je hervat" -#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:292 +#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:310 #, rust-format msgid "{n} change needs attention" msgid_plural "{n} changes need attention" @@ -6337,52 +6348,57 @@ msgstr "Proton Drive reageert niet" msgid "Signed in as {username} — locked" msgstr "Aangemeld als {username} — vergrendeld" -#: crates/pdfs-gui/src/main.rs:200 +#: crates/pdfs-gui/src/main.rs:209 +#, rust-format +msgid "Signed in as {username} — can't open your Drive" +msgstr "Aangemeld als {username} — kan Proton Drive niet openen" + +#: crates/pdfs-gui/src/main.rs:218 #, rust-format msgid "Signed in as {username} — not connected" msgstr "Aangemeld als {username} — niet verbonden" -#: crates/pdfs-gui/src/main.rs:205 +#: crates/pdfs-gui/src/main.rs:223 msgid "Not signed in" msgstr "Niet aangemeld" #. Translators: {error} is an error message, in English. -#: crates/pdfs-gui/src/main.rs:208 +#: crates/pdfs-gui/src/main.rs:226 #, rust-format msgid "Error: {error}" msgstr "Fout: {error}" -#: crates/pdfs-gui/src/main.rs:230 +#: crates/pdfs-gui/src/main.rs:248 msgid "Proton Drive sent an unexpected reply" msgstr "Proton Drive gaf een onverwacht antwoord" -#: crates/pdfs-gui/src/main.rs:284 +#: crates/pdfs-gui/src/main.rs:302 msgid "Session expired — sign in again" msgstr "Sessie verlopen — meld je opnieuw aan" -#: crates/pdfs-gui/src/main.rs:286 +#: crates/pdfs-gui/src/main.rs:304 msgid "Sign-in not saved — unlock your keyring" msgstr "Aanmelding niet opgeslagen — ontgrendel je sleutelbos" -#: crates/pdfs-gui/src/main.rs:289 +#: crates/pdfs-gui/src/main.rs:307 msgid "Update not finished — restart Proton Drive" msgstr "Update niet voltooid — start Proton Drive opnieuw" -#: crates/pdfs-gui/src/main.rs:299 +#: crates/pdfs-gui/src/main.rs:317 #, rust-format msgid "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgstr[0] "Bijgewerkt in {mountpoint} ({n} vastgezet)" msgstr[1] "Bijgewerkt in {mountpoint} ({n} vastgezet)" -#: crates/pdfs-gui/src/main.rs:306 +#: crates/pdfs-gui/src/main.rs:324 #, rust-format msgid "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)" msgstr[0] "Synchroniseren — {queued} ({n} vastgezet)" msgstr[1] "Synchroniseren — {queued} ({n} vastgezet)" -#: crates/pdfs-gui/src/main.rs:313 +#: crates/pdfs-gui/src/main.rs:331 #, rust-format msgid "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)" @@ -6390,60 +6406,60 @@ msgstr[0] "Offline — alleen bestanden in cache ({n} vastgezet)" msgstr[1] "Offline — alleen bestanden in cache ({n} vastgezet)" #. Translators: {queued} is a queue summary such as "3 uploads queued". -#: crates/pdfs-gui/src/main.rs:319 +#: crates/pdfs-gui/src/main.rs:337 #, rust-format msgid "Offline — {queued}" msgstr "Offline — {queued}" #. Translators: a menu item that opens the list of failing changes. -#: crates/pdfs-gui/src/main.rs:358 +#: crates/pdfs-gui/src/main.rs:376 #, rust-format msgid "{n} issue — View" msgid_plural "{n} issues — View" msgstr[0] "{n} probleem — Bekijken" msgstr[1] "{n} problemen — Bekijken" -#: crates/pdfs-gui/src/main.rs:465 +#: crates/pdfs-gui/src/main.rs:483 msgid "Sign In…" msgstr "Aanmelden…" -#: crates/pdfs-gui/src/main.rs:466 +#: crates/pdfs-gui/src/main.rs:484 msgid "Sign In Again…" msgstr "Opnieuw aanmelden…" -#: crates/pdfs-gui/src/main.rs:469 +#: crates/pdfs-gui/src/main.rs:487 msgid "Open Proton Drive" msgstr "Proton Drive openen" -#: crates/pdfs-gui/src/main.rs:473 +#: crates/pdfs-gui/src/main.rs:491 msgid "Restart Proton Drive" msgstr "Proton Drive opnieuw starten" -#: crates/pdfs-gui/src/main.rs:482 +#: crates/pdfs-gui/src/main.rs:500 msgid "Open Folder" msgstr "Map openen" -#: crates/pdfs-gui/src/main.rs:494 +#: crates/pdfs-gui/src/main.rs:512 msgid "Resume Sync" msgstr "Synchronisatie hervatten" -#: crates/pdfs-gui/src/main.rs:496 +#: crates/pdfs-gui/src/main.rs:514 msgid "Pause Sync" msgstr "Synchronisatie pauzeren" -#: crates/pdfs-gui/src/main.rs:515 +#: crates/pdfs-gui/src/main.rs:533 msgid "Connect" msgstr "Verbinden" -#: crates/pdfs-gui/src/main.rs:522 +#: crates/pdfs-gui/src/main.rs:540 msgid "Hide Tray Icon" msgstr "Pictogram in systeemvak verbergen" -#: crates/pdfs-gui/src/main.rs:526 +#: crates/pdfs-gui/src/main.rs:544 msgid "Stop Proton Drive…" msgstr "Proton Drive stoppen…" -#: crates/pdfs-gui/src/main.rs:530 +#: crates/pdfs-gui/src/main.rs:548 msgid "Quit" msgstr "Afsluiten" diff --git a/po/pdfs.pot b/po/pdfs.pot index 90860e8..dad3a5c 100644 --- a/po/pdfs.pot +++ b/po/pdfs.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 3.6.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:28+0100\n" +"POT-Creation-Date: 2026-10-31 10:37+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -397,14 +397,25 @@ msgstr "" msgid "The Proton Drive service isn't running." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1803 +#: crates/pdfs-gui/src/app/main.rs:1813 +msgid "Can't open your Drive" +msgstr "" + +#: crates/pdfs-gui/src/app/main.rs:1816 +#, rust-format +msgid "" +"The Proton Drive service keeps failing to start and tries again on its own. " +"The last error was: {error}" +msgstr "" + +#: crates/pdfs-gui/src/app/main.rs:1824 #: crates/pdfs-gui/src/app/pages/status.rs:355 #: crates/pdfs-gui/src/hud/mod.rs:269 crates/pdfs-gui/src/hud/mod.rs:579 -#: crates/pdfs-gui/src/main.rs:475 crates/pdfs-gui/src/main.rs:517 +#: crates/pdfs-gui/src/main.rs:493 crates/pdfs-gui/src/main.rs:535 msgid "Connecting…" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1804 +#: crates/pdfs-gui/src/app/main.rs:1825 msgid "Waiting for the Proton Drive service to start." msgstr "" @@ -2788,7 +2799,7 @@ msgid "Sync paused until you resume" msgstr "" #: crates/pdfs-gui/src/app/pages/locations.rs:570 -#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:290 +#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:308 msgid "Sync paused" msgstr "" @@ -4883,7 +4894,7 @@ msgstr "" msgid "Until you resume" msgstr "" -#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:292 +#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:310 #, rust-format msgid "{n} change needs attention" msgid_plural "{n} changes need attention" @@ -6071,52 +6082,57 @@ msgstr "" msgid "Signed in as {username} — locked" msgstr "" -#: crates/pdfs-gui/src/main.rs:200 +#: crates/pdfs-gui/src/main.rs:209 +#, rust-format +msgid "Signed in as {username} — can't open your Drive" +msgstr "" + +#: crates/pdfs-gui/src/main.rs:218 #, rust-format msgid "Signed in as {username} — not connected" msgstr "" -#: crates/pdfs-gui/src/main.rs:205 +#: crates/pdfs-gui/src/main.rs:223 msgid "Not signed in" msgstr "" #. Translators: {error} is an error message, in English. -#: crates/pdfs-gui/src/main.rs:208 +#: crates/pdfs-gui/src/main.rs:226 #, rust-format msgid "Error: {error}" msgstr "" -#: crates/pdfs-gui/src/main.rs:230 +#: crates/pdfs-gui/src/main.rs:248 msgid "Proton Drive sent an unexpected reply" msgstr "" -#: crates/pdfs-gui/src/main.rs:284 +#: crates/pdfs-gui/src/main.rs:302 msgid "Session expired — sign in again" msgstr "" -#: crates/pdfs-gui/src/main.rs:286 +#: crates/pdfs-gui/src/main.rs:304 msgid "Sign-in not saved — unlock your keyring" msgstr "" -#: crates/pdfs-gui/src/main.rs:289 +#: crates/pdfs-gui/src/main.rs:307 msgid "Update not finished — restart Proton Drive" msgstr "" -#: crates/pdfs-gui/src/main.rs:299 +#: crates/pdfs-gui/src/main.rs:317 #, rust-format msgid "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgstr[0] "" msgstr[1] "" -#: crates/pdfs-gui/src/main.rs:306 +#: crates/pdfs-gui/src/main.rs:324 #, rust-format msgid "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)" msgstr[0] "" msgstr[1] "" -#: crates/pdfs-gui/src/main.rs:313 +#: crates/pdfs-gui/src/main.rs:331 #, rust-format msgid "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)" @@ -6124,60 +6140,60 @@ msgstr[0] "" msgstr[1] "" #. Translators: {queued} is a queue summary such as "3 uploads queued". -#: crates/pdfs-gui/src/main.rs:319 +#: crates/pdfs-gui/src/main.rs:337 #, rust-format msgid "Offline — {queued}" msgstr "" #. Translators: a menu item that opens the list of failing changes. -#: crates/pdfs-gui/src/main.rs:358 +#: crates/pdfs-gui/src/main.rs:376 #, rust-format msgid "{n} issue — View" msgid_plural "{n} issues — View" msgstr[0] "" msgstr[1] "" -#: crates/pdfs-gui/src/main.rs:465 +#: crates/pdfs-gui/src/main.rs:483 msgid "Sign In…" msgstr "" -#: crates/pdfs-gui/src/main.rs:466 +#: crates/pdfs-gui/src/main.rs:484 msgid "Sign In Again…" msgstr "" -#: crates/pdfs-gui/src/main.rs:469 +#: crates/pdfs-gui/src/main.rs:487 msgid "Open Proton Drive" msgstr "" -#: crates/pdfs-gui/src/main.rs:473 +#: crates/pdfs-gui/src/main.rs:491 msgid "Restart Proton Drive" msgstr "" -#: crates/pdfs-gui/src/main.rs:482 +#: crates/pdfs-gui/src/main.rs:500 msgid "Open Folder" msgstr "" -#: crates/pdfs-gui/src/main.rs:494 +#: crates/pdfs-gui/src/main.rs:512 msgid "Resume Sync" msgstr "" -#: crates/pdfs-gui/src/main.rs:496 +#: crates/pdfs-gui/src/main.rs:514 msgid "Pause Sync" msgstr "" -#: crates/pdfs-gui/src/main.rs:515 +#: crates/pdfs-gui/src/main.rs:533 msgid "Connect" msgstr "" -#: crates/pdfs-gui/src/main.rs:522 +#: crates/pdfs-gui/src/main.rs:540 msgid "Hide Tray Icon" msgstr "" -#: crates/pdfs-gui/src/main.rs:526 +#: crates/pdfs-gui/src/main.rs:544 msgid "Stop Proton Drive…" msgstr "" -#: crates/pdfs-gui/src/main.rs:530 +#: crates/pdfs-gui/src/main.rs:548 msgid "Quit" msgstr "" diff --git a/po/pl.po b/po/pl.po index 8acb1fb..41a7913 100644 --- a/po/pl.po +++ b/po/pl.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:37+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Polish\n" @@ -425,14 +425,25 @@ msgstr "Brak połączenia" msgid "The Proton Drive service isn't running." msgstr "Usługa Proton Drive nie jest uruchomiona." -#: crates/pdfs-gui/src/app/main.rs:1803 +#: crates/pdfs-gui/src/app/main.rs:1813 +msgid "Can't open your Drive" +msgstr "Nie można otworzyć Proton Drive" + +#: crates/pdfs-gui/src/app/main.rs:1816 +#, rust-format +msgid "" +"The Proton Drive service keeps failing to start and tries again on its own. " +"The last error was: {error}" +msgstr "Usługa Proton Drive nie może się uruchomić i sama ponawia próby. Ostatni błąd: {error}" + +#: crates/pdfs-gui/src/app/main.rs:1824 #: crates/pdfs-gui/src/app/pages/status.rs:355 #: crates/pdfs-gui/src/hud/mod.rs:269 crates/pdfs-gui/src/hud/mod.rs:579 -#: crates/pdfs-gui/src/main.rs:475 crates/pdfs-gui/src/main.rs:517 +#: crates/pdfs-gui/src/main.rs:493 crates/pdfs-gui/src/main.rs:535 msgid "Connecting…" msgstr "Łączenie…" -#: crates/pdfs-gui/src/app/main.rs:1804 +#: crates/pdfs-gui/src/app/main.rs:1825 msgid "Waiting for the Proton Drive service to start." msgstr "Oczekiwanie na uruchomienie usługi Proton Drive." @@ -2961,7 +2972,7 @@ msgid "Sync paused until you resume" msgstr "Wstrzymano synchronizację do czasu wznowienia" #: crates/pdfs-gui/src/app/pages/locations.rs:570 -#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:290 +#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:308 msgid "Sync paused" msgstr "Wstrzymano synchronizację" @@ -5188,7 +5199,7 @@ msgstr "Wznowienie {time}" msgid "Until you resume" msgstr "Do czasu wznowienia" -#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:292 +#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:310 #, rust-format msgid "{n} change needs attention" msgid_plural "{n} changes need attention" @@ -6439,38 +6450,43 @@ msgstr "Proton Drive nie odpowiada" msgid "Signed in as {username} — locked" msgstr "Zalogowano jako {username} — zablokowane" -#: crates/pdfs-gui/src/main.rs:200 +#: crates/pdfs-gui/src/main.rs:209 +#, rust-format +msgid "Signed in as {username} — can't open your Drive" +msgstr "Zalogowano jako {username} — nie można otworzyć Proton Drive" + +#: crates/pdfs-gui/src/main.rs:218 #, rust-format msgid "Signed in as {username} — not connected" msgstr "Zalogowano jako {username} — brak połączenia" -#: crates/pdfs-gui/src/main.rs:205 +#: crates/pdfs-gui/src/main.rs:223 msgid "Not signed in" msgstr "Nie zalogowano" #. Translators: {error} is an error message, in English. -#: crates/pdfs-gui/src/main.rs:208 +#: crates/pdfs-gui/src/main.rs:226 #, rust-format msgid "Error: {error}" msgstr "Błąd: {error}" -#: crates/pdfs-gui/src/main.rs:230 +#: crates/pdfs-gui/src/main.rs:248 msgid "Proton Drive sent an unexpected reply" msgstr "Proton Drive wysłał nieoczekiwaną odpowiedź" -#: crates/pdfs-gui/src/main.rs:284 +#: crates/pdfs-gui/src/main.rs:302 msgid "Session expired — sign in again" msgstr "Sesja wygasła — zaloguj się ponownie" -#: crates/pdfs-gui/src/main.rs:286 +#: crates/pdfs-gui/src/main.rs:304 msgid "Sign-in not saved — unlock your keyring" msgstr "Logowanie nie zostało zapisane — odblokuj bazę kluczy" -#: crates/pdfs-gui/src/main.rs:289 +#: crates/pdfs-gui/src/main.rs:307 msgid "Update not finished — restart Proton Drive" msgstr "Aktualizacja niedokończona — uruchom ponownie Proton Drive" -#: crates/pdfs-gui/src/main.rs:299 +#: crates/pdfs-gui/src/main.rs:317 #, rust-format msgid "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)" @@ -6478,7 +6494,7 @@ msgstr[0] "Aktualne w {mountpoint} ({n} przypięty)" msgstr[1] "Aktualne w {mountpoint} ({n} przypięte)" msgstr[2] "Aktualne w {mountpoint} ({n} przypiętych)" -#: crates/pdfs-gui/src/main.rs:306 +#: crates/pdfs-gui/src/main.rs:324 #, rust-format msgid "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)" @@ -6486,7 +6502,7 @@ msgstr[0] "Synchronizowanie — {queued} (przypięte: {n})" msgstr[1] "Synchronizowanie — {queued} (przypięte: {n})" msgstr[2] "Synchronizowanie — {queued} (przypięte: {n})" -#: crates/pdfs-gui/src/main.rs:313 +#: crates/pdfs-gui/src/main.rs:331 #, rust-format msgid "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)" @@ -6495,13 +6511,13 @@ msgstr[1] "Offline — tylko pliki w pamięci podręcznej (przypięte: {n})" msgstr[2] "Offline — tylko pliki w pamięci podręcznej (przypięte: {n})" #. Translators: {queued} is a queue summary such as "3 uploads queued". -#: crates/pdfs-gui/src/main.rs:319 +#: crates/pdfs-gui/src/main.rs:337 #, rust-format msgid "Offline — {queued}" msgstr "Offline — {queued}" #. Translators: a menu item that opens the list of failing changes. -#: crates/pdfs-gui/src/main.rs:358 +#: crates/pdfs-gui/src/main.rs:376 #, rust-format msgid "{n} issue — View" msgid_plural "{n} issues — View" @@ -6509,47 +6525,47 @@ msgstr[0] "{n} problem — wyświetl" msgstr[1] "{n} problemy — wyświetl" msgstr[2] "{n} problemów — wyświetl" -#: crates/pdfs-gui/src/main.rs:465 +#: crates/pdfs-gui/src/main.rs:483 msgid "Sign In…" msgstr "Zaloguj się…" -#: crates/pdfs-gui/src/main.rs:466 +#: crates/pdfs-gui/src/main.rs:484 msgid "Sign In Again…" msgstr "Zaloguj się ponownie…" -#: crates/pdfs-gui/src/main.rs:469 +#: crates/pdfs-gui/src/main.rs:487 msgid "Open Proton Drive" msgstr "Otwórz Proton Drive" -#: crates/pdfs-gui/src/main.rs:473 +#: crates/pdfs-gui/src/main.rs:491 msgid "Restart Proton Drive" msgstr "Uruchom ponownie Proton Drive" -#: crates/pdfs-gui/src/main.rs:482 +#: crates/pdfs-gui/src/main.rs:500 msgid "Open Folder" msgstr "Otwórz katalog" -#: crates/pdfs-gui/src/main.rs:494 +#: crates/pdfs-gui/src/main.rs:512 msgid "Resume Sync" msgstr "Wznów synchronizację" -#: crates/pdfs-gui/src/main.rs:496 +#: crates/pdfs-gui/src/main.rs:514 msgid "Pause Sync" msgstr "Wstrzymaj synchronizację" -#: crates/pdfs-gui/src/main.rs:515 +#: crates/pdfs-gui/src/main.rs:533 msgid "Connect" msgstr "Połącz" -#: crates/pdfs-gui/src/main.rs:522 +#: crates/pdfs-gui/src/main.rs:540 msgid "Hide Tray Icon" msgstr "Ukryj ikonę w obszarze powiadamiania" -#: crates/pdfs-gui/src/main.rs:526 +#: crates/pdfs-gui/src/main.rs:544 msgid "Stop Proton Drive…" msgstr "Zatrzymaj Proton Drive…" -#: crates/pdfs-gui/src/main.rs:530 +#: crates/pdfs-gui/src/main.rs:548 msgid "Quit" msgstr "Zakończ" diff --git a/po/pt_BR.po b/po/pt_BR.po index 56b0d45..3080c77 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-26 22:26+0100\n" +"POT-Creation-Date: 2026-10-31 10:37+0100\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: Portuguese (Brazil)\n" @@ -420,14 +420,25 @@ msgstr "Não conectado" msgid "The Proton Drive service isn't running." msgstr "O serviço do Proton Drive não está em execução." -#: crates/pdfs-gui/src/app/main.rs:1803 +#: crates/pdfs-gui/src/app/main.rs:1813 +msgid "Can't open your Drive" +msgstr "Não foi possível abrir o Proton Drive" + +#: crates/pdfs-gui/src/app/main.rs:1816 +#, rust-format +msgid "" +"The Proton Drive service keeps failing to start and tries again on its own. " +"The last error was: {error}" +msgstr "O serviço do Proton Drive não consegue iniciar e tenta de novo sozinho. Último erro: {error}" + +#: crates/pdfs-gui/src/app/main.rs:1824 #: crates/pdfs-gui/src/app/pages/status.rs:355 #: crates/pdfs-gui/src/hud/mod.rs:269 crates/pdfs-gui/src/hud/mod.rs:579 -#: crates/pdfs-gui/src/main.rs:475 crates/pdfs-gui/src/main.rs:517 +#: crates/pdfs-gui/src/main.rs:493 crates/pdfs-gui/src/main.rs:535 msgid "Connecting…" msgstr "Conectando…" -#: crates/pdfs-gui/src/app/main.rs:1804 +#: crates/pdfs-gui/src/app/main.rs:1825 msgid "Waiting for the Proton Drive service to start." msgstr "Aguardando o serviço do Proton Drive iniciar." @@ -2892,7 +2903,7 @@ msgid "Sync paused until you resume" msgstr "Sincronização pausada até você retomar" #: crates/pdfs-gui/src/app/pages/locations.rs:570 -#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:290 +#: crates/pdfs-gui/src/app/pages/status.rs:1445 crates/pdfs-gui/src/main.rs:308 msgid "Sync paused" msgstr "Sincronização pausada" @@ -5093,7 +5104,7 @@ msgstr "Retoma {time}" msgid "Until you resume" msgstr "Até você retomar" -#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:292 +#: crates/pdfs-gui/src/app/pages/status.rs:1464 crates/pdfs-gui/src/main.rs:310 #, rust-format msgid "{n} change needs attention" msgid_plural "{n} changes need attention" @@ -6324,52 +6335,57 @@ msgstr "O Proton Drive não está respondendo" msgid "Signed in as {username} — locked" msgstr "Conectado como {username} — bloqueado" -#: crates/pdfs-gui/src/main.rs:200 +#: crates/pdfs-gui/src/main.rs:209 +#, rust-format +msgid "Signed in as {username} — can't open your Drive" +msgstr "Conectado como {username} — não foi possível abrir o Proton Drive" + +#: crates/pdfs-gui/src/main.rs:218 #, rust-format msgid "Signed in as {username} — not connected" msgstr "Conectado como {username} — sem conexão" -#: crates/pdfs-gui/src/main.rs:205 +#: crates/pdfs-gui/src/main.rs:223 msgid "Not signed in" msgstr "Não conectado" #. Translators: {error} is an error message, in English. -#: crates/pdfs-gui/src/main.rs:208 +#: crates/pdfs-gui/src/main.rs:226 #, rust-format msgid "Error: {error}" msgstr "Erro: {error}" -#: crates/pdfs-gui/src/main.rs:230 +#: crates/pdfs-gui/src/main.rs:248 msgid "Proton Drive sent an unexpected reply" msgstr "O Proton Drive enviou uma resposta inesperada" -#: crates/pdfs-gui/src/main.rs:284 +#: crates/pdfs-gui/src/main.rs:302 msgid "Session expired — sign in again" msgstr "Sessão expirada — entre novamente" -#: crates/pdfs-gui/src/main.rs:286 +#: crates/pdfs-gui/src/main.rs:304 msgid "Sign-in not saved — unlock your keyring" msgstr "Login não salvo — desbloqueie o chaveiro" -#: crates/pdfs-gui/src/main.rs:289 +#: crates/pdfs-gui/src/main.rs:307 msgid "Update not finished — restart Proton Drive" msgstr "Atualização não concluída — reinicie o Proton Drive" -#: crates/pdfs-gui/src/main.rs:299 +#: crates/pdfs-gui/src/main.rs:317 #, rust-format msgid "Up to date in {mountpoint} ({n} pinned)" msgid_plural "Up to date in {mountpoint} ({n} pinned)" msgstr[0] "Atualizado em {mountpoint} ({n} fixado)" msgstr[1] "Atualizado em {mountpoint} ({n} fixados)" -#: crates/pdfs-gui/src/main.rs:306 +#: crates/pdfs-gui/src/main.rs:324 #, rust-format msgid "Syncing — {queued} ({n} pinned)" msgid_plural "Syncing — {queued} ({n} pinned)" msgstr[0] "Sincronizando — {queued} ({n} fixado)" msgstr[1] "Sincronizando — {queued} ({n} fixados)" -#: crates/pdfs-gui/src/main.rs:313 +#: crates/pdfs-gui/src/main.rs:331 #, rust-format msgid "Offline — cached files only ({n} pinned)" msgid_plural "Offline — cached files only ({n} pinned)" @@ -6377,60 +6393,60 @@ msgstr[0] "Offline — somente arquivos em cache ({n} fixado)" msgstr[1] "Offline — somente arquivos em cache ({n} fixados)" #. Translators: {queued} is a queue summary such as "3 uploads queued". -#: crates/pdfs-gui/src/main.rs:319 +#: crates/pdfs-gui/src/main.rs:337 #, rust-format msgid "Offline — {queued}" msgstr "Offline — {queued}" #. Translators: a menu item that opens the list of failing changes. -#: crates/pdfs-gui/src/main.rs:358 +#: crates/pdfs-gui/src/main.rs:376 #, rust-format msgid "{n} issue — View" msgid_plural "{n} issues — View" msgstr[0] "{n} problema — Ver" msgstr[1] "{n} problemas — Ver" -#: crates/pdfs-gui/src/main.rs:465 +#: crates/pdfs-gui/src/main.rs:483 msgid "Sign In…" msgstr "Entrar…" -#: crates/pdfs-gui/src/main.rs:466 +#: crates/pdfs-gui/src/main.rs:484 msgid "Sign In Again…" msgstr "Entrar novamente…" -#: crates/pdfs-gui/src/main.rs:469 +#: crates/pdfs-gui/src/main.rs:487 msgid "Open Proton Drive" msgstr "Abrir o Proton Drive" -#: crates/pdfs-gui/src/main.rs:473 +#: crates/pdfs-gui/src/main.rs:491 msgid "Restart Proton Drive" msgstr "Reiniciar o Proton Drive" -#: crates/pdfs-gui/src/main.rs:482 +#: crates/pdfs-gui/src/main.rs:500 msgid "Open Folder" msgstr "Abrir pasta" -#: crates/pdfs-gui/src/main.rs:494 +#: crates/pdfs-gui/src/main.rs:512 msgid "Resume Sync" msgstr "Retomar sincronização" -#: crates/pdfs-gui/src/main.rs:496 +#: crates/pdfs-gui/src/main.rs:514 msgid "Pause Sync" msgstr "Pausar sincronização" -#: crates/pdfs-gui/src/main.rs:515 +#: crates/pdfs-gui/src/main.rs:533 msgid "Connect" msgstr "Conectar" -#: crates/pdfs-gui/src/main.rs:522 +#: crates/pdfs-gui/src/main.rs:540 msgid "Hide Tray Icon" msgstr "Ocultar ícone da bandeja" -#: crates/pdfs-gui/src/main.rs:526 +#: crates/pdfs-gui/src/main.rs:544 msgid "Stop Proton Drive…" msgstr "Parar o Proton Drive…" -#: crates/pdfs-gui/src/main.rs:530 +#: crates/pdfs-gui/src/main.rs:548 msgid "Quit" msgstr "Sair" From bdbfc0bfd4391597bcb1ba8fcc58bff963ab7836 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 31 Oct 2026 10:53:25 +0100 Subject: [PATCH 07/12] fix(core): read database errors handed to from_api as internal, not offline from_api took every error that was not a ProtonError for a transport failure. Pin, unpin, the searches and node loads hand it pdfs_core errors from the cache and the database, so a failure there said "You're offline" (B266, the rest of B205). The source chain now decides, and only an error with neither type reads as offline. --- crates/pdfs-core/src/error.rs | 68 ++++++++++++++++++++++++++--------- docs/BUGS.md | 11 +++--- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/crates/pdfs-core/src/error.rs b/crates/pdfs-core/src/error.rs index 8bdd102..f1e580a 100644 --- a/crates/pdfs-core/src/error.rs +++ b/crates/pdfs-core/src/error.rs @@ -160,13 +160,14 @@ impl CoreError { /// telling the user their connection is down beats telling them the /// server refused. The SDK's other failures say what went wrong instead /// (see [`proton_kind`]). + /// + /// Callers also hand it this crate's [`Error`] from the cache and the + /// database, which is not a transport error at all. Taking those for + /// "offline" told the user their connection was down when a pin failed on + /// the database (`docs/BUGS.md` B266), so the source chain is searched for + /// either type before falling back. pub fn from_api(e: &(dyn std::error::Error + 'static), what: &str) -> Self { - let kind = match e.downcast_ref::() { - Some(proton) => proton_kind(proton), - // Not the SDK's at all: a timeout around the call, or the socket. - None => ErrorKind::Offline, - }; - Self::new(kind, format!("{what}: {e}")) + Self::new(api_kind_of(e), format!("{what}: {e}")) } /// Prefix the message while keeping the classification. @@ -208,19 +209,40 @@ impl From for CoreError { impl From for CoreError { fn from(e: Error) -> Self { - let kind = match &e { - Error::NotLoggedIn - | Error::ReloginRequired - | Error::Locked - | Error::WrongMailboxPassword - | Error::HumanVerificationRequired(_) => ErrorKind::Denied, - Error::Proton(proton) => proton_kind(proton), - _ => ErrorKind::Internal, - }; - Self::new(kind, e.to_string()) + Self::new(core_kind(&e), e.to_string()) } } +/// What this crate's [`Error`] means to a front-end. +fn core_kind(e: &Error) -> ErrorKind { + match e { + Error::NotLoggedIn + | Error::ReloginRequired + | Error::Locked + | Error::WrongMailboxPassword + | Error::HumanVerificationRequired(_) => ErrorKind::Denied, + Error::Proton(proton) => proton_kind(proton), + _ => ErrorKind::Internal, + } +} + +/// The kind for [`CoreError::from_api`]: the first SDK or crate error in `e`'s +/// source chain decides it. +fn api_kind_of(e: &(dyn std::error::Error + 'static)) -> ErrorKind { + let mut cause = Some(e); + while let Some(err) = cause { + if let Some(proton) = err.downcast_ref::() { + return proton_kind(proton); + } + if let Some(core) = err.downcast_ref::() { + return core_kind(core); + } + cause = err.source(); + } + // Not the SDK's at all: a timeout around the call, or the socket. + ErrorKind::Offline +} + /// What a failed SDK call means to a front-end. /// /// Only a transport failure is the connection. The rest happened on this @@ -414,6 +436,20 @@ mod tests { assert_ne!(e.kind, ErrorKind::Offline); } + /// A pin or a search that fails on the database is not the connection, so + /// it must not say "You're offline" (`docs/BUGS.md` B266). An SDK error + /// wrapped in this crate's type classifies as the SDK error does. + #[test] + fn a_database_error_handed_to_from_api_is_not_offline() { + let db = Error::Other("database is locked".into()); + assert_eq!(CoreError::from_api(&db, "pin").kind, ErrorKind::Internal); + let wrapped = Error::Proton(api(C::DoesNotExist)); + assert_eq!( + CoreError::from_api(&wrapped, "load node").kind, + ErrorKind::NotFound + ); + } + /// `Error` is the plumbing type; converting it must classify as sharply as /// the direct path, or the two disagree about the same failure. #[test] diff --git a/docs/BUGS.md b/docs/BUGS.md index 319b4ca..101369e 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -75,7 +75,6 @@ original wording. | [B269](#b269--a-restore-that-skipped-every-folder-reports-success) | A restore that skipped every folder reports success | Not fixed | | [B268](#b268--pdfs-diagnose-says-no-problems-found-while-sync-is-stuck) | `pdfs diagnose` says "No problems found" while sync is stuck | Not fixed | | [B267](#b267--a-key-failure-at-a-start-from-a-warm-cache-shows-as-offline-for-good) | A key failure at a start from a warm cache shows as offline for good | Not fixed | -| [B266](#b266--pin-unpin-search-and-node-loads-report-a-database-error-as-offline) | Pin, unpin, search and node loads report a database error as "offline" | Not fixed | | [B265](#b265--address-keys-whose-token-is-for-an-inactive-account-key-never-open) | Address keys whose token is for an inactive account key never open | Needs an SDK change; not reproduced | | [B264](#b264--a-mount-that-fails-after-starting-its-workers-leaves-them-running) | A mount that fails after starting its workers leaves them running | Not fixed | | [B263](#b263--a-mount-that-can-never-succeed-retries-every-5-s-and-tells-only-the-journal) | A mount that can never succeed retries every 5 s and tells only the journal | Not fixed | @@ -116,6 +115,7 @@ a live account since. | Entry | Title | |---|---| +| [B266](#b266--pin-unpin-search-and-node-loads-report-a-database-error-as-offline) | Pin, unpin, search and node loads report a database error as "offline" | | [B256](#b256--pdfs-copy-right-after-a-save-copies-the-old-version) | `pdfs copy` right after a save copies the old version | | [B255](#b255--a-synced-folder-whose-files-are-all-gone-here-is-stuck-for-good) | A synced folder whose files are all gone here is stuck for good | | [B254](#b254--an-edit-in-the-same-second-as-a-new-upload-never-arrives) | An edit in the same second as a new upload never arrives | @@ -523,7 +523,7 @@ report the others as their own state through `Status`. ### B266 — Pin, unpin, search and node loads report a database error as "offline" -**Status:** Open +**Status:** Fixed, not verified live **Found:** 2026-10-31, code audit after issue #38. B205 fixed the photo upload path only. **Where:** `CoreError::from_api` in `crates/pdfs-core/src/error.rs`; its callers in `crates/pdfs-fuse/src/lib.rs` (`pin`, `unpin`, the searches, `load node`) @@ -536,8 +536,11 @@ it was written for SDK calls, where that means a transport error. These callers `pdfs_core::Error` values from the cache and the database. A `ProtonError` wrapped in `pdfs_core::Error::Proton` also misses the downcast, since the source chain is not walked. -**Fix.** Not done. Use `CoreError::internal` at the database callers, and walk the source -chain in `from_api` before falling back. +**Fix.** `from_api` walks the source chain. The first `ProtonError` or `pdfs_core::Error` in it +decides the kind, the same way `From` does, and only an error with neither, a transport +error, reads as offline. That covers every caller at once, the database ones included. + +**Test:** `a_database_error_handed_to_from_api_is_not_offline` in `pdfs-core`. --- From a617b2b410819fb0b44448bf70b461a97a1ce30f Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 31 Oct 2026 10:57:34 +0100 Subject: [PATCH 08/12] fix(cli): have diagnose report failing writes, a pause and folders in error diagnose dropped most of the Status reply and never looked at the synced folders, so it said "No problems found" while a folder would not sync (B268, issue #53). It now warns about writes that keep failing, a write a day old, a pause, folders in error or conflict, and a control socket nothing listens on. --- crates/pdfs-cli/src/main.rs | 159 ++++++++++++++++++++++++++++++++++-- docs/BUGS.md | 16 +++- 2 files changed, 166 insertions(+), 9 deletions(-) diff --git a/crates/pdfs-cli/src/main.rs b/crates/pdfs-cli/src/main.rs index 75f6e3b..1f73e9f 100644 --- a/crates/pdfs-cli/src/main.rs +++ b/crates/pdfs-cli/src/main.rs @@ -3369,6 +3369,79 @@ fn cmd_sync_limit(up: Option, down: Option) -> Result<()> { })?) } +/// How long the oldest write may wait to upload before `diagnose` calls it +/// out. Under a day it is a slow link or a big file; past it, something is +/// holding the queue. +const STAGED_STALE_SECS: u64 = 86_400; + +/// `diagnose` findings for the queue and the pause, from the `Status` reply. +/// `paused_for` is how long a timed pause still runs, in seconds. +fn sync_health( + failing_ops: u64, + failing_error: Option<&str>, + staged_bytes: u64, + staged_oldest_secs: u64, + paused: bool, + paused_for: Option, +) -> Vec<(DiagnoseLevel, &'static str, String)> { + let mut findings = Vec::new(); + if failing_ops > 0 { + findings.push(( + DiagnoseLevel::Warn, + " failing writes", + match failing_error { + Some(error) => format!("{failing_ops} keep failing; the last error: {error}"), + None => format!("{failing_ops} keep failing"), + }, + )); + } + if staged_bytes > 0 && staged_oldest_secs >= STAGED_STALE_SECS { + findings.push(( + DiagnoseLevel::Warn, + " oldest unsent write", + format!( + "{} old, {} only on this computer", + human_secs(staged_oldest_secs), + human_bytes(staged_bytes) + ), + )); + } + if paused { + findings.push(( + DiagnoseLevel::Warn, + " sync", + match paused_for { + Some(left) if left > 0 => { + format!("paused for another {}", human_secs(left as u64)) + } + _ => "paused until resumed (`pdfs sync resume`)".to_string(), + }, + )); + } + findings +} + +/// `diagnose` findings for the synced folders that need someone to look. +fn folder_health( + folders: &[pdfs_core::control::SyncFolderInfo], +) -> Vec<(DiagnoseLevel, String, String)> { + folders + .iter() + .filter_map(|folder| { + let what = match folder.state.as_str() { + "error" => "sync error; `pdfs sync list` and the activity feed say more", + "conflict" => "conflict copies to look at", + _ => return None, + }; + Some(( + DiagnoseLevel::Warn, + format!(" synced folder [{}]", folder.id), + format!("{}: {what}", folder.local_path), + )) + }) + .collect() +} + fn unix_now() -> i64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -3705,21 +3778,26 @@ fn cmd_diagnose() -> Result<()> { let socket_exists = socket.exists(); let socket_kind_ok = std::fs::symlink_metadata(&socket).is_ok_and(|m| m.file_type().is_socket()); + // A stale socket file with no listener is the common failure, so the reply + // is what decides this rather than the file's existence. + let status = control_request(CtlRequest::Status); report.finding( if socket_exists && !socket_kind_ok { DiagnoseLevel::Fail - } else if socket_exists { + } else if socket_exists && status.is_ok() { DiagnoseLevel::Ok } else { DiagnoseLevel::Warn }, " control socket", - socket.display().to_string(), + if socket_exists && socket_kind_ok && status.is_err() { + format!("{} (left over; nothing listens on it)", socket.display()) + } else { + socket.display().to_string() + }, ); - // A stale socket file with no listener is the common failure, so the reply - // is what decides this rather than the file's existence. let mut daemon_responding = false; - match control_request(CtlRequest::Status) { + match status { Ok(CtlResponse::Status { mountpoint, online, @@ -3727,6 +3805,12 @@ fn cmd_diagnose() -> Result<()> { tokens_unsaved, pending_uploads, pending_changes, + failing_ops, + failing_error, + staged_bytes, + staged_oldest_secs, + paused, + paused_until, version, .. }) => { @@ -3801,6 +3885,27 @@ fn cmd_diagnose() -> Result<()> { format!("{pending} waiting to upload") }, ); + // The rest of the daemon's state that the report used to drop: + // writes that keep failing, a pause, a folder in error. Each of + // them stops changes from reaching Drive while everything above + // reads as fine (`docs/BUGS.md` B268). + for (level, check, detail) in sync_health( + failing_ops, + failing_error.as_deref(), + staged_bytes, + staged_oldest_secs, + paused, + paused_until.map(|until| until - unix_now()), + ) { + report.finding(level, check, detail); + } + if let Ok(CtlResponse::SyncFolders { items }) = + control_request(CtlRequest::ListSyncFolders) + { + for (level, check, detail) in folder_health(&items) { + report.finding(level, &check, detail); + } + } match await_integrity_check() { Ok(CtlResponse::CacheReport { integrity_problems, @@ -3982,6 +4087,50 @@ mod pause_tests { mod diagnose_tests { use super::*; + /// A healthy queue says nothing; writes that keep failing, an old unsent + /// write and a pause each say so (`docs/BUGS.md` B268). + #[test] + fn a_stuck_queue_or_a_pause_is_reported() { + assert!(sync_health(0, None, 0, 0, false, None).is_empty()); + assert!(sync_health(0, None, 4096, STAGED_STALE_SECS - 1, false, None).is_empty()); + let checks: Vec<&str> = sync_health( + 2, + Some("upload a.txt: quota"), + 4096, + STAGED_STALE_SECS, + true, + Some(600), + ) + .into_iter() + .map(|(level, check, _)| { + assert!(matches!(level, DiagnoseLevel::Warn)); + check.trim() + }) + .collect(); + assert_eq!(checks, ["failing writes", "oldest unsent write", "sync"]); + } + + /// Issue #53: a folder that would not sync, and `diagnose` found nothing. + #[test] + fn a_synced_folder_in_error_is_reported() { + let folder = |id: i64, state: &str| pdfs_core::control::SyncFolderInfo { + id, + local_path: format!("/home/u/f{id}"), + remote_uid: "vol~link".into(), + mode: "mirror".into(), + state: state.into(), + last_sync: 0, + pending_mode: None, + progress: None, + paused: false, + }; + let findings = + folder_health(&[folder(1, "idle"), folder(2, "error"), folder(3, "syncing")]); + assert_eq!(findings.len(), 1); + assert_eq!(findings[0].1.trim(), "synced folder [2]"); + assert!(findings[0].2.starts_with("/home/u/f2: sync error")); + } + #[test] fn finding_severity_is_counted_centrally() { let mut report = DiagnoseReport::default(); diff --git a/docs/BUGS.md b/docs/BUGS.md index 319b4ca..68556e5 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -73,7 +73,6 @@ original wording. | [B271](#b271--one-failed-listing-makes-a-large-first-sync-start-over-from-nothing) | One failed listing makes a large first sync start over from nothing | Not fixed | | [B270](#b270--one-file-left-in-a-mirror-folder-clears-the-wipe-guard-for-all-the-others) | One file left in a mirror folder clears the wipe guard for all the others | Not fixed | | [B269](#b269--a-restore-that-skipped-every-folder-reports-success) | A restore that skipped every folder reports success | Not fixed | -| [B268](#b268--pdfs-diagnose-says-no-problems-found-while-sync-is-stuck) | `pdfs diagnose` says "No problems found" while sync is stuck | Not fixed | | [B267](#b267--a-key-failure-at-a-start-from-a-warm-cache-shows-as-offline-for-good) | A key failure at a start from a warm cache shows as offline for good | Not fixed | | [B266](#b266--pin-unpin-search-and-node-loads-report-a-database-error-as-offline) | Pin, unpin, search and node loads report a database error as "offline" | Not fixed | | [B265](#b265--address-keys-whose-token-is-for-an-inactive-account-key-never-open) | Address keys whose token is for an inactive account key never open | Needs an SDK change; not reproduced | @@ -96,6 +95,7 @@ original wording. | Entry | Title | What is left | |---|---|---| +| [B268](#b268--pdfs-diagnose-says-no-problems-found-while-sync-is-stuck) | `pdfs diagnose` says "No problems found" while sync is stuck | The sync engine's state is not checked | | [B199](#b199--a-stopped-share-stays-on-the-shared-by-me-page-for-minutes) | A stopped share stays on the Shared by me page for minutes | The share itself is not deleted; the SDK has no call for it | | [B88](#b88--pdfs-pin-rename-move-delete-and-sharing-still-cannot-name-a-path-under-a-secondary-mount) | `pdfs pin`, `rename`, `move`, `delete` and sharing still cannot name a path under a secondary mount | `pin`, `unpin`, opening a file, thumbnails, uploads, sharing and versions | | [B72](#b72--pdfs-trash-never-returns) | `pdfs trash` never returns | The slowness of the first refresh is unmeasured | @@ -482,7 +482,7 @@ exit path. ### B268 — `pdfs diagnose` says "No problems found" while sync is stuck -**Status:** Open +**Status:** Partly fixed. The sync engine's own state is not checked yet. **Found:** 2026-10-31, issue #53: a folder that never synced, and `diagnose` found nothing. **Where:** `diagnose` in `crates/pdfs-cli/src/main.rs` @@ -495,8 +495,16 @@ folders or for `Diagnostics`, so a folder in error and a stalled worker are not control socket file with no daemon behind it is also reported `[ok]`; only the next line says the daemon does not answer. -**Fix.** Not done. Warn on failing or parked writes, a pause, old staged data and folders in -error, and report the sync engine's state once B259 exposes it. +**Fix.** `diagnose` warns about writes that keep failing (with the last error), a write that +has waited a day or more to upload, a pause, and each synced folder in `error` or `conflict` +(`sync_health` and `folder_health` in `crates/pdfs-cli/src/main.rs`). A control socket file +that nothing listens on is a warning, not `[ok]`. Parked uploads are left out: a file still +wearing a download's temporary name is normal. Not done: the sync engine's state, which +`Diagnostics` reports once B259's change is in. + +**Test:** `a_stuck_queue_or_a_pause_is_reported` and `a_synced_folder_in_error_is_reported` in +`pdfs-cli`. On 2026-10-31 the new `diagnose` against a healthy 3.6.0 daemon still reported +"No problems found". --- From 6f2b4bb80aa4e00091faa256eae46c5ee5546740 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 31 Oct 2026 11:11:01 +0100 Subject: [PATCH 09/12] fix(fuse,cli): keep a pass going past a failed listing, report skipped restores A listing that failed anywhere in the remote walk ended the whole pass, so a first sync of a large folder on a flaky link walked the tree again and again and never downloaded anything (B271, issue #46). A failed listing below the root now records that folder as a gap: nothing under it is deleted, the rest of the pass applies, and the report names it. A restore that skipped every folder was logged as a successful activity (B269). The daemon now gets the restored and skipped folders back, logs any skip as a failed activity with the reasons, and sends the Rewatch for folders already restored when the database insert fails partway. `pdfs devices join` points to `pdfs activity` for the outcome. --- crates/pdfs-cli/src/main.rs | 6 +++- crates/pdfs-fuse/src/control.rs | 12 +++++-- crates/pdfs-fuse/src/profile.rs | 52 ++++++++++++++++++++++------ crates/pdfs-fuse/src/sync.rs | 50 +++++++++++++++++++++++--- crates/pdfs-fuse/src/sync/planner.rs | 41 ++++++++++++++++++++-- docs/BUGS.md | 28 ++++++++++----- 6 files changed, 160 insertions(+), 29 deletions(-) diff --git a/crates/pdfs-cli/src/main.rs b/crates/pdfs-cli/src/main.rs index 75f6e3b..944869b 100644 --- a/crates/pdfs-cli/src/main.rs +++ b/crates/pdfs-cli/src/main.rs @@ -1458,7 +1458,11 @@ fn cmd_sync_restore(yes: bool, device: Option) -> Result<()> { items: chosen, }, None => CtlRequest::RestoreSyncFolders { items: chosen }, - })?) + })?)?; + // The daemon acks before it works, so the outcome, and any folder it had + // to skip, only shows up later. + println!("Run `pdfs activity` to see which folders were restored."); + Ok(()) } /// Expand a leading `~` in a hand-typed path. The daemon requires an absolute diff --git a/crates/pdfs-fuse/src/control.rs b/crates/pdfs-fuse/src/control.rs index ea23938..55327c2 100644 --- a/crates/pdfs-fuse/src/control.rs +++ b/crates/pdfs-fuse/src/control.rs @@ -1526,9 +1526,17 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U let core = core.clone(); std::thread::spawn(move || { match core.restore_sync_folders(&items, device.as_deref()) { - Ok(message) => { + Ok(outcome) => { core.touch_profile(); - core.log_activity(ActivityKind::Download, &message, "", true); + if !outcome.complete() { + warn!(skipped = ?outcome.skipped, "restore skipped folders"); + } + core.log_activity( + ActivityKind::Download, + outcome.summary(), + outcome.skipped.join("; "), + outcome.complete(), + ); } Err(e) => { warn!(error = %e, "restore failed"); diff --git a/crates/pdfs-fuse/src/profile.rs b/crates/pdfs-fuse/src/profile.rs index 77f1f28..cd24a8d 100644 --- a/crates/pdfs-fuse/src/profile.rs +++ b/crates/pdfs-fuse/src/profile.rs @@ -388,7 +388,7 @@ impl Core { &self, items: &[pdfs_core::control::RestoreItem], device: Option<&str>, - ) -> CoreResult { + ) -> CoreResult { if items.is_empty() { return Err(CoreError::invalid("nothing to restore")); } @@ -434,10 +434,19 @@ impl Core { continue; } }; - let id = self - .db - .sync_folder_add(&local.to_string_lossy(), &item.remote_uid, &device.share_id) - .map_err(|e| CoreError::internal(format!("db: {e:?}")))?; + let id = match self.db.sync_folder_add( + &local.to_string_lossy(), + &item.remote_uid, + &device.share_id, + ) { + Ok(id) => id, + Err(e) => { + // The folders restored before this one still need their + // watches. + let _ = self.sync_tx.send(sync::SyncMsg::Rewatch); + return Err(CoreError::internal(format!("db: {e:?}"))); + } + }; // `ondemand` is applied through the normal request path, which // mounts the FUSE session and handles the folder being busy — the // restore does not get its own copy of that logic. @@ -454,12 +463,35 @@ impl Core { for reason in &skipped { info!(reason, "restore skipped a folder"); } - let mut message = format!("restored {restored} folder(s)"); - if !skipped.is_empty() { - message.push_str(&format!("; skipped: {}", skipped.join(", "))); - } info!(restored, skipped = skipped.len(), "restore complete"); - Ok(message) + Ok(RestoreOutcome { restored, skipped }) + } +} + +/// What a restore did: how many folders it attached, and why it left out the +/// others. +pub(crate) struct RestoreOutcome { + pub(crate) restored: usize, + pub(crate) skipped: Vec, +} + +impl RestoreOutcome { + /// The activity entry's headline. + pub(crate) fn summary(&self) -> String { + match (self.restored, self.skipped.len()) { + (n, 0) => format!("restored {n} folder(s)"), + (0, k) => format!("restored no folders; skipped {k}"), + (n, k) => format!("restored {n} folder(s); skipped {k}"), + } + } + + /// Whether the restore did all it was asked. A skipped folder is a + /// failure the user has to see: the reply went out as "restoring folders" + /// before any work, and a restore that skipped every folder used to be + /// logged as a success, so nothing anywhere said that nothing would + /// download (`docs/BUGS.md` B269). + pub(crate) fn complete(&self) -> bool { + self.skipped.is_empty() } } diff --git a/crates/pdfs-fuse/src/sync.rs b/crates/pdfs-fuse/src/sync.rs index 864e522..8ad48f2 100644 --- a/crates/pdfs-fuse/src/sync.rs +++ b/crates/pdfs-fuse/src/sync.rs @@ -1135,14 +1135,22 @@ impl Core { let uids = self .rt .block_on(self.drive.enumerate_folder_children_node_uids(folder)) - .map_err(|e| format!("enumerate {folder}: {e}"))?; + .map_err(|e| format!("enumerate {folder}: {e}")); + let uids = match unlisted_gap(walk, prefix, uids)? { + Some(uids) => uids, + None => return Ok(()), + }; if uids.is_empty() { return Ok(()); } let nodes = self .rt .block_on(self.drive.enumerate_nodes_light(&uids)) - .map_err(|e| format!("resolve nodes: {e}"))?; + .map_err(|e| format!("resolve nodes: {e}")); + let nodes = match unlisted_gap(walk, prefix, nodes)? { + Some(nodes) => nodes, + None => return Ok(()), + }; // The SDK leaves out a node it cannot decrypt, and says so only in its // log. Its name went with it, so all this can record is the folder. if nodes.len() < uids.len() { @@ -1186,10 +1194,14 @@ impl Core { let sized: HashMap)> = if stale.is_empty() { HashMap::new() } else { - self.rt + let full = self + .rt .block_on(self.drive.enumerate_nodes(&stale)) - .map_err(|e| format!("resolve nodes: {e}"))? - .iter() + .map_err(|e| format!("resolve nodes: {e}")); + let Some(full) = unlisted_gap(walk, prefix, full)? else { + return Ok(()); + }; + full.iter() .map(|n| { ( n.uid.to_string(), @@ -2007,6 +2019,34 @@ struct RemoteWalk<'a> { gaps: &'a mut RemoteGaps, } +/// A listing call's result in [`Core::walk_remote`]. A failure below the root +/// records `prefix` as unlisted and returns `None`, so the walk goes on +/// without that folder. A failure at the root still fails the pass, since +/// nothing would be left to compare. +/// +/// One listing error, after the SDK's own retries, used to fail the whole +/// walk. Nothing is applied before the walk ends, so on a link that drops a +/// request now and then a large first sync started over from nothing on +/// every pass and never got past it (`docs/BUGS.md` B271). +fn unlisted_gap( + walk: &mut RemoteWalk<'_>, + prefix: &str, + result: Result, +) -> Result, PassAbort> { + match result { + Ok(value) => Ok(Some(value)), + Err(error) if prefix.is_empty() => Err(error.into()), + Err(error) => { + warn!( + folder = prefix, + error, "sync: a folder on Drive could not be listed; leaving it out of this pass" + ); + walk.gaps.unlisted(prefix); + Ok(None) + } + } +} + /// Whether the file at `path` is no longer what the scan saw as `planned`. /// A file that is gone has nothing left to lose, so it has not changed. fn changed_since_scan(path: &Path, planned: &LocalItem) -> bool { diff --git a/crates/pdfs-fuse/src/sync/planner.rs b/crates/pdfs-fuse/src/sync/planner.rs index 988bdf6..42264ff 100644 --- a/crates/pdfs-fuse/src/sync/planner.rs +++ b/crates/pdfs-fuse/src/sync/planner.rs @@ -236,6 +236,9 @@ pub(super) struct RemoteGaps { short: HashSet, /// Paths seen, but whose entry could not be read. unread: HashSet, + /// Folders whose listing failed outright, after the SDK's retries. Nothing + /// under one is known, so all of it is left out of the pass (B271). + unlisted: HashSet, } impl RemoteGaps { @@ -249,8 +252,13 @@ impl RemoteGaps { self.unread.insert(rel); } + /// The listing of `folder` failed, so nothing under it was walked. + pub(super) fn unlisted(&mut self, folder: &str) { + self.unlisted.insert(folder.to_string()); + } + pub(super) fn is_empty(&self) -> bool { - self.short.is_empty() && self.unread.is_empty() + self.short.is_empty() && self.unread.is_empty() && self.unlisted.is_empty() } /// Whether `rel` may be missing from `remote` only because the walk could @@ -272,7 +280,7 @@ impl RemoteGaps { let mut at = parent_rel(rel); loop { if remote_dirs.contains_key(at) { - return self.short.contains(at); + return self.short.contains(at) || self.unlisted.contains(at); } if at.is_empty() { return false; @@ -295,6 +303,9 @@ impl RemoteGaps { .iter() .map(|rel| format!("{rel} (cannot be read on Proton Drive)")), ) + .chain(self.unlisted.iter().map(|rel| { + format!("{rel} (could not be listed on Proton Drive; tried again next pass)") + })) .collect(); lines.sort_unstable(); lines @@ -844,6 +855,32 @@ mod file_plan_tests { assert!(gaps.hides("top.txt", &remote, &dirs)); } + /// A folder whose listing failed was walked no further: nothing under it + /// reads as deleted on Drive, and a sibling that listed still does + /// (`docs/BUGS.md` B271). + #[test] + fn a_folder_that_failed_to_list_hides_everything_under_it() { + let remote = HashMap::from([ + ("docs".to_string(), folder()), + ("pics".to_string(), folder()), + ]); + let dirs = HashMap::from([ + (String::new(), uid("root")), + ("docs".to_string(), uid("docs")), + ("pics".to_string(), uid("pics")), + ]); + let mut gaps = RemoteGaps::default(); + gaps.unlisted("docs"); + assert!(gaps.hides("docs/a.txt", &remote, &dirs)); + assert!(gaps.hides("docs/sub/deep.txt", &remote, &dirs)); + assert!(!gaps.hides("docs", &remote, &dirs)); + assert!(!gaps.hides("pics/gone.jpg", &remote, &dirs)); + assert_eq!( + gaps.report(), + ["docs (could not be listed on Proton Drive; tried again next pass)"] + ); + } + #[test] fn a_file_whose_size_could_not_be_read_is_hidden() { let remote: HashMap = HashMap::new(); diff --git a/docs/BUGS.md b/docs/BUGS.md index 319b4ca..ed2a089 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -70,9 +70,7 @@ original wording. |---|---|---| | [B273](#b273--a-restore-proposes-another-computers-paths-when-the-two-share-a-hostname) | A restore proposes another computer's paths when the two share a hostname | Not fixed | | [B272](#b272--after-a-folder-is-removed-and-added-again-its-file-events-go-to-the-old-id) | After a folder is removed and added again, its file events go to the old id | Not fixed | -| [B271](#b271--one-failed-listing-makes-a-large-first-sync-start-over-from-nothing) | One failed listing makes a large first sync start over from nothing | Not fixed | | [B270](#b270--one-file-left-in-a-mirror-folder-clears-the-wipe-guard-for-all-the-others) | One file left in a mirror folder clears the wipe guard for all the others | Not fixed | -| [B269](#b269--a-restore-that-skipped-every-folder-reports-success) | A restore that skipped every folder reports success | Not fixed | | [B268](#b268--pdfs-diagnose-says-no-problems-found-while-sync-is-stuck) | `pdfs diagnose` says "No problems found" while sync is stuck | Not fixed | | [B267](#b267--a-key-failure-at-a-start-from-a-warm-cache-shows-as-offline-for-good) | A key failure at a start from a warm cache shows as offline for good | Not fixed | | [B266](#b266--pin-unpin-search-and-node-loads-report-a-database-error-as-offline) | Pin, unpin, search and node loads report a database error as "offline" | Not fixed | @@ -96,6 +94,7 @@ original wording. | Entry | Title | What is left | |---|---|---| +| [B269](#b269--a-restore-that-skipped-every-folder-reports-success) | A restore that skipped every folder reports success | The app shows the result only in the activity feed; no offer to replace or reset the folder in the way | | [B199](#b199--a-stopped-share-stays-on-the-shared-by-me-page-for-minutes) | A stopped share stays on the Shared by me page for minutes | The share itself is not deleted; the SDK has no call for it | | [B88](#b88--pdfs-pin-rename-move-delete-and-sharing-still-cannot-name-a-path-under-a-secondary-mount) | `pdfs pin`, `rename`, `move`, `delete` and sharing still cannot name a path under a secondary mount | `pin`, `unpin`, opening a file, thumbnails, uploads, sharing and versions | | [B72](#b72--pdfs-trash-never-returns) | `pdfs trash` never returns | The slowness of the first refresh is unmeasured | @@ -116,6 +115,7 @@ a live account since. | Entry | Title | |---|---| +| [B271](#b271--one-failed-listing-makes-a-large-first-sync-start-over-from-nothing) | One failed listing makes a large first sync start over from nothing | | [B256](#b256--pdfs-copy-right-after-a-save-copies-the-old-version) | `pdfs copy` right after a save copies the old version | | [B255](#b255--a-synced-folder-whose-files-are-all-gone-here-is-stuck-for-good) | A synced folder whose files are all gone here is stuck for good | | [B254](#b254--an-edit-in-the-same-second-as-a-new-upload-never-arrives) | An edit in the same second as a new upload never arrives | @@ -414,7 +414,7 @@ watched. ### B271 — One failed listing makes a large first sync start over from nothing -**Status:** Open +**Status:** Fixed, not verified live **Found:** 2026-10-31, code audit after issue #46: a folder of about 19,900 items restored on a second computer sat at `scanning…` with nothing downloaded, and the journal showed `RequestTimeout (http 408)`. @@ -431,8 +431,13 @@ the walk is done. Files that did download keep their baseline rows, so only the but on a first sync that is all of it. Progress is also counted only after a folder's full listing, so a large folder shows `scanning…` with 0 checked for a long time. -**Fix.** Not done. Record a failed subtree as a gap, as undecryptable nodes already are: no -deletions under it, and the rest of the pass still applies. +**Fix.** A listing that fails below the sync root records that folder as a gap in +`RemoteGaps`, next to the folders whose listing came back short. The planner deletes nothing +under a gap, the rest of the pass still applies, and the pass report names the folder as +tried again next pass. A failed listing of the root itself still fails the pass, since there is +nothing left to apply. The progress count inside one large folder is unchanged. + +**Test:** `a_folder_that_failed_to_list_hides_everything_under_it` in `sync::planner`. --- @@ -459,7 +464,7 @@ choice to the user: delete on the other side, or sync from scratch. ### B269 — A restore that skipped every folder reports success -**Status:** Open +**Status:** Partly fixed **Found:** 2026-10-31, code audit after issue #46: the journal showed `restore complete restored=0 skipped=1` several times, and the user saw nothing. **Where:** the `RestoreSyncFolders` and `RestoreDeviceFolders` handler in @@ -474,9 +479,14 @@ result, `restored 0 folder(s); skipped: …`, is then logged as a successful act reason is only in the activity text and the journal. When the database insert fails partway, the `?` also skips the `Rewatch` for the folders already restored. -**Fix.** Not done. Log a restore that skipped folders as a failure, name the reason in the app -and the CLI, and offer to replace or reset the folder in the way. Send the `Rewatch` on every -exit path. +**Fix.** `restore_sync_folders` returns what it restored and what it skipped, with the reason +for each skip. The daemon logs a restore that skipped any folder as a failed activity, with the +reasons as its detail, and warns in the journal. A database error partway now sends the +`Rewatch` for the folders already restored before it returns. `pdfs devices join` points to +`pdfs activity` for the result. + +Left: the app shows the result only in the activity feed, and nothing offers to replace or reset +the folder in the way. --- From fccb3d956103e578741b0f8351560e265c44b2db Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 31 Oct 2026 12:58:11 +0100 Subject: [PATCH 10/12] fix(fuse): retry a write open whose queued revision landed under the copy A write open copies the queued revision's blob into its scratch file. When the upload landed between the sample and the copy, the drain had already deleted the blob, and the open failed with EIO (B274, seen once in CI). The drain drops the pending entry before the blob, so a failed copy whose entry moved on now goes around again, as the re-check after the copy already does. Two simulations raced the daemon rather than it: - a_file_deleted_after_a_restart_leaves_drive_with_what_its_create_sent let the create's reply through before the kernel's release attached the second bytes. That attach resets the attempts, so the retry landed before the test could see the failure. It now waits for the bytes to be attached. - a_file_deleted_offline_while_open_still_reads_and_takes_writes read the cache as the queue emptied, which is a moment before the eviction. It now waits for the cache to empty. --- crates/pdfs-fuse/src/filesystem.rs | 8 ++++++ crates/pdfs-fuse/src/sim/daemon.rs | 39 ++++++++++++++++++++++++------ docs/BUGS.md | 27 +++++++++++++++++++++ 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/crates/pdfs-fuse/src/filesystem.rs b/crates/pdfs-fuse/src/filesystem.rs index 5d3e443..4f6dd33 100644 --- a/crates/pdfs-fuse/src/filesystem.rs +++ b/crates/pdfs-fuse/src/filesystem.rs @@ -1434,6 +1434,14 @@ impl ProtonFs { // (`ContentCache::clone_or_copy`). if let Err(e) = ContentCache::clone_or_copy(&pending.path, &path) { let _ = std::fs::remove_file(&path); + // The drain lets go of the pending entry before it + // discards the blob, so a blob that went away under the + // copy is a revision that landed or was replaced: the + // same race the re-check below catches, only earlier. + if self.core.pending_blob(&uid).as_ref() != Some(&pending.path) { + warn!(%uid, attempt, "queued revision went away while opening for write; retrying"); + continue; + } error!(%uid, source = %pending.path.display(), error = %e, "copy queued revision into write scratch failed"); break 'install Err(Errno::EIO); diff --git a/crates/pdfs-fuse/src/sim/daemon.rs b/crates/pdfs-fuse/src/sim/daemon.rs index 52c6080..7081a5e 100644 --- a/crates/pdfs-fuse/src/sim/daemon.rs +++ b/crates/pdfs-fuse/src/sim/daemon.rs @@ -592,14 +592,22 @@ mod tests { daemon.pending(), drive.tree() ); - // The bytes kept for the handle went once the trash landed with it closed. - let cached: Vec<_> = std::fs::read_dir(dir.join("cache")) - .unwrap() - .filter_map(|e| e.ok()) - .filter(|e| e.file_type().is_ok_and(|t| t.is_file())) - .map(|e| e.file_name()) - .collect(); - assert!(cached.is_empty(), "{cached:?}"); + // The bytes kept for the handle went once the trash landed with it + // closed. The queue empties as the trash's row goes, a moment before + // the eviction, so this waits too. + let cached = || -> Vec<_> { + std::fs::read_dir(dir.join("cache")) + .unwrap() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_ok_and(|t| t.is_file())) + .map(|e| e.file_name()) + .collect() + }; + assert!( + wait_until(Duration::from_secs(30), || cached().is_empty()), + "{:?}", + cached() + ); assert!(matches!(daemon.stop(), Ok(MountOutcome::Shutdown))); let _ = std::fs::remove_dir_all(&dir); @@ -1998,6 +2006,21 @@ mod tests { daemon.write_new(&path, b"first").unwrap(); assert!(wait_until(Duration::from_secs(30), || held.reached())); std::fs::write(&path, b"second").unwrap(); + // The kernel sends the release after `close` returns. Attaching the + // bytes resets the create's attempts, so a release that came after the + // lost reply had it retried at once, and the retry landed before + // anything here saw it fail. The bytes it replaced go only once they + // are attached. + let staged = dir.join("cache").join("staging"); + assert!(wait_until(Duration::from_secs(30), || { + let blobs: Vec> = std::fs::read_dir(&staged) + .into_iter() + .flatten() + .filter_map(|e| e.ok()) + .filter_map(|e| std::fs::read(e.path()).ok()) + .collect(); + blobs.iter().any(|b| b == b"second") && !blobs.iter().any(|b| b == b"first") + })); drop(held); assert!(wait_until(Duration::from_secs(30), || { daemon diff --git a/docs/BUGS.md b/docs/BUGS.md index 319b4ca..a66532e 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -116,6 +116,7 @@ a live account since. | Entry | Title | |---|---| +| [B274](#b274--opening-a-file-for-writing-fails-with-eio-while-its-queued-revision-lands) | Opening a file for writing fails with EIO while its queued revision lands | | [B256](#b256--pdfs-copy-right-after-a-save-copies-the-old-version) | `pdfs copy` right after a save copies the old version | | [B255](#b255--a-synced-folder-whose-files-are-all-gone-here-is-stuck-for-good) | A synced folder whose files are all gone here is stuck for good | | [B254](#b254--an-edit-in-the-same-second-as-a-new-upload-never-arrives) | An edit in the same second as a new upload never arrives | @@ -375,6 +376,32 @@ a live account since. Newest first. +### B274 — Opening a file for writing fails with EIO while its queued revision lands + +**Status:** Fixed, not verified live +**Found:** 2026-10-31, CI: the simulation +`a_file_whose_upload_was_not_read_back_takes_two_writes_at_once` failed once with +`Input/output error` on a write, after `copy queued revision into write scratch failed … No +such file or directory`. It did not fail in 54 local runs. +**Where:** the write open in `crates/pdfs-fuse/src/filesystem.rs` + +**Repro:** Write a file, and open it for writing again at the moment its queued revision +finishes uploading. The open fails with EIO, and the program sees its save fail. + +**Cause.** The open samples the queued revision, then copies its blob into the new handle's +scratch file. The drain lets go of the pending entry and then deletes the blob when the upload +lands. A blob deleted between the sample and the copy failed the open, although the re-check +just after the copy already retries a revision that changed during it. + +**Fix.** A failed copy checks whether the pending entry still names that blob. When it does +not, the revision landed or was replaced, and the open goes around again with the base the +file has now, as the re-check does. Since the drain drops the entry before the blob, a blob +that is gone and still named is a real failure and still returns EIO. + +**Test:** none deterministic; the simulation above covers the window. + +--- + ### B273 — A restore proposes another computer's paths when the two share a hostname **Status:** Open From 53543c91f9af1a466ee7052bea45396efc120ac7 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 31 Oct 2026 13:16:47 +0100 Subject: [PATCH 11/12] docs(bugs): record the slow-link conflict copy seen in CI (B275) --- docs/BUGS.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/BUGS.md b/docs/BUGS.md index a66532e..7caddf1 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -68,6 +68,7 @@ original wording. | Entry | Title | What is left | |---|---|---| +| [B275](#b275--on-a-slow-link-a-files-last-write-can-land-only-in-a-conflict-copy) | On a slow link, a file's last write can land only in a conflict copy | Cause not found; seen once in CI | | [B273](#b273--a-restore-proposes-another-computers-paths-when-the-two-share-a-hostname) | A restore proposes another computer's paths when the two share a hostname | Not fixed | | [B272](#b272--after-a-folder-is-removed-and-added-again-its-file-events-go-to-the-old-id) | After a folder is removed and added again, its file events go to the old id | Not fixed | | [B271](#b271--one-failed-listing-makes-a-large-first-sync-start-over-from-nothing) | One failed listing makes a large first sync start over from nothing | Not fixed | @@ -376,6 +377,30 @@ a live account since. Newest first. +### B275 — On a slow link, a file's last write can land only in a conflict copy + +**Status:** Open +**Found:** 2026-10-31, CI on #60: `one_client_on_a_slow_link`, profile `one-client-wifi` seed 2, +failed with `Drive does not hold what the clients wrote, 120s after the links came up`. The +same seed passed 4 times locally, and #60 changes no daemon logic, so the failure depends on +timing. +**Where:** not located; the drain's conflict check in `crates/pdfs-fuse/src/drain.rs` is the +first suspect + +**Repro:** Only the CI run above. The last steps created `x/z/y/a.txt` (37 bytes), wrote 42 +bytes into it at offset 20, and brought every link up. Drive then held the file at its first +37 bytes, and the full content was in `x/z/y/a (sync-conflict …).txt`. No other client wrote +to it. + +**Cause.** Not known. Nothing was lost, since the conflict copy holds the bytes, but the file +conflicted with its own create. B183 and B184 found the same symptom after a lost create +reply. + +**Fix.** Not done. Needs a reproduction: run the seed in a loop under load and keep the state +directory of a failing run. + +--- + ### B274 — Opening a file for writing fails with EIO while its queued revision lands **Status:** Fixed, not verified live From 7957e1ed59e896fd878ebcbd22445afb6f169958 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 31 Oct 2026 13:48:01 +0100 Subject: [PATCH 12/12] chore(deps): bump proton-sdk and proton-drive-rs to 0.10.1 0.10.1 decrypts address key tokens with every account key that opens, not only the active ones, and leaves out an address whose keys do not open instead of failing the account. That is the likely cause of issue #40, a mount that retried "missing key" forever (B265). --- Cargo.lock | 8 ++++---- Cargo.toml | 4 ++-- docs/BUGS.md | 17 +++++++++++++---- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 903bfe2..6ba869b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3779,9 +3779,9 @@ dependencies = [ [[package]] name = "proton-drive-rs" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f70318394b2815b009c52f804d75747086db93a338ceb6eae62541f2b25d5bf" +checksum = "a8fa2a88130883010b32035e208aaa36fb275f6d7d78ed44a1069b1d33d24481" dependencies = [ "async-trait", "base64 0.23.1", @@ -3806,9 +3806,9 @@ dependencies = [ [[package]] name = "proton-sdk" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4196392032f56b3bbaac0bb1f24eea62fa7e2a0af72004de8d08e518d14720fb" +checksum = "af0bf8d50fa829d1a65b09c6855b606578d02734d4089874c971d1063c23c353" dependencies = [ "aes-gcm 0.11.1", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 0911a02..92c34d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,8 @@ keywords = ["proton", "drive", "fuse", "sync", "linux"] categories = ["filesystem", "command-line-utilities"] [workspace.dependencies] -proton-sdk = { version = "0.10.0" } -proton-drive-rs = { version = "0.10.0" } +proton-sdk = { version = "0.10.1" } +proton-drive-rs = { version = "0.10.1" } # Internal crates. pdfs-core = { path = "crates/pdfs-core" } diff --git a/docs/BUGS.md b/docs/BUGS.md index 319b4ca..4b20edd 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -76,7 +76,6 @@ original wording. | [B268](#b268--pdfs-diagnose-says-no-problems-found-while-sync-is-stuck) | `pdfs diagnose` says "No problems found" while sync is stuck | Not fixed | | [B267](#b267--a-key-failure-at-a-start-from-a-warm-cache-shows-as-offline-for-good) | A key failure at a start from a warm cache shows as offline for good | Not fixed | | [B266](#b266--pin-unpin-search-and-node-loads-report-a-database-error-as-offline) | Pin, unpin, search and node loads report a database error as "offline" | Not fixed | -| [B265](#b265--address-keys-whose-token-is-for-an-inactive-account-key-never-open) | Address keys whose token is for an inactive account key never open | Needs an SDK change; not reproduced | | [B264](#b264--a-mount-that-fails-after-starting-its-workers-leaves-them-running) | A mount that fails after starting its workers leaves them running | Not fixed | | [B263](#b263--a-mount-that-can-never-succeed-retries-every-5-s-and-tells-only-the-journal) | A mount that can never succeed retries every 5 s and tells only the journal | Not fixed | | [B262](#b262--watching-a-large-synced-folder-holds-up-the-sync-engine-and-a-failed-watch-is-never-retried) | Watching a large synced folder holds up the sync engine, and a failed watch is never retried | Not fixed | @@ -116,6 +115,7 @@ a live account since. | Entry | Title | |---|---| +| [B265](#b265--address-keys-whose-token-is-for-an-inactive-account-key-never-open) | Address keys whose token is for an inactive account key never open | | [B256](#b256--pdfs-copy-right-after-a-save-copies-the-old-version) | `pdfs copy` right after a save copies the old version | | [B255](#b255--a-synced-folder-whose-files-are-all-gone-here-is-stuck-for-good) | A synced folder whose files are all gone here is stuck for good | | [B254](#b254--an-edit-in-the-same-second-as-a-new-upload-never-arrives) | An edit in the same second as a new upload never arrives | @@ -543,7 +543,7 @@ chain in `from_api` before falling back. ### B265 — Address keys whose token is for an inactive account key never open -**Status:** Open +**Status:** Fixed, not verified live **Found:** 2026-10-31, code audit after issue #40: a Duo account whose mount failed on every start with `fetch My Files root: cryptography error: failed to decrypt: missing key`, after three `failed to decrypt address key token` warnings. The web app worked. B210's fix does not apply, @@ -560,8 +560,17 @@ already reports "token is for inactive account key", so the case is known. Separ `addresses()` fails as a whole through `?` when one address has no usable primary key, which takes the working addresses down with it. -**Fix.** Not done, and it belongs in the SDK. Use every account key that opens for decrypting -tokens, and only the active ones for signing. Skip and log an address that cannot be opened. +**Fix.** In `proton-sdk` 0.10.1 (narrrl/proton-sdk-rs#3). Tokens are tried with every account +key that opens, active ones first, and signing uses active keys only. An address that cannot be +opened is left out with a warning, and the others work; `addresses()` fails only when none +opens. When the left-out address is the default, `default_address` fails with the reason +rather than signing with another address. `key_report` walks the same chain, so `pdfs diagnose` +reports such a key as opening. Not verified against an account with an inactive key, since +none is at hand; issue #40's reporter can confirm. + +**Test:** `an_address_key_whose_token_is_for_an_inactive_account_key_opens`, +`an_address_whose_keys_do_not_open_leaves_the_others_working` and +`an_account_whose_addresses_all_fail_to_open_is_an_error` in `proton-sdk`. ---