mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 16:56:59 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
160626b29d | ||
|
|
f1f1f8ad4d | ||
|
|
bb28a18f49 | ||
|
|
a3c0c9ee9b | ||
|
|
91fa1614ed | ||
|
|
87361a4573 | ||
|
|
6d0b6ad553 | ||
|
|
e402981708 | ||
|
|
453c439b74 | ||
|
|
ceba0f5849 |
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -3,3 +3,5 @@
|
||||
*.pdb
|
||||
.env
|
||||
/rpmbuild
|
||||
/result
|
||||
/result-*
|
||||
Generated
+4
-4
@@ -3474,7 +3474,7 @@ checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
|
||||
|
||||
[[package]]
|
||||
name = "pdfs-cli"
|
||||
version = "3.2.1"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -3490,7 +3490,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfs-core"
|
||||
version = "3.2.1"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.23.1",
|
||||
@@ -3516,7 +3516,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfs-fuse"
|
||||
version = "3.2.1"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.23.1",
|
||||
@@ -3538,7 +3538,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pdfs-gui"
|
||||
version = "3.2.1"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ resolver = "2"
|
||||
members = ["crates/pdfs-core", "crates/pdfs-fuse", "crates/pdfs-cli", "crates/pdfs-gui"]
|
||||
|
||||
[workspace.package]
|
||||
version = "3.2.1"
|
||||
version = "3.3.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
authors = ["Nils Pukropp <contact@narl.io>"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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: >k4::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: >k4::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: >k4::Button, add_folder: >k
|
||||
.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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,28 @@ 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.
|
||||
@@ -1736,6 +1758,7 @@ 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
|
||||
|
||||
+6
-3
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -9,7 +9,7 @@
|
||||
# Build + install: cd packaging && makepkg -fi
|
||||
|
||||
pkgname=proton-drive-linux
|
||||
pkgver=3.2.1
|
||||
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"
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
};
|
||||
}
|
||||
+418
-393
File diff suppressed because it is too large.
Load diff
+431
-406
File diff suppressed because it is too large.
Load diff
+420
-394
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user