Compare commits

..
Author SHA1 Message Date
nvrl 160626b29d chore(release): 3.3.0 2026-10-11 21:53:52 +02:00
nvrl f1f1f8ad4d ci(release): build the Nix flake at every release tag
A flake input pinned to vX.Y.Z is the Nix release, so the release
workflow now builds the flake at the tag and checks its version before
the GitHub release is published. The install guide shows how to pin the
input to a release tag, and the release checklist names the files that
carry the version.
2026-10-11 21:53:52 +02:00
nvrl bb28a18f49 Merge branch 'feat/issue-37-nix' 2026-10-11 21:48:28 +02:00
nvrl a3c0c9ee9b ci(release): take the release notes from the changelog
The release workflow created each GitHub release without a body, so 3.1.2,
3.2.0 and 3.2.1 shipped with empty notes. It now copies the version's
section of docs/CHANGELOG.md into the release, and fails when the section
is missing.
2026-10-11 21:24:39 +02:00
nvrl 91fa1614ed fix(core,fuse,cli,gui): say when an upgrade left the old daemon running
A package upgrade replaces the binaries but does not restart the user
service, so the daemon keeps the old release and its bugs until the next
login. On 3.2.0 the reporter of #33 still saw "Couldn't sync Documents:
read .../snapshots/37: Permission denied", which only a daemon older than
3.2.0 can produce: since then the scan leaves an unreadable subfolder out
and only an unreadable sync root fails the pass (B206).

The daemon now reports its release in Status; one from before the field
omits it, which reads as older. When the daemon is older than the
front-end, the tray says "Update not finished - restart Proton Drive" and
offers a restart, the sync card shows "Restart to finish updating" with a
Restart button, and pdfs status and pdfs diagnose print the systemctl
command. A newer daemon is not flagged, since then the front-end is the
stale one.

