From 87361a45738c2ba5c45e75113ca5f5c0560ab99f Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 11 Oct 2026 19:26:39 +0200 Subject: [PATCH] fix(core,fuse,gui): a photo upload the SDK refuses no longer says offline Every SDK error that was not an API reply was classified as Offline. Only a transport failure is. An upload on an account without a photo library fails in the SDK before any request, and the app said "You're offline" while every other call worked (issue #38). Classify by variant: Transport is Offline, InvalidOperation is Invalid, Serialization is Remote, Crypto and KeysLocked are Internal. The daemon logs a failed photo upload, and the Photos page greys out Upload when the account has no photo library. docs/BUGS.md B205. --- crates/pdfs-core/src/error.rs | 54 +++++++++++++++++++------ crates/pdfs-fuse/src/control.rs | 3 ++ crates/pdfs-gui/src/app/main.rs | 1 + crates/pdfs-gui/src/app/pages/photos.rs | 9 +++++ crates/pdfs-gui/src/app/pages/status.rs | 4 +- docs/BUGS.md | 29 +++++++++++++ docs/CHANGELOG.md | 6 +++ 7 files changed, 93 insertions(+), 13 deletions(-) diff --git a/crates/pdfs-core/src/error.rs b/crates/pdfs-core/src/error.rs index 005c446..defb445 100644 --- a/crates/pdfs-core/src/error.rs +++ b/crates/pdfs-core/src/error.rs @@ -154,17 +154,17 @@ impl CoreError { /// through the boxes the drain deals in, where the concrete type survives /// but the static one does not. /// - /// A failure that is not an API response at all — a transport error, a - /// timeout, a TLS failure — is what being offline looks like from here, so - /// it classifies as [`ErrorKind::Offline`] rather than - /// [`ErrorKind::Remote`]: the request never reached Proton, and telling the - /// user their connection is down beats telling them the server refused. + /// A transport error, a timeout or a TLS failure is what being offline + /// looks like from here, so it classifies as [`ErrorKind::Offline`] rather + /// than [`ErrorKind::Remote`]: the request never reached Proton, and + /// 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`]). pub fn from_api(e: &(dyn std::error::Error + 'static), what: &str) -> Self { let kind = match e.downcast_ref::() { - Some(proton_sdk::ProtonError::Api(api)) => api_kind(api.code), - // A `ProtonError` that is not an `Api` variant never reached the - // API; so does anything that is not a `ProtonError` at all. - _ => ErrorKind::Offline, + 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}")) } @@ -214,15 +214,33 @@ impl From for CoreError { | Error::Locked | Error::WrongMailboxPassword | Error::HumanVerificationRequired(_) => ErrorKind::Denied, - Error::Proton(proton_sdk::ProtonError::Api(api)) => api_kind(api.code), - // A `ProtonError` that never reached the API is a transport failure. - Error::Proton(_) => ErrorKind::Offline, + Error::Proton(proton) => proton_kind(proton), _ => ErrorKind::Internal, }; Self::new(kind, e.to_string()) } } +/// What a failed SDK call means to a front-end. +/// +/// Only a transport failure is the connection. The rest happened on this +/// machine, and calling them "offline" sent the user to check a network that +/// worked (issue #38: a photo upload on an account without a photo library +/// read "You're offline"). +fn proton_kind(e: &proton_sdk::ProtonError) -> ErrorKind { + use proton_sdk::ProtonError as P; + match e { + P::Api(api) => api_kind(api.code), + P::Transport(_) => ErrorKind::Offline, + // Proton answered, but not with anything this client can read. + P::Serialization(_) => ErrorKind::Remote, + // The SDK refused the call before sending it, such as an upload to a + // photo library the account does not have. + P::InvalidOperation(_) => ErrorKind::Invalid, + P::Crypto(_) | P::KeysLocked => ErrorKind::Internal, + } +} + /// What an API response code means to a front-end. /// /// The grouping is by *what the user can do about it*, which is what @@ -367,6 +385,18 @@ mod tests { assert_eq!(CoreError::from_api(&io, "list").kind, ErrorKind::Offline); } + /// Issue #38: the SDK refused a photo upload before sending it, and the + /// app said "You're offline" while every other call went through. + #[test] + fn a_call_the_sdk_refuses_to_send_is_not_offline() { + let refused = ProtonError::invalid_operation("account has no photos volume"); + let e = CoreError::from_api(&refused, "upload photo failed"); + assert_eq!(e.kind, ErrorKind::Invalid); + assert!(e.message.ends_with("account has no photos volume")); + let e: CoreError = Error::Proton(ProtonError::KeysLocked).into(); + assert_ne!(e.kind, ErrorKind::Offline); + } + /// `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/crates/pdfs-fuse/src/control.rs b/crates/pdfs-fuse/src/control.rs index 86646fd..f112e14 100644 --- a/crates/pdfs-fuse/src/control.rs +++ b/crates/pdfs-fuse/src/control.rs @@ -940,6 +940,9 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U } } Err(e) => { + // The reply goes to the app's toast only; without + // this the journal shows a failure and no reason. + warn!(error = %e, "photo upload failed"); CtlResponse::error(CoreError::from_api(&e, "upload photo failed")) } } diff --git a/crates/pdfs-gui/src/app/main.rs b/crates/pdfs-gui/src/app/main.rs index f80ef8c..8cb84b4 100644 --- a/crates/pdfs-gui/src/app/main.rs +++ b/crates/pdfs-gui/src/app/main.rs @@ -538,6 +538,7 @@ fn build_window(app: &adw::Application) { reload_pending: Cell::new(false), loading: Cell::new(false), has_more: Cell::new(false), + library: Cell::new(true), page_waiters: RefCell::new(Vec::new()), width: Cell::new(0), photo_tex: RefCell::new(HashMap::new()), diff --git a/crates/pdfs-gui/src/app/pages/photos.rs b/crates/pdfs-gui/src/app/pages/photos.rs index bfd4b33..94105b2 100644 --- a/crates/pdfs-gui/src/app/pages/photos.rs +++ b/crates/pdfs-gui/src/app/pages/photos.rs @@ -140,6 +140,9 @@ pub(crate) struct GalleryState { pub(crate) loading: Cell, /// Whether the last page came back full, so there may be more to load. pub(crate) has_more: Cell, + /// Whether the account has a photo library. Until the daemon says it has + /// none, assume it does, so Upload is not greyed out while the page loads. + pub(crate) library: Cell, /// Run once the page in flight has landed in [`Self::model`] — how the /// lightbox steps past the last loaded photo. pub(crate) page_waiters: RefCell>>, @@ -3341,6 +3344,12 @@ pub(crate) fn load_gallery(ui: &Rc, append: bool) { items, counts, })) => { + // Nothing can go into a library the account does not have; the + // SDK refuses the upload before sending it (issue #38). + ui.gallery.library.set(available); + ui.gallery + .upload + .set_sensitive(available && *ui.mounted.borrow()); if !available { gallery_status( &ui, diff --git a/crates/pdfs-gui/src/app/pages/status.rs b/crates/pdfs-gui/src/app/pages/status.rs index 19e674e..3930802 100644 --- a/crates/pdfs-gui/src/app/pages/status.rs +++ b/crates/pdfs-gui/src/app/pages/status.rs @@ -958,7 +958,9 @@ fn quota_display(max_space: i64, used_space: i64) -> (f64, String) { pub(crate) fn set_mounted(ui: &Rc, mounted: bool) { *ui.mounted.borrow_mut() = mounted; sync_mounted_actions(ui, mounted); - ui.gallery.upload.set_sensitive(mounted); + ui.gallery + .upload + .set_sensitive(mounted && ui.gallery.library.get()); ui.details.details.pin_row.set_sensitive(mounted); ui.details.details.rename_button.set_sensitive(mounted); ui.details.details.trash_button.set_sensitive(mounted); diff --git a/docs/BUGS.md b/docs/BUGS.md index f18e3ea..40fd438 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -100,6 +100,7 @@ a live account since. | Entry | Title | |---|---| +| [B205](#b205--a-photo-upload-the-sdk-refuses-says-youre-offline) | A photo upload the SDK refuses says "You're offline" | | [B204](#b204--a-write-over-a-partial-edit-whose-base-was-never-read-fails-with-eio) | A write over a partial edit whose base was never read fails with `EIO` | | [B203](#b203--pdfs-unlock-leaves-a-running-daemon-a-spent-refresh-token) | `pdfs unlock` leaves a running daemon a spent refresh token | | [B202](#b202--a-refused-session-shows-as-offline-forever-and-nothing-says-to-sign-in-again) | A refused session shows as offline forever, and nothing says to sign in again | @@ -307,6 +308,34 @@ a live account since. Newest first. +### B205 — A photo upload the SDK refuses says "You're offline" + +**Status:** Fixed, not verified live +**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. +**Where:** `CoreError::from_api` and `From for CoreError` in `crates/pdfs-core/src/error.rs`; +the `UploadPhoto` handler in `crates/pdfs-fuse/src/control.rs`; the Photos page in +`crates/pdfs-gui/src/app/pages/photos.rs` + +**Repro:** On an account without a photo library, open Photos and upload an image. + +**Cause.** Every `ProtonError` other than `Api` was classified as `ErrorKind::Offline`, on the +reasoning that it never reached Proton. That holds for `Transport` only. `InvalidOperation`, +`Crypto`, `KeysLocked` and `Serialization` happen on this machine or after Proton answered. A +failure that fast is most likely the SDK's "account has no photos volume", which it returns from +its cache without sending a request. The app turns `Offline` into "You're offline", and the +daemon only sent the error back, so the journal did not say what it was. + +**Fix.** `proton_kind` classifies by variant: `Transport` is `Offline`, `InvalidOperation` is +`Invalid`, `Serialization` is `Remote`, and `Crypto` and `KeysLocked` are `Internal`. The app then +shows "Couldn't upload photo" with the daemon's reason. The daemon logs a failed photo upload. The +Photos page greys out Upload when the daemon reports no photo library. + +**Test:** `error::tests::a_call_the_sdk_refuses_to_send_is_not_offline`. + +--- + ### B204 — A write over a partial edit whose base was never read fails with `EIO` **Status:** Fixed, not verified live diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3f43b2a..4993d88 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,12 @@ scratch (user data in `staging/` and `recovery/` is never touched by this). ## [Unreleased] +### Fixed +- **A failed photo upload said "You're offline" while everything else worked.** Only a failed + connection now reads as offline. Other failures show what went wrong, and Upload is greyed out + on an account without a photo library. + ([#38](https://github.com/narrrl/proton-drive-linux/issues/38)) + ## [3.2.1] — 2026-10-11 Schema: **42**, unchanged.