diff --git a/Cargo.lock b/Cargo.lock index 97ce9de..6ea968b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3474,7 +3474,7 @@ checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "pdfs-cli" -version = "3.1.1" +version = "3.1.2" dependencies = [ "anyhow", "clap", @@ -3490,7 +3490,7 @@ dependencies = [ [[package]] name = "pdfs-core" -version = "3.1.1" +version = "3.1.2" dependencies = [ "async-trait", "base64 0.23.1", @@ -3516,7 +3516,7 @@ dependencies = [ [[package]] name = "pdfs-fuse" -version = "3.1.1" +version = "3.1.2" dependencies = [ "async-trait", "base64 0.23.1", @@ -3538,7 +3538,7 @@ dependencies = [ [[package]] name = "pdfs-gui" -version = "3.1.1" +version = "3.1.2" dependencies = [ "anyhow", "async-channel", diff --git a/Cargo.toml b/Cargo.toml index ed2a1a5..63a100f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/pdfs-core", "crates/pdfs-fuse", "crates/pdfs-cli", "crates/pdfs-gui"] [workspace.package] -version = "3.1.1" +version = "3.1.2" edition = "2024" license = "MIT" authors = ["Nils Pukropp "] diff --git a/crates/pdfs-gui/src/app/main.rs b/crates/pdfs-gui/src/app/main.rs index e2b8a16..9bf08a5 100644 --- a/crates/pdfs-gui/src/app/main.rs +++ b/crates/pdfs-gui/src/app/main.rs @@ -202,12 +202,20 @@ impl Ui { } fn main() -> glib::ExitCode { - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), - ) - .init(); + let verify = std::env::args().nth(1).as_deref() == Some(VERIFY_ARG); + let logs = tracing_subscriber::fmt().with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), + ); + // The verification window's stdout carries its token back to the app. + if verify { + logs.with_writer(std::io::stderr).init(); + } else { + logs.init(); + } i18n::init(); + if verify { + return run_verification_window(); + } // The tray launches the app with arguments ("--page locations", // "--confirm-stop"), which a running instance must receive too. @@ -303,6 +311,11 @@ fn build_window(app: &adw::Application) { return; } }; + // On a fresh install the daemon has not run yet to make these, and the + // Takeout page keeps its staged archives in the state dir (B194). + if let Err(e) = dirs.ensure() { + tracing::warn!("cannot create the app's directories: {e}"); + } let stack = adw::ViewStack::new(); let (login_page, login_widgets) = build_login_page(); diff --git a/crates/pdfs-gui/src/app/pages/login.rs b/crates/pdfs-gui/src/app/pages/login.rs index 31d406d..c87fdbb 100644 --- a/crates/pdfs-gui/src/app/pages/login.rs +++ b/crates/pdfs-gui/src/app/pages/login.rs @@ -150,15 +150,27 @@ pub(crate) fn wire_login(ui: &Rc) { }); // Same shape for human verification: only fires when the API gates the - // sign-in, and the worker is blocked on the token until it does. + // sign-in, and the worker is blocked on the token until it does. A + // window that never came up is told apart from one the user closed, so + // the result below can say why the sign-in stopped. let ui_hv = ui.clone(); + let verification_failed = Rc::new(Cell::new(false)); + let hv_failed = verification_failed.clone(); glib::spawn_future_local(async move { if let Ok((url, token_tx)) = hv_req_rx.recv().await { ui_hv .login .login_status .set_text(&gettext("Complete the verification to continue…")); - prompt_human_verification(&ui_hv, &url, token_tx); + // Anything but a token drops the sender, which the worker reads + // as a cancelled sign-in. + match verify_human(url).await { + Verification::Solved(token) => { + let _ = token_tx.send(token); + } + Verification::Cancelled => {} + Verification::Failed => hv_failed.set(true), + } } }); @@ -194,6 +206,9 @@ pub(crate) fn wire_login(ui: &Rc) { let _ = gio::spawn_blocking(service::enable_start).await; refresh(&ui); } + Err(_) if verification_failed.get() => ui.login.login_status.set_text(&gettext( + "The verification page couldn't be opened, so the sign-in was cancelled. Your system may keep the web view's sandbox from starting; the troubleshooting guide explains what to do.", + )), Err(e) => ui.login.login_status.set_text(&e), } }); diff --git a/crates/pdfs-gui/src/app/pages/verify.rs b/crates/pdfs-gui/src/app/pages/verify.rs index bc31fe1..1b55f93 100644 --- a/crates/pdfs-gui/src/app/pages/verify.rs +++ b/crates/pdfs-gui/src/app/pages/verify.rs @@ -1,4 +1,4 @@ -//! The human-verification (CAPTCHA) dialog. +//! The human-verification (CAPTCHA) window. //! //! Proton gates logins it does not recognise — a new IP, a VPN exit — behind a //! CAPTCHA. The API answers the login with a challenge instead of a session, and @@ -8,10 +8,26 @@ //! The page is hosted in a `WebKitWebView` rather than handed to the system //! browser because it reports completion by posting a message to its host, not //! by redirecting: a browser has nowhere to post it back to. +//! +//! The webview runs in a child `pdfs-app` started with [`VERIFY_ARG`], not in the +//! app itself. WebKit aborts its whole process when its sandbox cannot start, as +//! on hardened systems that keep bubblewrap from the journal socket +//! (`docs/BUGS.md` B195); in a child that costs the verification, not the app. +//! The URL goes to the child on stdin, so it never shows in a process list, and +//! the token comes back on stdout. use crate::*; +use std::io::{BufRead, BufReader, Write as _}; +use std::process::{ExitStatus, Stdio}; use webkit6::prelude::*; +/// The argument that makes `pdfs-app` the verification window instead of the app. +pub(crate) const VERIFY_ARG: &str = "--human-verification"; + +/// Marks the token among whatever else lands on the child's stdout: GLib and +/// WebKit write debug output there. +const TOKEN_PREFIX: &str = "hv-token "; + /// Bridge the verification page's `postMessage` to a handler this side can read. /// /// The page targets its embedding host (`window.parent`), which in a webview is @@ -26,15 +42,100 @@ window.addEventListener('message', function (event) { }); "#; -/// Present the challenge and hand the solved token back through `token_tx`. -/// -/// Closing the dialog without solving drops the sender, which the waiting login -/// worker reads as a cancelled sign-in — the same contract as the 2FA prompt. -pub(crate) fn prompt_human_verification( - ui: &Rc, - url: &str, - token_tx: std::sync::mpsc::Sender, -) { +/// How the verification window ended. +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum Verification { + Solved(String), + /// Closed without solving. + Cancelled, + /// Never came up, or died: most likely WebKit could not start its sandbox. + Failed, +} + +/// Show the challenge in the verification window and wait until it closes. +pub(crate) async fn verify_human(url: String) -> Verification { + match gio::spawn_blocking(move || run_verification_child(&url)).await { + Ok(Ok(verification)) => verification, + Ok(Err(e)) => { + tracing::error!("cannot run the verification window: {e}"); + Verification::Failed + } + Err(_) => Verification::Failed, + } +} + +fn run_verification_child(url: &str) -> std::io::Result { + let mut child = Command::new(own_exe()) + .arg(VERIFY_ARG) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + if let Some(mut stdin) = child.stdin.take() { + // A failed write means the child is already gone; its exit status says + // how. Dropping `stdin` closes it. + let _ = writeln!(stdin, "{url}"); + } + // Stops reading at the token rather than at EOF: WebKit's helper processes + // inherit the pipe and may hold it open a moment after the child quits. + let token = child + .stdout + .take() + .and_then(|out| find_token(BufReader::new(out))); + let status = child.wait()?; + Ok(outcome(token, status)) +} + +/// This very binary, so a development build opens its own window. After an +/// upgrade replaced the file, `current_exe` names a deleted path, and the +/// installed `pdfs-app` is the next best. +fn own_exe() -> PathBuf { + std::env::current_exe() + .ok() + .filter(|path| path.is_file()) + .unwrap_or_else(|| PathBuf::from("pdfs-app")) +} + +fn find_token(out: impl BufRead) -> Option { + out.lines() + .map_while(Result::ok) + .find_map(|line| line.strip_prefix(TOKEN_PREFIX).map(str::to_owned)) +} + +/// A token wins over the exit status: the user solved the page, even if +/// WebKit's teardown crashed after it. +fn outcome(token: Option, status: ExitStatus) -> Verification { + match token { + Some(token) => Verification::Solved(token), + None if status.success() => Verification::Cancelled, + None => { + tracing::error!(%status, "the verification window failed"); + Verification::Failed + } + } +} + +/// Run as the verification window: read the page's URL from stdin, show it, and +/// print the token once the user has solved it. Closing the window exits +/// without a token, which the app reads as a cancelled sign-in. +pub(crate) fn run_verification_window() -> glib::ExitCode { + let mut url = String::new(); + if std::io::stdin().lock().read_line(&mut url).is_err() || url.trim().is_empty() { + tracing::error!("{VERIFY_ARG} expects the verification URL on stdin"); + return glib::ExitCode::FAILURE; + } + let url = url.trim().to_owned(); + + // Not unique: the app that started it already holds APP_ID. + let app = adw::Application::builder() + .flags(gio::ApplicationFlags::NON_UNIQUE) + .build(); + app.connect_activate(move |app| build_verification_window(app, &url)); + // GApplication would refuse VERIFY_ARG as an unknown option. + let argv0 = std::env::args().next().unwrap_or_default(); + app.run_with_args(&[argv0]) +} + +fn build_verification_window(app: &adw::Application, url: &str) { let content = webkit6::UserContentManager::new(); content.add_script(&webkit6::UserScript::new( BRIDGE, @@ -53,24 +154,19 @@ pub(crate) fn prompt_human_verification( .hexpand(true) .build(); - let dialog = adw::Dialog::builder() + let body = gtk4::Box::new(gtk4::Orientation::Vertical, 0); + body.append(&adw::HeaderBar::new()); + body.append(&webview); + let window = adw::ApplicationWindow::builder() + .application(app) .title(gettext("Verification")) - .content_width(420) - .content_height(560) + .icon_name(APP_ID) + .default_width(420) + .default_height(560) + .content(&body) .build(); - let header = adw::HeaderBar::new(); - let body = gtk4::Box::new(gtk4::Orientation::Vertical, 0); - body.append(&header); - body.append(&webview); - dialog.set_child(Some(&body)); - - // Taken on the first success *or* on close, so the token is sent at most - // once and a cancelled dialog always drops the sender. - let token_tx = Rc::new(RefCell::new(Some(token_tx))); - - let tx = token_tx.clone(); - let dlg_weak = dialog.downgrade(); + let app = app.downgrade(); content.connect_script_message_received(Some("hv"), move |_, value| { let Some(token) = extract_token(&value.to_str()) else { // Every `postMessage` on the page reaches here, most of them the @@ -78,25 +174,17 @@ pub(crate) fn prompt_human_verification( // is simply not ours. return; }; - if let Some(tx) = tx.borrow_mut().take() { - let _ = tx.send(token); + let mut stdout = std::io::stdout().lock(); + if let Err(e) = writeln!(stdout, "{TOKEN_PREFIX}{token}").and_then(|()| stdout.flush()) { + tracing::error!("cannot hand the verification token back: {e}"); } - if let Some(dlg) = dlg_weak.upgrade() { - dlg.close(); + if let Some(app) = app.upgrade() { + app.quit(); } }); - // Covers the close button, Escape, and the user giving up. - dialog.connect_closed(move |_| { - token_tx.borrow_mut().take(); - }); - webview.load_uri(url); - - let parent = ui.login.login_button.root().and_downcast::(); - // Returns immediately; the login worker stays blocked on the channel until - // the handler above sends, or the dialog closes and drops the sender. - dialog.present(parent.as_ref()); + window.present(); } /// Pull the verification token out of a message posted by the verification page. @@ -133,7 +221,12 @@ fn extract_token(raw: &str) -> Option { #[cfg(test)] mod tests { - use super::extract_token; + use super::{Verification, extract_token, find_token, outcome}; + use std::os::unix::process::ExitStatusExt as _; + use std::process::ExitStatus; + + /// The wait status of a process killed by SIGABRT. + const ABORTED: i32 = 6; #[test] fn a_completion_message_yields_its_token() { @@ -163,4 +256,39 @@ mod tests { assert!(extract_token(raw).is_none(), "accepted: {raw}"); } } + + #[test] + fn the_token_is_found_among_other_output() { + let out = "Gtk-Message: something\nhv-token tok-1\nmore\n"; + assert_eq!(find_token(out.as_bytes()).as_deref(), Some("tok-1")); + assert_eq!(find_token("only chatter\n".as_bytes()), None); + } + + #[test] + fn a_window_closed_without_a_token_cancels() { + assert_eq!( + outcome(None, ExitStatus::from_raw(0)), + Verification::Cancelled + ); + } + + /// WebKit aborts when its sandbox cannot start (B195). That must read as a + /// failure the app explains, not as the user giving up. + #[test] + fn a_window_that_died_fails() { + assert_eq!( + outcome(None, ExitStatus::from_raw(ABORTED)), + Verification::Failed + ); + let exited_1 = ExitStatus::from_raw(1 << 8); + assert_eq!(outcome(None, exited_1), Verification::Failed); + } + + #[test] + fn a_token_counts_even_if_the_window_died_after_it() { + assert_eq!( + outcome(Some("tok-1".into()), ExitStatus::from_raw(ABORTED)), + Verification::Solved("tok-1".into()) + ); + } } diff --git a/crates/pdfs-gui/src/main.rs b/crates/pdfs-gui/src/main.rs index 3d7d2f6..27ae2d0 100644 --- a/crates/pdfs-gui/src/main.rs +++ b/crates/pdfs-gui/src/main.rs @@ -485,6 +485,10 @@ fn main() -> Result<()> { return Ok(()); } + // The socket lives in the state dir, which nothing has made yet when the app + // starts the tray on a fresh install (B194). + dirs.ensure().context("create the app's directories")?; + // Ensure only one instance of the tray runs. let tray_sock = dirs.tray_socket(); if std::os::unix::net::UnixStream::connect(&tray_sock).is_ok() { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e71a462..a0ea309 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -542,7 +542,7 @@ sequenceDiagram actor User participant UI as pdfs-app participant Auth as pdfs-core auth - participant Web as WebKitGTK dialog + participant Web as pdfs-app --human-verification participant API as Proton API User->>UI: email and password @@ -550,9 +550,9 @@ sequenceDiagram Auth->>API: SRP handshake API-->>Auth: 9001, challenge URL Auth-->>UI: HumanVerificationRequired - UI->>Web: load challenge + UI->>Web: challenge URL (stdin) User->>Web: solve it - Web-->>UI: token (postMessage) + Web-->>UI: token (stdout) UI->>Auth: login_verified(token) Auth->>API: new SRP handshake with the token API-->>Auth: session @@ -561,6 +561,9 @@ sequenceDiagram - The challenge used up the first SRP handshake, so the retry starts a new one with the token attached. - The page may post the token as an object or as a JSON string; both are accepted. +- The page runs in a WebKitGTK view inside a child `pdfs-app`. WebKit aborts its process when its + sandbox cannot start, as on hardened systems such as secureblue (B195). In the child, that ends + the verification, and the app says why, instead of closing the app. - The CLI has no web view. It fails with a message to sign in once with `pdfs-app`; the stored session then works for the CLI and the service. diff --git a/docs/BUGS.md b/docs/BUGS.md index a4de421..9cf6517 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -47,7 +47,7 @@ original wording. it in the index in the same change. ```markdown -### B194 — What the user sees, in one line +### B196 — What the user sees, in one line **Status:** Open **Found:** 2026-10-06, how it was found @@ -100,6 +100,7 @@ a live account since. | Entry | Title | |---|---| +| [B195](#b195--the-app-crashes-when-proton-asks-for-a-captcha-on-secureblue) | The app crashes when Proton asks for a CAPTCHA on secureblue | | [B193](#b193--deleting-a-file-whose-create-failed-can-trash-a-file-of-the-users-on-drive) | Deleting a file whose create failed can trash a file of the user's on Drive | | [B191](#b191--a-file-deleted-after-its-create-failed-and-was-written-again-can-stay-on-drive) | A file deleted after its create failed and was written again can stay on Drive | | [B190](#b190--a-file-saved-just-before-the-daemon-stops-can-keep-its-old-content-on-drive) | A file saved just before the daemon stops can keep its old content on Drive | @@ -214,6 +215,7 @@ a live account since. | Entry | Title | |---|---| +| [B194](#b194--on-a-fresh-install-the-tray-exits-and-the-takeout-page-cannot-remember-its-archives) | On a fresh install the tray exits and the Takeout page cannot remember its archives | | [B192](#b192--a-new-file-closed-empty-reaches-drive-only-half-a-minute-later) | A new file closed empty reaches Drive only half a minute later | | [B181](#b181--the-app-shows-a-queued-edit-kept-as-a-conflict-copy-in-english) | The app shows a queued edit kept as a conflict copy in English | | [B180](#b180--a-stop-waits-10-s-for-a-drain-worker-that-slept-through-it) | A stop waits 10 s for a drain worker that slept through it | @@ -296,6 +298,60 @@ a live account since. Newest first. +### B195 — The app crashes when Proton asks for a CAPTCHA on secureblue + +**Status:** Fixed (unverified). Nobody has run the fix on secureblue yet. +**Found:** 2026-10-07, [#29](https://github.com/narrrl/proton-drive-linux/issues/29). On +secureblue 44 (a hardened Fedora Atomic), `pdfs-app` aborted a few seconds after the sign-in, with +`bwrap: Can't get type of source /run/systemd/journal/socket: Permission denied` and +`Failed to fully launch dbus-proxy`. +**Where:** `crates/pdfs-gui/src/app/pages/verify.rs`, `crates/pdfs-gui/src/app/pages/login.rs` + +**Repro:** Sign in on secureblue from a network Proton asks a CAPTCHA for, such as a VPN. + +**Cause.** The CAPTCHA page runs in a WebKitGTK view (B8). Before it loads anything, WebKit starts +its sandbox: bubblewrap, with the journal socket bound in, and `xdg-dbus-proxy`. secureblue keeps +bubblewrap from the socket, the proxy does not start, and WebKit ends its process with `g_error`. +The view was in the app's own process, so the whole app went down, and nothing in our code can +catch that. + +**Fix.** The view runs in a child, `pdfs-app --human-verification`, that reads the URL on stdin +and prints the token on stdout. A child that exits without a token after a crash or a non-zero +status ends the sign-in with a message that says the page could not be opened. TROUBLESHOOTING.md +gives the cause and a one-time workaround. The sandbox is not switched off by the app: users choose +a hardened system for that isolation. + +**Test:** unit tests `a_window_that_died_fails`, `a_window_closed_without_a_token_cancels`, +`a_token_counts_even_if_the_window_died_after_it` and `the_token_is_found_among_other_output`. +On 2026-10-08 the child ran under real WebKit on Arch against a local page that posts a completion, +and handed back its token. + +--- + +### B194 — On a fresh install the tray exits and the Takeout page cannot remember its archives + +**Status:** Fixed. +**Found:** 2026-10-07, in the log of [#29](https://github.com/narrrl/proton-drive-linux/issues/29): +`failed to bind tray single-instance socket: No such file or directory` and +`cannot remember staged Takeout archives: No such file or directory`. +**Where:** `crates/pdfs-gui/src/main.rs`, `build_window` in `crates/pdfs-gui/src/app/main.rs` + +**Repro:** Start `pdfs-app` with no `~/.local/state/proton-drive-linux`, as on a fresh install. The +tray is gone, and staging a Takeout archive logs the warning. + +**Cause.** Both write to the state directory, and only the daemon and the CLI made it +(`AppDirs::ensure`). The app starts the tray before the first sign-in, so before the daemon has +ever run. + +**Fix.** `pdfs-tray` and `pdfs-app` call `AppDirs::ensure` at start, which also makes the +directories owner-only (B6). + +**Verified:** 2026-10-08, `pdfs-tray` with `XDG_STATE_HOME`, `XDG_CONFIG_HOME` and +`XDG_CACHE_HOME` pointing at an empty directory. It exited with the error before the fix, and +afterwards ran with `tray.sock` in a `0700` state directory. + +--- + ### B193 — Deleting a file whose create failed can trash a file of the user's on Drive **Status:** Fixed (unverified). diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 98cbda7..cdf8744 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,36 @@ scratch (user data in `staging/` and `recovery/` is never touched by this). ## [Unreleased] +## [3.1.2] — 2026-10-08 + +Schema: **42**, unchanged. + +### Fixed +- **The app crashed when Proton asked for a CAPTCHA on hardened systems.** On secureblue, WebKit + cannot start the sandbox for the verification page and ends its process, which took the app + down with it. The page now opens in a separate window process: if that process fails, the + sign-in stops with a message that says why, and TROUBLESHOOTING.md has a workaround. + ([#29](https://github.com/narrrl/proton-drive-linux/issues/29)) +- **On a fresh install the tray icon did not start.** The tray and the app now create their state + directory themselves instead of waiting for the service to do it. This also lets the Takeout + page remember staged archives before the first mount. + ([#29](https://github.com/narrrl/proton-drive-linux/issues/29)) + +## [3.1.1] — 2026-10-07 + +Schema: **42**, unchanged. + +### Changed +- **The client identifies itself with its release.** Sign-in and every API call used to send the + fixed `external-drive-linux@0.1.0-alpha` and `proton-drive-linux/0.1.0`. If Proton retired that + version, every release would fail to sign in at once and no update could fix it. The client now + sends `external-drive-linux@-stable` and `proton-drive-linux/`. `app_version` + and `user_agent` in `config.json` are optional overrides now. The fixed values that releases up + to 3.1.0 wrote there count as unset and are removed on the next save. A value you set by hand is + kept. +- The About dialog shows the release version, and its debug info shows the identity that is + actually sent. + ## [3.1.0] — 2026-10-07 Schema: **42**, unchanged. @@ -1635,6 +1665,12 @@ First stable release: FUSE files-on-demand mount, sync daemon under `proton-driv - The outstanding FUSE defects tracked in `docs/BUGS.md`, plus a truncate defect, validated by a new POSIX compliance suite for the filesystem. +[3.1.2]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.1.2 + +[3.1.1]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.1.1 + +[3.1.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.1.0 + [3.0.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.0.0 [2.10.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v2.10.0 diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index e733661..aab2f5c 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -72,6 +72,20 @@ pdfs status unfamiliar network. The CLI cannot show it. Sign in with `pdfs-app`, which displays the challenge; the stored session then works for the CLI too. +**"The verification page couldn't be opened"** — WebKit, which shows the CAPTCHA page, could not +start its sandbox. Hardened systems such as secureblue keep its bubblewrap from the journal socket, +and the log then shows `bwrap: Can't get type of source /run/systemd/journal/socket: Permission +denied` and `Failed to fully launch dbus-proxy`. Check that `bwrap` and `xdg-dbus-proxy` are +installed. If they are, sign in once with the sandbox switched off for this one run: + +```bash +WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 pdfs-app +``` + +Quit the app first, so this starts a new instance. The page then runs without WebKit's isolation, +so use this only for the sign-in. The stored session keeps working without it. Signing in from +your usual network, without a VPN, often avoids the CAPTCHA in the first place. + **Sign-in works but is forgotten after a restart** — no Secret Service provider is running, or its keyring is locked. Install and unlock GNOME Keyring, KWallet or KeePassXC (with Secret Service enabled). `pdfs diagnose` shows `keyring session` as failing in that case. diff --git a/packaging/PKGBUILD b/packaging/PKGBUILD index cc33f9f..fbb2a0f 100644 --- a/packaging/PKGBUILD +++ b/packaging/PKGBUILD @@ -9,7 +9,7 @@ # Build + install: cd packaging && makepkg -fi pkgname=proton-drive-linux -pkgver=3.1.1 +pkgver=3.1.2 pkgrel=1 pkgdesc="Unofficial Proton Drive client for Linux: files-on-demand FUSE mount, CLI, GTK4 app + tray" arch=('x86_64') diff --git a/po/de.po b/po/de.po index 17dcc1b..d7f77f7 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-07 02:39+0200\n" +"POT-Creation-Date: 2026-10-08 10:15+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: German\n" @@ -57,7 +57,7 @@ msgstr "%-d. %b %Y" msgid "%A, %B %-d, %Y, %H:%M" msgstr "%A, %-d. %B %Y, %H:%M" -#: crates/pdfs-gui/src/app/main.rs:747 +#: crates/pdfs-gui/src/app/main.rs:760 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:819 @@ -70,91 +70,91 @@ msgstr "%A, %-d. %B %Y, %H:%M" msgid "My files" msgstr "Meine Dateien" -#: crates/pdfs-gui/src/app/main.rs:750 crates/pdfs-gui/src/app/main.rs:1394 +#: crates/pdfs-gui/src/app/main.rs:763 crates/pdfs-gui/src/app/main.rs:1407 #: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/photos.rs:842 #: crates/pdfs-gui/src/app/pages/places.rs:441 msgid "Photos" msgstr "Fotos" -#: crates/pdfs-gui/src/app/main.rs:755 +#: crates/pdfs-gui/src/app/main.rs:768 #: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:482 msgid "Shared with me" msgstr "Mit mir geteilt" -#: crates/pdfs-gui/src/app/main.rs:760 +#: crates/pdfs-gui/src/app/main.rs:773 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 msgid "Shared by me" msgstr "Von mir geteilt" -#: crates/pdfs-gui/src/app/main.rs:763 +#: crates/pdfs-gui/src/app/main.rs:776 #: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:481 msgid "Computers" msgstr "Computer" -#: crates/pdfs-gui/src/app/main.rs:764 +#: crates/pdfs-gui/src/app/main.rs:777 #: crates/pdfs-gui/src/app/pages/trash.rs:101 msgid "Trash" msgstr "Papierkorb" #. Translators: sidebar entry for the page listing this computer's synced folders. -#: crates/pdfs-gui/src/app/main.rs:766 +#: crates/pdfs-gui/src/app/main.rs:779 msgid "Sync" msgstr "Synchronisierung" #. Translators: a failed action's toast. {what} says what failed, such as #. "Couldn't rename"; {detail} is the reason, usually untranslated text #. from the mount service. -#: crates/pdfs-gui/src/app/main.rs:1083 +#: crates/pdfs-gui/src/app/main.rs:1096 #, rust-format msgid "{what}: {detail}" msgstr "{what}: {detail}" -#: crates/pdfs-gui/src/app/main.rs:1102 +#: crates/pdfs-gui/src/app/main.rs:1115 msgid "You're offline" msgstr "Keine Internetverbindung" -#: crates/pdfs-gui/src/app/main.rs:1103 +#: crates/pdfs-gui/src/app/main.rs:1116 msgid "That's not there any more" msgstr "Das ist nicht mehr vorhanden" -#: crates/pdfs-gui/src/app/main.rs:1104 +#: crates/pdfs-gui/src/app/main.rs:1117 msgid "You don't have access to that" msgstr "Kein Zugriff darauf" -#: crates/pdfs-gui/src/app/main.rs:1105 +#: crates/pdfs-gui/src/app/main.rs:1118 msgid "Something changed this first" msgstr "Etwas anderes hat dies zuerst geändert" -#: crates/pdfs-gui/src/app/main.rs:1106 +#: crates/pdfs-gui/src/app/main.rs:1119 msgid "Your Proton Drive is full" msgstr "Das Proton Drive ist voll" -#: crates/pdfs-gui/src/app/main.rs:1147 crates/pdfs-gui/src/app/main.rs:1376 +#: crates/pdfs-gui/src/app/main.rs:1160 crates/pdfs-gui/src/app/main.rs:1389 #: crates/pdfs-gui/src/app/pages/status.rs:385 msgid "Preferences" msgstr "Einstellungen" -#: crates/pdfs-gui/src/app/main.rs:1148 +#: crates/pdfs-gui/src/app/main.rs:1161 #: crates/pdfs-gui/src/app/widgets/compat.rs:262 msgid "Keyboard Shortcuts" msgstr "Tastenkürzel" -#: crates/pdfs-gui/src/app/main.rs:1150 +#: crates/pdfs-gui/src/app/main.rs:1163 msgid "About Proton Drive for Linux" msgstr "Info zu Proton Drive for Linux" -#: crates/pdfs-gui/src/app/main.rs:1155 +#: crates/pdfs-gui/src/app/main.rs:1168 msgid "Main menu" msgstr "Hauptmenü" -#: crates/pdfs-gui/src/app/main.rs:1211 +#: crates/pdfs-gui/src/app/main.rs:1224 msgid "Proton Drive for Linux" msgstr "Proton Drive for Linux" -#: crates/pdfs-gui/src/app/main.rs:1219 +#: crates/pdfs-gui/src/app/main.rs:1232 msgid "" "Files-on-demand Proton Drive for the Linux desktop.\n" "\n" @@ -166,23 +166,23 @@ msgstr "" "Inoffizieller Client – nicht mit Proton AG verbunden, von Proton AG " "unterstützt oder befürwortet." -#: crates/pdfs-gui/src/app/main.rs:1224 +#: crates/pdfs-gui/src/app/main.rs:1237 msgid "Source Code" msgstr "Quellcode" -#: crates/pdfs-gui/src/app/main.rs:1228 +#: crates/pdfs-gui/src/app/main.rs:1241 msgid "Proton Terms of Service" msgstr "Nutzungsbedingungen von Proton" -#: crates/pdfs-gui/src/app/main.rs:1232 +#: crates/pdfs-gui/src/app/main.rs:1245 msgid "Proton Privacy Policy" msgstr "Datenschutzerklärung von Proton" -#: crates/pdfs-gui/src/app/main.rs:1237 +#: crates/pdfs-gui/src/app/main.rs:1250 msgid "Disclaimer" msgstr "Haftungsausschluss" -#: crates/pdfs-gui/src/app/main.rs:1241 +#: crates/pdfs-gui/src/app/main.rs:1254 msgid "" "This is an unofficial, community-built client. It is not affiliated with, " "endorsed by, or supported by Proton AG.\n" @@ -207,7 +207,7 @@ msgstr "" "bereitgestellt. Daten, deren Verlust nicht hinnehmbar ist, sollten " "zusätzlich gesichert werden." -#: crates/pdfs-gui/src/app/main.rs:1249 +#: crates/pdfs-gui/src/app/main.rs:1262 msgid "" "Places are named with data from GeoNames, licensed under GeoNames, lizenziert unter CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -227,7 +227,7 @@ msgstr "" "href=\"https://www.naturalearthdata.com/\">Natural Earth gezeichnet, die " "gemeinfrei sind." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, licensed under CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -224,7 +224,7 @@ msgstr "" "\">Natural Earth land outlines and country borders, which are in the " "public domain." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, con licencia CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -225,7 +225,7 @@ msgstr "" "fronteras de países de Natural " "Earth, que son de dominio público." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, sous licence CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -226,7 +226,7 @@ msgstr "" "frontières des pays de Natural " "Earth, qui sont dans le domaine public." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, con licenza CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -224,7 +224,7 @@ msgstr "" "confini nazionali di Natural " "Earth, che sono di pubblico dominio." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, gelicentieerd onder CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -225,7 +225,7 @@ msgstr "" "href=\"https://www.naturalearthdata.com/\">Natural Earth, die in het " "publieke domein vallen." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the \n" "Language-Team: LANGUAGE \n" @@ -60,7 +60,7 @@ msgstr "" msgid "%A, %B %-d, %Y, %H:%M" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:747 +#: crates/pdfs-gui/src/app/main.rs:760 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:819 @@ -73,91 +73,91 @@ msgstr "" msgid "My files" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:750 crates/pdfs-gui/src/app/main.rs:1394 +#: crates/pdfs-gui/src/app/main.rs:763 crates/pdfs-gui/src/app/main.rs:1407 #: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/photos.rs:842 #: crates/pdfs-gui/src/app/pages/places.rs:441 msgid "Photos" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:755 +#: crates/pdfs-gui/src/app/main.rs:768 #: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:482 msgid "Shared with me" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:760 +#: crates/pdfs-gui/src/app/main.rs:773 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 msgid "Shared by me" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:763 +#: crates/pdfs-gui/src/app/main.rs:776 #: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:481 msgid "Computers" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:764 +#: crates/pdfs-gui/src/app/main.rs:777 #: crates/pdfs-gui/src/app/pages/trash.rs:101 msgid "Trash" msgstr "" #. Translators: sidebar entry for the page listing this computer's synced folders. -#: crates/pdfs-gui/src/app/main.rs:766 +#: crates/pdfs-gui/src/app/main.rs:779 msgid "Sync" msgstr "" #. Translators: a failed action's toast. {what} says what failed, such as #. "Couldn't rename"; {detail} is the reason, usually untranslated text #. from the mount service. -#: crates/pdfs-gui/src/app/main.rs:1083 +#: crates/pdfs-gui/src/app/main.rs:1096 #, rust-format msgid "{what}: {detail}" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1102 +#: crates/pdfs-gui/src/app/main.rs:1115 msgid "You're offline" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1103 +#: crates/pdfs-gui/src/app/main.rs:1116 msgid "That's not there any more" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1104 +#: crates/pdfs-gui/src/app/main.rs:1117 msgid "You don't have access to that" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1105 +#: crates/pdfs-gui/src/app/main.rs:1118 msgid "Something changed this first" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1106 +#: crates/pdfs-gui/src/app/main.rs:1119 msgid "Your Proton Drive is full" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1147 crates/pdfs-gui/src/app/main.rs:1376 +#: crates/pdfs-gui/src/app/main.rs:1160 crates/pdfs-gui/src/app/main.rs:1389 #: crates/pdfs-gui/src/app/pages/status.rs:385 msgid "Preferences" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1148 +#: crates/pdfs-gui/src/app/main.rs:1161 #: crates/pdfs-gui/src/app/widgets/compat.rs:262 msgid "Keyboard Shortcuts" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1150 +#: crates/pdfs-gui/src/app/main.rs:1163 msgid "About Proton Drive for Linux" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1155 +#: crates/pdfs-gui/src/app/main.rs:1168 msgid "Main menu" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1211 +#: crates/pdfs-gui/src/app/main.rs:1224 msgid "Proton Drive for Linux" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1219 +#: crates/pdfs-gui/src/app/main.rs:1232 msgid "" "Files-on-demand Proton Drive for the Linux desktop.\n" "\n" @@ -165,23 +165,23 @@ msgid "" "AG." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1224 +#: crates/pdfs-gui/src/app/main.rs:1237 msgid "Source Code" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1228 +#: crates/pdfs-gui/src/app/main.rs:1241 msgid "Proton Terms of Service" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1232 +#: crates/pdfs-gui/src/app/main.rs:1245 msgid "Proton Privacy Policy" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1237 +#: crates/pdfs-gui/src/app/main.rs:1250 msgid "Disclaimer" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1241 +#: crates/pdfs-gui/src/app/main.rs:1254 msgid "" "This is an unofficial, community-built client. It is not affiliated with, " "endorsed by, or supported by Proton AG.\n" @@ -194,21 +194,21 @@ msgid "" "of any data you cannot afford to lose." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1249 +#: crates/pdfs-gui/src/app/main.rs:1262 msgid "" "Places are named with data from GeoNames, licensed under CC BY 4.0." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " "public domain." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames udostępnionych na licencji CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -228,7 +228,7 @@ msgstr "" "www.naturalearthdata.com/\">Natural Earth, które należą do domeny " "publicznej." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, licenciados sob CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1257 +#: crates/pdfs-gui/src/app/main.rs:1270 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -224,7 +224,7 @@ msgstr "" "fronteiras de países do Natural Earth, que são de domínio público." -#: crates/pdfs-gui/src/app/main.rs:1265 +#: crates/pdfs-gui/src/app/main.rs:1278 msgid "" "The street map, when turned on, comes from OpenFreeMap in the