Refs #33
2026-10-11 21:20:51 +02:00
nvrl 87361a4573 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.
2026-10-11 19:26:53 +02:00
Nils Pukropp 6d0b6ad553 chore(aur): update packages to 3.2.1 2026-10-11 17:24:37 +00:00
nvrl 170c3f01ff chore(release): 3.2.1 2026-10-11 19:19:36 +02:00
nvrl 976ad2aa04 docs(changelog): list the issue #35 fixes under Unreleased
The CPU spin while offline, the expired session shown as offline, the
lost refresh tokens and the keyring-refused rotation were fixed without
a changelog entry.
2026-10-11 18:56:55 +02:00
nvrl b73cd087aa test(core): time the B12 index write as the best of five runs
A single timed run lost the CPU to the rest of the suite running
alongside it and read as the full-scan regression (171 ms against
701 ms). The fastest of five runs still catches the 6x scaling of the
old path.
2026-10-11 18:56:55 +02:00
nvrl e402981708 fix(packaging): ship the Adwaita icons with the Nix package
GTK ships only a few stock icons itself. On a NixOS session without
Adwaita installed, such as Hyprland, the Photos and Computers sidebar
entries and about twenty other icons showed as missing (#37).
adwaita-icon-theme in buildInputs puts it on the wrapper's XDG_DATA_DIRS.
2026-10-11 18:55:00 +02:00
Claude 453c439b74 fix(packaging): give the Nix package TLS for WebKitGTK
On NixOS the CAPTCHA window failed with "TLS support is not available":
WebKitGTK's GIO had no TLS backend. With glib-networking in buildInputs,
wrapGAppsHook4 puts its GIO modules on GIO_EXTRA_MODULES in every
wrapper.

Refs #37

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuyxByBpK3fSWtyPkugeHz
2026-10-10 08:23:21 +00:00
Claude ceba0f5849 feat(packaging): add a Nix flake and NixOS module
Builds the four binaries with rustPlatform.buildRustPackage from
Cargo.lock, installs the desktop entries, icon, user service and
translations as the other packages do, and wraps the binaries with
exiftool and ffmpeg on PATH. The NixOS module installs the package,
pulls proton-drive.service into the graphical session with the
fusermount3 wrapper on its PATH, and enables FUSE and GNOME Keyring.

A Nix workflow builds the flake when its recipe, the lock files or the
code change.

Refs #37

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuyxByBpK3fSWtyPkugeHz
2026-10-09 22:07:20 +00:00
41 changed files with 4534 additions and 3611 deletions

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
name: Nix
# Builds the flake (docs/INSTALL.md#nixos-flake) whenever its recipe, the lock
# files or the code change, so a NixOS user's `nix build` keeps working. Run it
# from the Actions tab for any branch to get a build to try.
on:
push:
paths:
- 'flake.nix'
- 'flake.lock'
- 'packaging/nix/**'
- 'Cargo.lock'
- 'Cargo.toml'
- 'crates/**'
- 'po/**'
- '.github/workflows/nix.yml'
pull_request:
paths:
- 'flake.nix'
- 'flake.lock'
- 'packaging/nix/**'
- 'Cargo.lock'
- '.github/workflows/nix.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: nix-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: cachix/install-nix-action@v31
- name: Check the flake
run: nix flake check --print-build-logs --no-build
- name: Build the package
run: nix build --print-build-logs .#proton-drive-linux
- name: Smoke test
run: |
./result/bin/pdfs --version
for bin in pdfs-app pdfs-tray pdfs-prompt; do test -x "result/bin/$bin"; done
grep -q "ExecStart=$(readlink -f result)/bin/pdfs" result/lib/systemd/user/proton-drive.service
ls result/share/locale/*/LC_MESSAGES/pdfs.mo
+40 -1
View File
@@ -230,21 +230,60 @@ jobs:
*.rpm
if-no-files-found: error
# The flake is the Nix release: a tag is what `github:narrrl/proton-drive-linux/vX.Y.Z`
# pins, so it has to build before the release is announced.
nix:
name: Build the Nix package
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: cachix/install-nix-action@v31
- name: Build the flake
run: nix build --print-build-logs .#proton-drive-linux
- name: Check the version
run: |
built="$(./result/bin/pdfs --version)"
echo "$built"
case "$GITHUB_REF" in
refs/tags/v*) grep -qF "${GITHUB_REF_NAME#v}" <<< "$built" ;;
esac
publish:
name: Create GitHub Release
needs: [checks, build-release]
needs: [checks, build-release, nix]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Download packages
uses: actions/download-artifact@v8
with:
name: proton-drive-linux-builds
# The release notes are this version's section of the changelog, from its
# heading up to the next one.
- name: Extract release notes
run: |
version="${GITHUB_REF_NAME#v}"
awk -v heading="## [$version]" \
'index($0, heading) == 1 { found = 1; print; next }
found && /^## \[/ { exit }
found' docs/CHANGELOG.md > release-notes.md
if [ ! -s release-notes.md ]; then
echo "::error::docs/CHANGELOG.md has no section for $version"
exit 1
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
body_path: release-notes.md
files: |
*.deb
*.tar.gz
+2
View File
@@ -3,3 +3,5 @@
*.pdb
.env
/rpmbuild
/result
/result-*
Generated
+4 -4
View File
@@ -3474,7 +3474,7 @@ checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
[[package]]
name = "pdfs-cli"
version = "3.2.0"
version = "3.3.0"
dependencies = [
"anyhow",
"clap",
@@ -3490,7 +3490,7 @@ dependencies = [
[[package]]
name = "pdfs-core"
version = "3.2.0"
version = "3.3.0"
dependencies = [
"async-trait",
"base64 0.23.1",
@@ -3516,7 +3516,7 @@ dependencies = [
[[package]]
name = "pdfs-fuse"
version = "3.2.0"
version = "3.3.0"
dependencies = [
"async-trait",
"base64 0.23.1",
@@ -3538,7 +3538,7 @@ dependencies = [
[[package]]
name = "pdfs-gui"
version = "3.2.0"
version = "3.3.0"
dependencies = [
"anyhow",
"async-channel",
+1 -1
View File
@@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/pdfs-core", "crates/pdfs-fuse", "crates/pdfs-cli", "crates/pdfs-gui"]
[workspace.package]
version = "3.2.0"
version = "3.3.0"
edition = "2024"
license = "MIT"
authors = ["Nils Pukropp <contact@narl.io>"]
+1
View File
@@ -76,6 +76,7 @@ scriptable CLI.
| Debian, Ubuntu 24.04 or newer | `.deb` from the [latest release](https://github.com/narrrl/proton-drive-linux/releases/latest) |
| Fedora 44 or newer | `.rpm` from the [latest release](https://github.com/narrrl/proton-drive-linux/releases/latest) |
| Arch Linux | [`proton-drive-for-linux`](https://aur.archlinux.org/packages/proton-drive-for-linux) (also `-bin` and `-git`) in the AUR |
| NixOS | The flake in this repository: see [Install → NixOS](docs/INSTALL.md#nixos-flake) |
| Anything else | [Build from source](docs/INSTALL.md#build-from-source) |
```bash
+27 -1
View File
@@ -15,7 +15,7 @@ use pdfs_core::config::AppDirs;
use pdfs_core::control::{
ConflictKeep, ErrorKind, MountKind, RefreshScope, Request as CtlRequest,
Response as CtlResponse, RestoreItem as CtlRestoreItem, ShareEntryKind, SyncIssue, SyncPhase,
pending_summary,
daemon_outdated, pending_summary,
};
use pdfs_core::db::Db;
use pdfs_core::service;
@@ -2026,6 +2026,7 @@ fn cmd_status() -> Result<()> {
paused_until,
session_expired,
tokens_unsaved,
version,
..
}) => {
let state = match (online, paused) {
@@ -2067,6 +2068,9 @@ fn cmd_status() -> Result<()> {
"Keyring refuses the refreshed sign-in; unlock it, or a reboot needs `pdfs login`"
);
}
if daemon_outdated(&version) {
println!("Daemon {}", outdated_daemon_note(&version));
}
}
Ok(other) => println!("Mount: unexpected response {other:?}"),
Err(_) => println!("Mount: not running."),
@@ -2074,6 +2078,20 @@ fn cmd_status() -> Result<()> {
Ok(())
}
/// What `pdfs status` and `pdfs diagnose` say about a daemon still running
/// an older release than this CLI.
fn outdated_daemon_note(version: &str) -> String {
let running = if version.is_empty() {
"an older release".to_string()
} else {
version.to_string()
};
format!(
"still runs {running}, this is {}; run `systemctl --user restart proton-drive` to finish the update",
env!("CARGO_PKG_VERSION"),
)
}
fn cmd_locations() -> Result<()> {
let response = control_request(CtlRequest::ListLocations)?;
if emit_json(&response)? {
@@ -3444,9 +3462,17 @@ fn cmd_diagnose() -> Result<()> {
tokens_unsaved,
pending_uploads,
pending_changes,
version,
..
}) => {
report.finding(DiagnoseLevel::Ok, " daemon responding", "");
if daemon_outdated(&version) {
report.finding(
DiagnoseLevel::Warn,
" daemon version",
outdated_daemon_note(&version),
);
}
let live_type = mounted_fs_type(Path::new(&mountpoint));
let is_fuse = live_type
.as_deref()
+47
View File
@@ -1850,6 +1850,27 @@ pub fn pending_summary(uploads: u64, changes: u64) -> Option<String> {
}
}
/// Whether the daemon that reported `version` in a [`Response::Status`] is an
/// older release than this build. Installing a package replaces the binaries
/// but leaves the running user service on the old one, which keeps its bugs
/// until it restarts, while the app and the CLI already describe the new
/// release (#33). Empty means a daemon from before the field, so older too.
///
/// Only older counts. A newer daemon means this front-end is the one still
/// running from before the upgrade, and restarting the service would not help.
pub fn daemon_outdated(version: &str) -> bool {
fn parse(v: &str) -> Option<(u64, u64, u64)> {
let mut parts = v.split('.').map(|p| p.parse().ok());
let parsed = (parts.next()??, parts.next()??, parts.next()??);
parts.next().is_none().then_some(parsed)
}
match (parse(version), parse(env!("CARGO_PKG_VERSION"))) {
(Some(running), Some(this)) => running < this,
(None, _) => version.is_empty(),
(Some(_), None) => false,
}
}
/// One worker thread in a [`Diagnostics`] report.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct WorkerState {
@@ -1969,6 +1990,11 @@ pub enum Response {
/// out.
#[serde(default)]
tokens_unsaved: bool,
/// The release of the daemon that answered. A package upgrade does not
/// restart the user service, so it can be older than the front-end
/// asking; daemons before 3.3.0 omit it, which reads as empty.
#[serde(default)]
version: String,
},
/// A human-readable success message.
Ok { message: String },
@@ -2612,6 +2638,27 @@ mod tests {
);
}
/// #33: a daemon left running over an upgrade is named as older, also when
/// it predates the version field, but a newer one is not, since restarting
/// the service cannot bring it closer to this build.
#[test]
fn only_an_older_daemon_is_outdated() {
assert!(!daemon_outdated(env!("CARGO_PKG_VERSION")));
assert!(daemon_outdated(""));
assert!(daemon_outdated("0.9.9"));
assert!(!daemon_outdated("999.0.0"));
assert!(!daemon_outdated("not-a-version"));
let old: Response = serde_json::from_str(
r#"{"Status":{"username":"u","mountpoint":"/m","pinned":0,
"used":0,"budget":0,"pins":[]}}"#,
)
.unwrap();
let Response::Status { version, .. } = old else {
panic!("not a status: {old:?}");
};
assert!(daemon_outdated(&version));
}
/// The mutation requests must survive a line-delimited JSON round-trip, since
/// that is exactly how they cross the control socket.
#[test]
+10 -3
View File
@@ -1689,9 +1689,16 @@ fn writing_nodes_does_not_scale_with_the_size_of_the_index() {
.collect();
db.upsert_nodes(&batch).unwrap();
let t = Instant::now();
db.upsert_nodes(&batch).unwrap();
t.elapsed()
// The fastest of a few runs: the rest of the suite runs alongside, and
// one run that lost the CPU to it read as the regression.
(0..5)
.map(|_| {
let t = Instant::now();
db.upsert_nodes(&batch).unwrap();
t.elapsed()
})
.min()
.unwrap()
}
let small = cost_against_index_of(200);
+42 -12
View File
@@ -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::<proton_sdk::ProtonError>() {
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<Error> 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]
+4
View File
@@ -336,6 +336,7 @@ pub(crate) fn status_response(core: &Core, username: &str, mountpoint: &Path) ->
online: core.online.load(Ordering::Relaxed),
session_expired: core.session_expired(),
tokens_unsaved: pdfs_core::auth::tokens_unsaved(),
version: env!("CARGO_PKG_VERSION").to_string(),
pending_uploads: queued.uploads.max(0) as u64 + landing,
pending_changes: queued.changes.max(0) as u64,
}
@@ -940,6 +941,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"))
}
}
+1
View File
@@ -329,6 +329,7 @@ mod tests {
online: true,
session_expired: false,
tokens_unsaved: false,
version: "3.3.0".into(),
pending_uploads: 1,
pending_changes: 0,
};
+2
View File
@@ -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()),
@@ -580,6 +581,7 @@ fn build_window(app: &adw::Application) {
pause: locations_widgets.pause.clone(),
paused: Cell::new(false),
sign_in: locations_widgets.card_sign_in.clone(),
restart: locations_widgets.card_restart.clone(),
},
queue: QueueState {
group: locations_widgets.queue_group.clone(),
+20 -2
View File
@@ -48,6 +48,8 @@ pub(crate) struct SyncCard {
pub(crate) paused: Cell<bool>,
/// Shown only while Proton refuses the session; opens the sign-in page.
pub(crate) sign_in: gtk4::Button,
/// Shown only while the daemon is older than the app; restarts it.
pub(crate) restart: gtk4::Button,
}
/// What a folder row's controls are built from. The subtitle and progress bar
@@ -111,6 +113,7 @@ pub(crate) struct LocationsWidgets {
pub(crate) card_title: gtk4::Label,
pub(crate) card_detail: gtk4::Label,
pub(crate) card_sign_in: gtk4::Button,
pub(crate) card_restart: gtk4::Button,
pub(crate) pause: adw::SplitButton,
pub(crate) queue_group: adw::PreferencesGroup,
pub(crate) retry_all: gtk4::Button,
@@ -137,7 +140,7 @@ pub(crate) fn build_locations_page(history: &gtk4::Widget) -> (gtk4::Widget, Loc
.build();
add_folder.add_css_class("flat");
let transfers_group = build_transfers_group();
let (card, card_icon, card_title, card_detail, card_sign_in) = build_sync_card();
let (card, card_icon, card_title, card_detail, card_sign_in, card_restart) = build_sync_card();
let pause = build_pause_button();
let (queue_group, retry_all) = build_queue_group();
let conflicts_group = adw::PreferencesGroup::builder()
@@ -257,6 +260,7 @@ pub(crate) fn build_locations_page(history: &gtk4::Widget) -> (gtk4::Widget, Loc
card_title,
card_detail,
card_sign_in,
card_restart,
pause,
queue_group,
retry_all,
@@ -299,6 +303,11 @@ pub(crate) fn wire_locations(ui: &Rc<Ui>, retry: &gtk4::Button, add_folder: &gtk
.card
.sign_in
.connect_clicked(move |_| show_login(&ui_sign_in));
let ui_restart = ui.clone();
ui.locations
.card
.restart
.connect_clicked(move |_| restart_service_then(&ui_restart, |_| {}));
let ui_retry = ui.clone();
ui.locations
.queue
@@ -396,6 +405,7 @@ fn build_sync_card() -> (
gtk4::Label,
gtk4::Label,
gtk4::Button,
gtk4::Button,
) {
let icon = gtk4::Image::builder()
.icon_name("pdfs-sync-symbolic")
@@ -430,10 +440,18 @@ fn build_sync_card() -> (
.build();
sign_in.add_css_class("pill");
sign_in.add_css_class("suggested-action");
let restart = gtk4::Button::builder()
.label(gettext("Restart"))
.valign(gtk4::Align::Center)
.visible(false)
.build();
restart.add_css_class("pill");
restart.add_css_class("suggested-action");
card.append(&icon);
card.append(&text);
card.append(&sign_in);
(card, icon, title, detail, sign_in)
card.append(&restart);
(card, icon, title, detail, sign_in, restart)
}
/// Pause/Resume, with timed pauses in its menu.
+9
View File
@@ -140,6 +140,9 @@ pub(crate) struct GalleryState {
pub(crate) loading: Cell<bool>,
/// Whether the last page came back full, so there may be more to load.
pub(crate) has_more: Cell<bool>,
/// 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<bool>,
/// 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<Vec<Box<dyn FnOnce()>>>,
@@ -3341,6 +3344,12 @@ pub(crate) fn load_gallery(ui: &Rc<Ui>, 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,
+28 -1
View File
@@ -958,7 +958,9 @@ fn quota_display(max_space: i64, used_space: i64) -> (f64, String) {
pub(crate) fn set_mounted(ui: &Rc<Ui>, 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);
@@ -1230,6 +1232,7 @@ pub(crate) fn paint_status(ui: &Rc<Ui>, status: Response) {
paused_until,
session_expired,
tokens_unsaved,
version,
..
} = status
else {
@@ -1244,6 +1247,8 @@ pub(crate) fn paint_status(ui: &Rc<Ui>, status: Response) {
SyncState::SessionExpired { queued }
} else if tokens_unsaved {
SyncState::TokensUnsaved
} else if pdfs_core::control::daemon_outdated(&version) {
SyncState::Outdated { running: version }
} else if paused {
SyncState::Paused {
until: paused_until,
@@ -1315,6 +1320,12 @@ pub(crate) enum SyncState {
/// The keyring refuses the session's refreshed tokens. Sync goes on, but
/// a reboot before the keyring takes them signs the user out.
TokensUnsaved,
/// The daemon is an older release than this window: an upgrade replaced
/// the files but left the service running, with the old release's bugs,
/// until it restarts (#33). `running` is empty when it is too old to say.
Outdated {
running: String,
},
/// The user paused syncing; nothing goes up until it resumes. Outranks
/// everything else, because it is the reason for everything else.
Paused {
@@ -1350,6 +1361,7 @@ fn paint_sync_status(ui: &Rc<Ui>, state: SyncState) {
let paused = matches!(state, SyncState::Paused { .. });
let connected = !matches!(state, SyncState::Disconnected | SyncState::NotResponding);
let expired = matches!(state, SyncState::SessionExpired { .. });
let outdated = matches!(state, SyncState::Outdated { .. });
let (icon, class, title, detail) = match state {
SyncState::SessionExpired { queued } => (
"dialog-password-symbolic",
@@ -1372,6 +1384,20 @@ fn paint_sync_status(ui: &Rc<Ui>, state: SyncState) {
"Unlock your keyring, or the next reboot signs you out",
)),
),
SyncState::Outdated { running } => (
"dialog-warning-symbolic",
Some("warning"),
gettext("Restart to finish updating"),
Some(if running.is_empty() {
gettext("An older version of Proton Drive is still running")
} else {
// Translators: {version} is a release number such as "3.2.0".
gettext_f(
"Proton Drive {version} is still running",
&[("version", &running)],
)
}),
),
SyncState::Paused { until, queued } => (
"media-playback-pause-symbolic",
Some("warning"),
@@ -1443,6 +1469,7 @@ fn paint_sync_status(ui: &Rc<Ui>, state: SyncState) {
image.add_css_class(class);
}
ui.locations.card.sign_in.set_visible(expired);
ui.locations.card.restart.set_visible(outdated);
ui.status.status_title.set_label(&title);
ui.status.status_detail.set_visible(detail.is_some());
ui.status
+38 -3
View File
@@ -22,7 +22,8 @@ use ksni::{MenuItem, Status, ToolTip, Tray};
use pdfs_core::auth;
use pdfs_core::config::AppDirs;
use pdfs_core::control::{
Event, Feed, JobItem, Request, Response, TransferDirection, TransferItem, follow, send,
Event, Feed, JobItem, Request, Response, TransferDirection, TransferItem, daemon_outdated,
follow, send,
};
use pdfs_core::service;
@@ -86,6 +87,9 @@ struct DriveState {
paused: bool,
/// Queued changes that keep failing, for the "N issues" item.
failing: u64,
/// Whether the daemon is an older release than the tray, left running by
/// an upgrade; offers the restart that finishes it.
outdated: bool,
}
/// Summarise a work snapshot into one menu line, or empty when idle. A single
@@ -166,6 +170,7 @@ fn poll_state(socket: &Path, default_mountpoint: &Path) -> DriveState {
sync: String::new(),
paused: false,
failing: 0,
outdated: false,
},
// No daemon: describe login state instead so the menu is still useful.
Err(_) => {
@@ -202,6 +207,7 @@ fn poll_state(socket: &Path, default_mountpoint: &Path) -> DriveState {
sync: String::new(),
paused: false,
failing: 0,
outdated: false,
}
}
}
@@ -218,6 +224,7 @@ fn unexpected_reply(default_mountpoint: &Path) -> DriveState {
sync: String::new(),
paused: false,
failing: 0,
outdated: false,
}
}
@@ -239,6 +246,7 @@ fn status_state(
paused,
session_expired,
tokens_unsaved,
version,
..
} = status
else {
@@ -246,9 +254,10 @@ fn status_state(
};
let (pinned, online, failing_ops, paused) = (*pinned, *online, *failing_ops, *paused);
let queued = i18n::pending_summary(*pending_uploads, *pending_changes);
let outdated = daemon_outdated(version);
let phase = if *session_expired {
Phase::SessionExpired
} else if *tokens_unsaved {
} else if *tokens_unsaved || outdated {
Phase::Attention
} else {
phase_of(
@@ -265,6 +274,9 @@ fn status_state(
_ if *session_expired => gettext("Session expired — sign in again"),
// Sync goes on meanwhile, but a reboot ends it until the next login.
_ if *tokens_unsaved => gettext("Sign-in not saved — unlock your keyring"),
// An upgrade replaced the files but not the running service, which
// keeps the old release's bugs until it restarts.
_ if outdated => gettext("Update not finished — restart Proton Drive"),
_ if paused => gettext("Sync paused"),
_ if failing_ops > 0 => ngettext_f(
"{n} change needs attention",
@@ -302,6 +314,7 @@ fn status_state(
mountpoint: PathBuf::from(mountpoint),
sync: sync_line(items, jobs),
failing: failing_ops,
outdated,
}
}
@@ -445,6 +458,13 @@ impl Tray for DriveTray {
_ => items.push(item(&gettext("Open Proton Drive"), |_| open_manager(&[]))),
}
if self.state.outdated {
items.push(item(&gettext("Restart Proton Drive"), |this| {
service::restart();
this.state.line = gettext("Connecting…");
}));
}
if self.state.mounted {
items.push(
StandardItem {
@@ -626,9 +646,14 @@ mod tests {
}
fn status(extra: &str) -> Response {
status_from(env!("CARGO_PKG_VERSION"), extra)
}
fn status_from(version: &str, extra: &str) -> Response {
serde_json::from_str(&format!(
r#"{{"Status":{{"username":"u","mountpoint":"/m","pinned":0,"used":0,"budget":0,
"pins":[],"online":false,"pending_uploads":3,"failing_ops":2,"paused":true{extra}}}}}"#
"pins":[],"online":false,"pending_uploads":3,"failing_ops":2,"paused":true,
"version":"{version}"{extra}}}}}"#
))
.unwrap()
}
@@ -660,6 +685,16 @@ mod tests {
assert_eq!(state.line, "Sign-in not saved — unlock your keyring");
}
/// #33: a daemon an upgrade left running on the old release is named,
/// above a pause, and the menu offers the restart that finishes it.
#[test]
fn a_daemon_older_than_the_tray_needs_a_restart() {
let state = status_state(&status_from("0.1.0", ""), &[], &[], Path::new("/m"));
assert_eq!(state.phase, Phase::Attention);
assert_eq!(state.line, "Update not finished — restart Proton Drive");
assert!(state.outdated);
}
#[test]
fn a_daemon_that_does_not_say_has_a_live_session() {
let state = status_state(&status(""), &[], &[], Path::new("/m"));
+61
View File
@@ -100,6 +100,8 @@ a live account since.
| Entry | Title |
|---|---|
| [B206](#b206--an-upgrade-leaves-the-old-daemon-running-and-nothing-says-so) | An upgrade leaves the old daemon running, and nothing says so |
| [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 +309,65 @@ a live account since.
Newest first.
### B206 — An upgrade leaves the old daemon running, and nothing says so
**Status:** Fixed, not verified live
**Found:** 2026-10-11, issue [#33](https://github.com/narrrl/proton-drive-linux/issues/33). On
3.2.0 the reporter still saw `Couldn't sync Documents: Read …/snapshots/37: Permission denied`,
the B196 failure 3.2.0 fixed. Since 3.2.0 the scan leaves an unreadable subfolder out and
`walk_local_dir` can only fail the pass on the sync root, so that message comes from a daemon
older than 3.2.0.
**Where:** `Response::Status` and `daemon_outdated` in `crates/pdfs-core/src/control.rs`;
`status_response` in `crates/pdfs-fuse/src/control.rs`; the tray in `crates/pdfs-gui/src/main.rs`;
the sync card in `crates/pdfs-gui/src/app/pages/status.rs`; `pdfs status` and `pdfs diagnose`
**Repro:** With the service running, install a newer package, then open the app.
**Cause.** A package upgrade replaces the binaries but does not restart the user service, so the
running daemon keeps the old release and its bugs until the next login or a manual restart. The
app, the tray and the CLI start from the new binaries and could not tell, so they described the
new release while the old one did the work.
**Fix.** The daemon reports its release in `Status`. A daemon from before the field omits it,
which reads as older. When the daemon is older than the front-end, the tray says "Update not
finished — restart Proton Drive" with a Restart Proton Drive item, the sync card says "Restart to
finish updating" with a Restart button, and `pdfs status` and `pdfs diagnose` print the
`systemctl --user restart proton-drive` command. A newer daemon is not flagged: then the
front-end is the stale one, and restarting the service would not help.
**Test:** `control::tests::only_an_older_daemon_is_outdated`;
`tests::a_daemon_older_than_the_tray_needs_a_restart` in `pdfs-tray`.
---
### 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<Error> 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
+45
View File
@@ -11,6 +11,32 @@ scratch (user data in `staging/` and `recovery/` is never touched by this).
## [Unreleased]
## [3.3.0] — 2026-10-11
Schema: **42**, unchanged.
### Added
- **A Nix flake for NixOS.** `nix build github:narrrl/proton-drive-linux` builds the client, and
the flake's NixOS module installs it and runs the mount in every graphical session. Pin the
input to a release tag such as `github:narrrl/proton-drive-linux/v3.3.0` to stay on releases.
See [Install → NixOS](INSTALL.md#nixos-flake).
([#37](https://github.com/narrrl/proton-drive-linux/issues/37))
### 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))
- **After an upgrade, the old version kept running in the background, and nothing said so.**
Installing a package does not restart the service, so fixed bugs, such as an unreadable folder
stopping a synced folder, came back until the next login. The tray, the app, `pdfs status` and
`pdfs diagnose` now say when the service is older than the app, and the tray and the app offer
to restart it. ([#33](https://github.com/narrrl/proton-drive-linux/issues/33))
## [3.2.1] — 2026-10-11
Schema: **42**, unchanged.
### Fixed
- **Writing to a file again soon after a partial write could fail with an I/O error.** When the
first write changed only part of a file this client had not read, a second write, or saving
@@ -20,6 +46,22 @@ scratch (user data in `staging/` and `recovery/` is never touched by this).
the app, as on secureblue, the app now offers it in your browser and continues the sign-in
once you are done. `pdfs login` now handles a CAPTCHA the same way instead of failing.
([#29](https://github.com/narrrl/proton-drive-linux/issues/29))
- **The daemon kept several CPU cores busy while offline with changes queued.** Its upload
workers checked the queue in a tight loop for as long as the outage lasted. They now sleep
until the connection comes back. ([#35](https://github.com/narrrl/proton-drive-linux/issues/35))
- **An expired sign-in was shown as "offline" for good.** When Proton refuses the session, the
tray now says "Session expired — sign in again" with a Sign In Again… item, the app shows the
same with a button, and `pdfs status` and `pdfs diagnose` tell you to run `pdfs login`. Cached
files stay readable and changes stay queued until you sign in.
([#35](https://github.com/narrrl/proton-drive-linux/issues/35))
- **The sign-in could be lost after `pdfs login`, `pdfs unlock` or a slow network.** The old
daemon wrote its spent tokens over a new sign-in when it stopped, a timeout could drop a token
refresh halfway, and a refresh was retried with a token Proton had already used. Each process
now writes only into its own session, a refresh always finishes, and it is never repeated.
([#35](https://github.com/narrrl/proton-drive-linux/issues/35))
- **A sign-in refreshed while the keyring was locked was lost on the next restart.** The client
now keeps retrying, holds the new tokens in the kernel keyring meanwhile, and the tray says
"Sign-in not saved — unlock your keyring" until it is written.
## [3.2.0] — 2026-10-08
@@ -1716,6 +1758,9 @@ 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.3.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.3.0
[3.2.1]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.2.1
[3.2.0]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.2.0
[3.1.2]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.1.2
+6 -3
View File
@@ -153,15 +153,18 @@ Releases are cut from `main` by pushing a `v*` tag. `.github/workflows/release.y
1. checks that the tag, the workspace version in `Cargo.toml` and `pkgver` in
`packaging/PKGBUILD` agree;
2. runs the quality gates;
3. builds the release binaries and packages a `.tar.gz`, a `.deb` and a GPG-signed `.rpm`;
4. publishes them as a GitHub release;
3. builds the release binaries and packages a `.tar.gz`, a `.deb` and a GPG-signed `.rpm`, and
builds the Nix flake at the tag, which is what a flake input pinned to `vX.Y.Z` gets;
4. publishes them as a GitHub release, with the version's section of `docs/CHANGELOG.md` as the
notes;
5. calls `.github/workflows/aur.yml`, which updates and pushes the AUR recipes and commits them
back to `main`.
To prepare a release:
```bash
# 1. Bump the version in Cargo.toml and packaging/PKGBUILD, then refresh the lock file.
# 1. Bump the version in Cargo.toml, packaging/PKGBUILD, po/pdfs.pot and the pinned flake
# URL in docs/INSTALL.md, then refresh the lock file.
cargo update --workspace
# 2. Move the [Unreleased] notes in docs/CHANGELOG.md under the new version.
# 3. Commit, tag and push.
+42
View File
@@ -82,6 +82,48 @@ paru -S proton-drive-for-linux # or yay, or any other AUR helper
The recipes live in [`packaging/aur/`](../packaging/aur/README.md).
### NixOS (flake)
The repository is a flake. Add it as an input and enable the module, which installs the package,
links the user service into every graphical session and turns on the `fusermount3` wrapper and
GNOME Keyring for you:
```nix
{
inputs.proton-drive-linux.url = "github:narrrl/proton-drive-linux";
outputs = { nixpkgs, proton-drive-linux, ... }: {
nixosConfigurations.<host> = nixpkgs.lib.nixosSystem {
modules = [
proton-drive-linux.nixosModules.default
{ services.proton-drive-linux.enable = true; }
];
};
};
}
```
That URL follows `main`. To stay on releases, pin the input to a release tag instead, and change
the tag to update:
```nix
inputs.proton-drive-linux.url = "github:narrrl/proton-drive-linux/v3.3.0";
```
Each release tag is built with Nix before the release is published.
To try it without changing your system, run the app straight from the flake:
```bash
nix run github:narrrl/proton-drive-linux#proton-drive-linux -- --help # the CLI
nix shell github:narrrl/proton-drive-linux -c pdfs-app # the desktop app
```
The mount itself needs the setuid `fusermount3` from NixOS (`programs.fuse.enable`, on by default
with the module) and a Secret Service such as GNOME Keyring or KWallet. To leave out ffmpeg and
with it video thumbnails, override the package with `withFfmpeg = false`. The recipe lives in
[`packaging/nix/`](../packaging/nix/).
### Other distributions
Unpack the `.tar.gz` into a directory on your `PATH`, then install the service unit as shown in
Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1791456040,
"narHash": "sha256-H83yz/do+hjOO0uujHyH8RgtIQP2v7Mw+cLwcZK/xKc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e7439b6b14ad3cc35d05608ebca9bce01a25f5f8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+41
View File
@@ -0,0 +1,41 @@
{
description = "Unofficial Proton Drive client for Linux: files-on-demand FUSE mount, CLI, GTK4 app and tray";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs =
{ self, nixpkgs }:
let
systems = [
"x86_64-linux"
"aarch64-linux"
];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
in
{
packages = forAllSystems (pkgs: rec {
proton-drive-linux = pkgs.callPackage ./packaging/nix/package.nix { };
default = proton-drive-linux;
});
overlays.default = final: _: {
proton-drive-linux = final.callPackage ./packaging/nix/package.nix { };
};
nixosModules.default = import ./packaging/nix/module.nix self;
devShells = forAllSystems (pkgs: {
default = pkgs.mkShell {
inputsFrom = [ self.packages.${pkgs.stdenv.hostPlatform.system}.proton-drive-linux ];
packages = with pkgs; [
cargo
clippy
rustfmt
rust-analyzer
];
};
});
formatter = forAllSystems (pkgs: pkgs.nixfmt);
};
}
+1 -1
View File
@@ -9,7 +9,7 @@
# Build + install: cd packaging && makepkg -fi
pkgname=proton-drive-linux
pkgver=3.2.0
pkgver=3.3.0
pkgrel=1
pkgdesc="Unofficial Proton Drive client for Linux: files-on-demand FUSE mount, CLI, GTK4 app + tray"
arch=('x86_64')
@@ -1,6 +1,6 @@
pkgbase = proton-drive-for-linux-bin
pkgdesc = Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)
pkgver = 3.2.0
pkgver = 3.2.1
pkgrel = 1
url = https://proton-drive.narl.io
arch = x86_64
@@ -21,21 +21,21 @@ pkgbase = proton-drive-for-linux-bin
optdepends = xdg-utils: "Open folder" action in the tray menu
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
provides = proton-drive-for-linux=3.2.0
provides = proton-drive-linux=3.2.0
provides = proton-drive-for-linux=3.2.1
provides = proton-drive-linux=3.2.1
provides = pdfs
conflicts = proton-drive-linux
conflicts = proton-drive-for-linux
conflicts = proton-drive-for-linux-git
options = !strip
options = !debug
source = proton-drive-linux-3.2.0-x86_64.tar.gz::https://github.com/narrrl/proton-drive-linux/releases/download/v3.2.0/proton-drive-linux-3.2.0-x86_64.tar.gz
source = io.narl.proton-drive-linux-3.2.0.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.0/packaging/io.narl.proton-drive-linux.desktop
source = io.narl.proton-drive-linux-tray-3.2.0.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.0/packaging/io.narl.proton-drive-linux-tray.desktop
source = io.narl.proton-drive-linux-3.2.0.svg::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.0/packaging/io.narl.proton-drive-linux.svg
source = proton-drive-3.2.0.service::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.0/packaging/proton-drive.service
source = LICENSE-3.2.0::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.0/LICENSE
sha256sums = 3d498c9ea366c94e77e6f3c81b804b8b3497faecfa74480274d6ec9b5121665b
source = proton-drive-linux-3.2.1-x86_64.tar.gz::https://github.com/narrrl/proton-drive-linux/releases/download/v3.2.1/proton-drive-linux-3.2.1-x86_64.tar.gz
source = io.narl.proton-drive-linux-3.2.1.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.1/packaging/io.narl.proton-drive-linux.desktop
source = io.narl.proton-drive-linux-tray-3.2.1.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.1/packaging/io.narl.proton-drive-linux-tray.desktop
source = io.narl.proton-drive-linux-3.2.1.svg::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.1/packaging/io.narl.proton-drive-linux.svg
source = proton-drive-3.2.1.service::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.1/packaging/proton-drive.service
source = LICENSE-3.2.1::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.1/LICENSE
sha256sums = b031f15296a1947d4083f944ad0cd12a18e676cdb9746055d7673b3b3832c850
sha256sums = d190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123
sha256sums = 1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0
sha256sums = e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828
@@ -1,7 +1,7 @@
# Maintainer: Nils Pukropp <contact@narl.io>
pkgname=proton-drive-for-linux-bin
pkgver=3.2.0
pkgver=3.2.1
pkgrel=1
pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)"
arch=('x86_64')
@@ -34,7 +34,7 @@ source=("proton-drive-linux-$pkgver-x86_64.tar.gz::$_repo/releases/download/$_ta
"io.narl.proton-drive-linux-$pkgver.svg::$_raw/packaging/io.narl.proton-drive-linux.svg"
"proton-drive-$pkgver.service::$_raw/packaging/proton-drive.service"
"LICENSE-$pkgver::$_raw/LICENSE")
sha256sums=('3d498c9ea366c94e77e6f3c81b804b8b3497faecfa74480274d6ec9b5121665b'
sha256sums=('b031f15296a1947d4083f944ad0cd12a18e676cdb9746055d7673b3b3832c850'
'd190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123'
'1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0'
'e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828'
@@ -1,6 +1,6 @@
pkgbase = proton-drive-for-linux-git
pkgdesc = Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git)
pkgver = 3.2.0.r0.g4c498d5
pkgver = 3.2.1.r0.g170c3f0
pkgrel = 1
url = https://proton-drive.narl.io
arch = x86_64
@@ -24,8 +24,8 @@ pkgbase = proton-drive-for-linux-git
optdepends = xdg-utils: "Open folder" action in the tray menu
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
provides = proton-drive-for-linux=3.2.0.r0.g4c498d5
provides = proton-drive-linux=3.2.0.r0.g4c498d5
provides = proton-drive-for-linux=3.2.1.r0.g170c3f0
provides = proton-drive-linux=3.2.1.r0.g170c3f0
provides = pdfs
conflicts = proton-drive-linux
conflicts = proton-drive-for-linux
@@ -1,7 +1,7 @@
# Maintainer: Nils Pukropp <contact@narl.io>
pkgname=proton-drive-for-linux-git
pkgver=3.2.0.r0.g4c498d5
pkgver=3.2.1.r0.g170c3f0
pkgrel=1
pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git)"
arch=('x86_64')
@@ -1,6 +1,6 @@
pkgbase = proton-drive-for-linux
pkgdesc = Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray
pkgver = 3.2.0
pkgver = 3.2.1
pkgrel = 1
url = https://proton-drive.narl.io
arch = x86_64
@@ -23,13 +23,13 @@ pkgbase = proton-drive-for-linux
optdepends = xdg-utils: "Open folder" action in the tray menu
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
provides = proton-drive-linux=3.2.0
provides = proton-drive-linux=3.2.1
provides = pdfs
conflicts = proton-drive-linux
conflicts = proton-drive-for-linux-bin
conflicts = proton-drive-for-linux-git
options = !lto
source = proton-drive-for-linux-3.2.0.tar.gz::https://github.com/narrrl/proton-drive-linux/archive/refs/tags/v3.2.0.tar.gz
sha256sums = a15ddb37808dc01bde3a3d092b03b5047cdecca37efd14d906c9bf353d242bd8
source = proton-drive-for-linux-3.2.1.tar.gz::https://github.com/narrrl/proton-drive-linux/archive/refs/tags/v3.2.1.tar.gz
sha256sums = 275fefa2feacbd8319b3986205433729d6f5241b5bedfa4651b47bea9700b525
pkgname = proton-drive-for-linux
@@ -1,7 +1,7 @@
# Maintainer: Nils Pukropp <contact@narl.io>
pkgname=proton-drive-for-linux
pkgver=3.2.0
pkgver=3.2.1
pkgrel=1
pkgdesc="Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray"
arch=('x86_64')
@@ -24,7 +24,7 @@ conflicts=('proton-drive-linux' 'proton-drive-for-linux-bin' 'proton-drive-for-l
# LTO has broken GTK/Rust links for this workspace; the release profile keeps it off.
options=('!lto')
source=("$pkgname-$pkgver.tar.gz::$_repo/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('a15ddb37808dc01bde3a3d092b03b5047cdecca37efd14d906c9bf353d242bd8')
sha256sums=('275fefa2feacbd8319b3986205433729d6f5241b5bedfa4651b47bea9700b525')
_srcname="proton-drive-linux-$pkgver"
+42
View File
@@ -0,0 +1,42 @@
# NixOS module: installs the package and runs the mount daemon in every
# graphical session, as the other packages' user service does.
#
# services.proton-drive-linux.enable = true;
self:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.proton-drive-linux;
in
{
options.services.proton-drive-linux = {
enable = lib.mkEnableOption "the Proton Drive for Linux mount daemon, app and tray";
package = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.stdenv.hostPlatform.system}.proton-drive-linux;
defaultText = lib.literalExpression "proton-drive-linux.packages.\${system}.proton-drive-linux";
description = "The package to use.";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
# The unit ships in the package; this links it and pulls it into the
# graphical session, which is what `systemctl --user enable` would do.
systemd.packages = [ cfg.package ];
systemd.user.services.proton-drive = {
wantedBy = [ "graphical-session.target" ];
# The mount runs fusermount3 from PATH, and it has to be the setuid
# wrapper; the PATH NixOS gives a unit does not include it.
path = [ "/run/wrappers" ];
};
programs.fuse.enable = lib.mkDefault true;
# Session tokens are kept in the Secret Service.
services.gnome.gnome-keyring.enable = lib.mkDefault true;
};
}
+127
View File
@@ -0,0 +1,127 @@
# Nix package for the workspace, built from the checkout this file lives in.
# The flake at the repository root exposes it; `nix build` builds it.
{
lib,
rustPlatform,
adwaita-icon-theme,
pkg-config,
gettext,
glib,
wrapGAppsHook4,
fuse3,
glib-networking,
gtk4,
libadwaita,
webkitgtk_6_0,
libsecret,
dbus,
exiftool,
ffmpeg-headless,
withFfmpeg ? true,
}:
let
cargoToml = lib.importTOML ../../Cargo.toml;
in
rustPlatform.buildRustPackage {
pname = "proton-drive-linux";
inherit (cargoToml.workspace.package) version;
# Only what the build reads, so editing docs or CI does not rebuild.
src = lib.fileset.toSource {
root = ../..;
fileset = lib.fileset.unions [
../../Cargo.toml
../../Cargo.lock
../../crates
../../po
../../LICENSE
../proton-drive.service
../io.narl.proton-drive-linux.desktop
../io.narl.proton-drive-linux-tray.desktop
../io.narl.proton-drive-linux.svg
];
};
cargoLock.lockFile = ../../Cargo.lock;
cargoBuildFlags = [
"--bin"
"pdfs"
"--bin"
"pdfs-tray"
"--bin"
"pdfs-app"
"--bin"
"pdfs-prompt"
];
nativeBuildInputs = [
pkg-config
gettext # msgfmt, for po/build.sh
glib # glib-compile-resources, for the GUI's build.rs
wrapGAppsHook4
];
buildInputs = [
fuse3
# TLS for WebKitGTK's GIO: without it the CAPTCHA page fails with "TLS
# support is not available". wrapGAppsHook4 adds it to GIO_EXTRA_MODULES.
glib-networking
# The stock icons the app asks for, such as the Photos and Computers
# sidebar entries. GTK ships only a few itself, and a session without
# Adwaita installed showed the rest as missing. wrapGAppsHook4 adds it to
# XDG_DATA_DIRS.
adwaita-icon-theme
gtk4
libadwaita
webkitgtk_6_0
libsecret
dbus
];
# The app looks for its catalogs here instead of /usr/share/locale.
env.PDFS_LOCALEDIR = "${placeholder "out"}/share/locale";
# The suite mounts FUSE, talks to D-Bus and the kernel keyring, and checks
# permissions a build sandbox does not have; CI runs it on every push.
doCheck = false;
postInstall = ''
install -Dm644 packaging/io.narl.proton-drive-linux.desktop \
$out/share/applications/io.narl.proton-drive-linux.desktop
install -Dm644 packaging/io.narl.proton-drive-linux-tray.desktop \
$out/etc/xdg/autostart/io.narl.proton-drive-linux-tray.desktop
install -Dm644 packaging/io.narl.proton-drive-linux.svg \
$out/share/icons/hicolor/scalable/apps/io.narl.proton-drive-linux.svg
substituteInPlace $out/share/applications/io.narl.proton-drive-linux.desktop \
--replace-fail "Exec=pdfs-app" "Exec=$out/bin/pdfs-app"
substituteInPlace $out/etc/xdg/autostart/io.narl.proton-drive-linux-tray.desktop \
--replace-fail "Exec=pdfs-tray" "Exec=$out/bin/pdfs-tray"
install -Dm644 packaging/proton-drive.service \
$out/lib/systemd/user/proton-drive.service
substituteInPlace $out/lib/systemd/user/proton-drive.service \
--replace-fail "ExecStart=/usr/bin/pdfs" "ExecStart=$out/bin/pdfs"
patchShebangs po/build.sh
po/build.sh $out/share/locale
'';
# Thumbnails shell out to exiftool, and to ffmpeg for video. Suffixed, so a
# user's own builds on PATH win. fusermount3 comes from the system: on NixOS
# it is the setuid wrapper in /run/wrappers/bin.
preFixup = ''
gappsWrapperArgs+=(--suffix PATH : ${
lib.makeBinPath ([ exiftool ] ++ lib.optional withFfmpeg ffmpeg-headless)
})
'';
meta = {
description = "Unofficial Proton Drive client for Linux: files-on-demand FUSE mount, CLI, GTK4 app and tray";
homepage = "https://proton-drive.narl.io";
license = lib.licenses.mit;
platforms = lib.platforms.linux;
mainProgram = "pdfs";
};
}
+421 -394
View File
File diff suppressed because it is too large. Load diff
+418 -393
View File
File diff suppressed because it is too large. Load diff
+420 -394
View File
File diff suppressed because it is too large. Load diff
+421 -394
View File
File diff suppressed because it is too large. Load diff
+420 -394
View File
File diff suppressed because it is too large. Load diff
+420 -394
View File
File diff suppressed because it is too large. Load diff
+431 -406
View File
File diff suppressed because it is too large. Load diff
+421 -394
View File
File diff suppressed because it is too large. Load diff
+420 -394
View File
File diff suppressed because it is too large. Load diff