mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 08:56:59 +01:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
160626b29d | ||
|
|
f1f1f8ad4d | ||
|
|
bb28a18f49 | ||
|
|
a3c0c9ee9b | ||
|
|
91fa1614ed | ||
|
|
87361a4573 | ||
|
|
6d0b6ad553 | ||
|
|
170c3f01ff | ||
|
|
976ad2aa04 | ||
|
|
b73cd087aa |
No files matched your search
@@ -230,21 +230,60 @@ jobs:
|
|||||||
*.rpm
|
*.rpm
|
||||||
if-no-files-found: error
|
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:
|
publish:
|
||||||
name: Create GitHub Release
|
name: Create GitHub Release
|
||||||
needs: [checks, build-release]
|
needs: [checks, build-release, nix]
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Download packages
|
- name: Download packages
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: proton-drive-linux-builds
|
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
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v3
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
|
body_path: release-notes.md
|
||||||
files: |
|
files: |
|
||||||
*.deb
|
*.deb
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
|||||||
Generated
+4
-4
@@ -3474,7 +3474,7 @@ checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdfs-cli"
|
name = "pdfs-cli"
|
||||||
version = "3.2.0"
|
version = "3.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -3490,7 +3490,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdfs-core"
|
name = "pdfs-core"
|
||||||
version = "3.2.0"
|
version = "3.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.23.1",
|
"base64 0.23.1",
|
||||||
@@ -3516,7 +3516,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdfs-fuse"
|
name = "pdfs-fuse"
|
||||||
version = "3.2.0"
|
version = "3.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.23.1",
|
"base64 0.23.1",
|
||||||
@@ -3538,7 +3538,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdfs-gui"
|
name = "pdfs-gui"
|
||||||
version = "3.2.0"
|
version = "3.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ resolver = "2"
|
|||||||
members = ["crates/pdfs-core", "crates/pdfs-fuse", "crates/pdfs-cli", "crates/pdfs-gui"]
|
members = ["crates/pdfs-core", "crates/pdfs-fuse", "crates/pdfs-cli", "crates/pdfs-gui"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "3.2.0"
|
version = "3.3.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Nils Pukropp <contact@narl.io>"]
|
authors = ["Nils Pukropp <contact@narl.io>"]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use pdfs_core::config::AppDirs;
|
|||||||
use pdfs_core::control::{
|
use pdfs_core::control::{
|
||||||
ConflictKeep, ErrorKind, MountKind, RefreshScope, Request as CtlRequest,
|
ConflictKeep, ErrorKind, MountKind, RefreshScope, Request as CtlRequest,
|
||||||
Response as CtlResponse, RestoreItem as CtlRestoreItem, ShareEntryKind, SyncIssue, SyncPhase,
|
Response as CtlResponse, RestoreItem as CtlRestoreItem, ShareEntryKind, SyncIssue, SyncPhase,
|
||||||
pending_summary,
|
daemon_outdated, pending_summary,
|
||||||
};
|
};
|
||||||
use pdfs_core::db::Db;
|
use pdfs_core::db::Db;
|
||||||
use pdfs_core::service;
|
use pdfs_core::service;
|
||||||
@@ -2026,6 +2026,7 @@ fn cmd_status() -> Result<()> {
|
|||||||
paused_until,
|
paused_until,
|
||||||
session_expired,
|
session_expired,
|
||||||
tokens_unsaved,
|
tokens_unsaved,
|
||||||
|
version,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
let state = match (online, paused) {
|
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`"
|
"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:?}"),
|
Ok(other) => println!("Mount: unexpected response {other:?}"),
|
||||||
Err(_) => println!("Mount: not running."),
|
Err(_) => println!("Mount: not running."),
|
||||||
@@ -2074,6 +2078,20 @@ fn cmd_status() -> Result<()> {
|
|||||||
Ok(())
|
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<()> {
|
fn cmd_locations() -> Result<()> {
|
||||||
let response = control_request(CtlRequest::ListLocations)?;
|
let response = control_request(CtlRequest::ListLocations)?;
|
||||||
if emit_json(&response)? {
|
if emit_json(&response)? {
|
||||||
@@ -3444,9 +3462,17 @@ fn cmd_diagnose() -> Result<()> {
|
|||||||
tokens_unsaved,
|
tokens_unsaved,
|
||||||
pending_uploads,
|
pending_uploads,
|
||||||
pending_changes,
|
pending_changes,
|
||||||
|
version,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
report.finding(DiagnoseLevel::Ok, " daemon responding", "");
|
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 live_type = mounted_fs_type(Path::new(&mountpoint));
|
||||||
let is_fuse = live_type
|
let is_fuse = live_type
|
||||||
.as_deref()
|
.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.
|
/// One worker thread in a [`Diagnostics`] report.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct WorkerState {
|
pub struct WorkerState {
|
||||||
@@ -1969,6 +1990,11 @@ pub enum Response {
|
|||||||
/// out.
|
/// out.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
tokens_unsaved: bool,
|
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.
|
/// A human-readable success message.
|
||||||
Ok { message: String },
|
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
|
/// The mutation requests must survive a line-delimited JSON round-trip, since
|
||||||
/// that is exactly how they cross the control socket.
|
/// that is exactly how they cross the control socket.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1689,9 +1689,16 @@ fn writing_nodes_does_not_scale_with_the_size_of_the_index() {
|
|||||||
.collect();
|
.collect();
|
||||||
db.upsert_nodes(&batch).unwrap();
|
db.upsert_nodes(&batch).unwrap();
|
||||||
|
|
||||||
let t = Instant::now();
|
// The fastest of a few runs: the rest of the suite runs alongside, and
|
||||||
db.upsert_nodes(&batch).unwrap();
|
// one run that lost the CPU to it read as the regression.
|
||||||
t.elapsed()
|
(0..5)
|
||||||
|
.map(|_| {
|
||||||
|
let t = Instant::now();
|
||||||
|
db.upsert_nodes(&batch).unwrap();
|
||||||
|
t.elapsed()
|
||||||
|
})
|
||||||
|
.min()
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
let small = cost_against_index_of(200);
|
let small = cost_against_index_of(200);
|
||||||
|
|||||||
@@ -154,17 +154,17 @@ impl CoreError {
|
|||||||
/// through the boxes the drain deals in, where the concrete type survives
|
/// through the boxes the drain deals in, where the concrete type survives
|
||||||
/// but the static one does not.
|
/// but the static one does not.
|
||||||
///
|
///
|
||||||
/// A failure that is not an API response at all — a transport error, a
|
/// A transport error, a timeout or a TLS failure is what being offline
|
||||||
/// timeout, a TLS failure — is what being offline looks like from here, so
|
/// looks like from here, so it classifies as [`ErrorKind::Offline`] rather
|
||||||
/// it classifies as [`ErrorKind::Offline`] rather than
|
/// than [`ErrorKind::Remote`]: the request never reached Proton, and
|
||||||
/// [`ErrorKind::Remote`]: the request never reached Proton, and telling the
|
/// telling the user their connection is down beats telling them the
|
||||||
/// user their connection is down beats telling them the server refused.
|
/// 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 {
|
pub fn from_api(e: &(dyn std::error::Error + 'static), what: &str) -> Self {
|
||||||
let kind = match e.downcast_ref::<proton_sdk::ProtonError>() {
|
let kind = match e.downcast_ref::<proton_sdk::ProtonError>() {
|
||||||
Some(proton_sdk::ProtonError::Api(api)) => api_kind(api.code),
|
Some(proton) => proton_kind(proton),
|
||||||
// A `ProtonError` that is not an `Api` variant never reached the
|
// Not the SDK's at all: a timeout around the call, or the socket.
|
||||||
// API; so does anything that is not a `ProtonError` at all.
|
None => ErrorKind::Offline,
|
||||||
_ => ErrorKind::Offline,
|
|
||||||
};
|
};
|
||||||
Self::new(kind, format!("{what}: {e}"))
|
Self::new(kind, format!("{what}: {e}"))
|
||||||
}
|
}
|
||||||
@@ -214,15 +214,33 @@ impl From<Error> for CoreError {
|
|||||||
| Error::Locked
|
| Error::Locked
|
||||||
| Error::WrongMailboxPassword
|
| Error::WrongMailboxPassword
|
||||||
| Error::HumanVerificationRequired(_) => ErrorKind::Denied,
|
| Error::HumanVerificationRequired(_) => ErrorKind::Denied,
|
||||||
Error::Proton(proton_sdk::ProtonError::Api(api)) => api_kind(api.code),
|
Error::Proton(proton) => proton_kind(proton),
|
||||||
// A `ProtonError` that never reached the API is a transport failure.
|
|
||||||
Error::Proton(_) => ErrorKind::Offline,
|
|
||||||
_ => ErrorKind::Internal,
|
_ => ErrorKind::Internal,
|
||||||
};
|
};
|
||||||
Self::new(kind, e.to_string())
|
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.
|
/// What an API response code means to a front-end.
|
||||||
///
|
///
|
||||||
/// The grouping is by *what the user can do about it*, which is what
|
/// 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);
|
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
|
/// `Error` is the plumbing type; converting it must classify as sharply as
|
||||||
/// the direct path, or the two disagree about the same failure.
|
/// the direct path, or the two disagree about the same failure.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ pub(crate) fn status_response(core: &Core, username: &str, mountpoint: &Path) ->
|
|||||||
online: core.online.load(Ordering::Relaxed),
|
online: core.online.load(Ordering::Relaxed),
|
||||||
session_expired: core.session_expired(),
|
session_expired: core.session_expired(),
|
||||||
tokens_unsaved: pdfs_core::auth::tokens_unsaved(),
|
tokens_unsaved: pdfs_core::auth::tokens_unsaved(),
|
||||||
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
pending_uploads: queued.uploads.max(0) as u64 + landing,
|
pending_uploads: queued.uploads.max(0) as u64 + landing,
|
||||||
pending_changes: queued.changes.max(0) as u64,
|
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) => {
|
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"))
|
CtlResponse::error(CoreError::from_api(&e, "upload photo failed"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ mod tests {
|
|||||||
online: true,
|
online: true,
|
||||||
session_expired: false,
|
session_expired: false,
|
||||||
tokens_unsaved: false,
|
tokens_unsaved: false,
|
||||||
|
version: "3.3.0".into(),
|
||||||
pending_uploads: 1,
|
pending_uploads: 1,
|
||||||
pending_changes: 0,
|
pending_changes: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -538,6 +538,7 @@ fn build_window(app: &adw::Application) {
|
|||||||
reload_pending: Cell::new(false),
|
reload_pending: Cell::new(false),
|
||||||
loading: Cell::new(false),
|
loading: Cell::new(false),
|
||||||
has_more: Cell::new(false),
|
has_more: Cell::new(false),
|
||||||
|
library: Cell::new(true),
|
||||||
page_waiters: RefCell::new(Vec::new()),
|
page_waiters: RefCell::new(Vec::new()),
|
||||||
width: Cell::new(0),
|
width: Cell::new(0),
|
||||||
photo_tex: RefCell::new(HashMap::new()),
|
photo_tex: RefCell::new(HashMap::new()),
|
||||||
@@ -580,6 +581,7 @@ fn build_window(app: &adw::Application) {
|
|||||||
pause: locations_widgets.pause.clone(),
|
pause: locations_widgets.pause.clone(),
|
||||||
paused: Cell::new(false),
|
paused: Cell::new(false),
|
||||||
sign_in: locations_widgets.card_sign_in.clone(),
|
sign_in: locations_widgets.card_sign_in.clone(),
|
||||||
|
restart: locations_widgets.card_restart.clone(),
|
||||||
},
|
},
|
||||||
queue: QueueState {
|
queue: QueueState {
|
||||||
group: locations_widgets.queue_group.clone(),
|
group: locations_widgets.queue_group.clone(),
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ pub(crate) struct SyncCard {
|
|||||||
pub(crate) paused: Cell<bool>,
|
pub(crate) paused: Cell<bool>,
|
||||||
/// Shown only while Proton refuses the session; opens the sign-in page.
|
/// Shown only while Proton refuses the session; opens the sign-in page.
|
||||||
pub(crate) sign_in: gtk4::Button,
|
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
|
/// 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_title: gtk4::Label,
|
||||||
pub(crate) card_detail: gtk4::Label,
|
pub(crate) card_detail: gtk4::Label,
|
||||||
pub(crate) card_sign_in: gtk4::Button,
|
pub(crate) card_sign_in: gtk4::Button,
|
||||||
|
pub(crate) card_restart: gtk4::Button,
|
||||||
pub(crate) pause: adw::SplitButton,
|
pub(crate) pause: adw::SplitButton,
|
||||||
pub(crate) queue_group: adw::PreferencesGroup,
|
pub(crate) queue_group: adw::PreferencesGroup,
|
||||||
pub(crate) retry_all: gtk4::Button,
|
pub(crate) retry_all: gtk4::Button,
|
||||||
@@ -137,7 +140,7 @@ pub(crate) fn build_locations_page(history: >k4::Widget) -> (gtk4::Widget, Loc
|
|||||||
.build();
|
.build();
|
||||||
add_folder.add_css_class("flat");
|
add_folder.add_css_class("flat");
|
||||||
let transfers_group = build_transfers_group();
|
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 pause = build_pause_button();
|
||||||
let (queue_group, retry_all) = build_queue_group();
|
let (queue_group, retry_all) = build_queue_group();
|
||||||
let conflicts_group = adw::PreferencesGroup::builder()
|
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_title,
|
||||||
card_detail,
|
card_detail,
|
||||||
card_sign_in,
|
card_sign_in,
|
||||||
|
card_restart,
|
||||||
pause,
|
pause,
|
||||||
queue_group,
|
queue_group,
|
||||||
retry_all,
|
retry_all,
|
||||||
@@ -299,6 +303,11 @@ pub(crate) fn wire_locations(ui: &Rc<Ui>, retry: >k4::Button, add_folder: >k
|
|||||||
.card
|
.card
|
||||||
.sign_in
|
.sign_in
|
||||||
.connect_clicked(move |_| show_login(&ui_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();
|
let ui_retry = ui.clone();
|
||||||
ui.locations
|
ui.locations
|
||||||
.queue
|
.queue
|
||||||
@@ -396,6 +405,7 @@ fn build_sync_card() -> (
|
|||||||
gtk4::Label,
|
gtk4::Label,
|
||||||
gtk4::Label,
|
gtk4::Label,
|
||||||
gtk4::Button,
|
gtk4::Button,
|
||||||
|
gtk4::Button,
|
||||||
) {
|
) {
|
||||||
let icon = gtk4::Image::builder()
|
let icon = gtk4::Image::builder()
|
||||||
.icon_name("pdfs-sync-symbolic")
|
.icon_name("pdfs-sync-symbolic")
|
||||||
@@ -430,10 +440,18 @@ fn build_sync_card() -> (
|
|||||||
.build();
|
.build();
|
||||||
sign_in.add_css_class("pill");
|
sign_in.add_css_class("pill");
|
||||||
sign_in.add_css_class("suggested-action");
|
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(&icon);
|
||||||
card.append(&text);
|
card.append(&text);
|
||||||
card.append(&sign_in);
|
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.
|
/// Pause/Resume, with timed pauses in its menu.
|
||||||
|
|||||||
@@ -140,6 +140,9 @@ pub(crate) struct GalleryState {
|
|||||||
pub(crate) loading: Cell<bool>,
|
pub(crate) loading: Cell<bool>,
|
||||||
/// Whether the last page came back full, so there may be more to load.
|
/// Whether the last page came back full, so there may be more to load.
|
||||||
pub(crate) has_more: Cell<bool>,
|
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
|
/// Run once the page in flight has landed in [`Self::model`] — how the
|
||||||
/// lightbox steps past the last loaded photo.
|
/// lightbox steps past the last loaded photo.
|
||||||
pub(crate) page_waiters: RefCell<Vec<Box<dyn FnOnce()>>>,
|
pub(crate) page_waiters: RefCell<Vec<Box<dyn FnOnce()>>>,
|
||||||
@@ -3341,6 +3344,12 @@ pub(crate) fn load_gallery(ui: &Rc<Ui>, append: bool) {
|
|||||||
items,
|
items,
|
||||||
counts,
|
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 {
|
if !available {
|
||||||
gallery_status(
|
gallery_status(
|
||||||
&ui,
|
&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) {
|
pub(crate) fn set_mounted(ui: &Rc<Ui>, mounted: bool) {
|
||||||
*ui.mounted.borrow_mut() = mounted;
|
*ui.mounted.borrow_mut() = mounted;
|
||||||
sync_mounted_actions(ui, 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.pin_row.set_sensitive(mounted);
|
||||||
ui.details.details.rename_button.set_sensitive(mounted);
|
ui.details.details.rename_button.set_sensitive(mounted);
|
||||||
ui.details.details.trash_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,
|
paused_until,
|
||||||
session_expired,
|
session_expired,
|
||||||
tokens_unsaved,
|
tokens_unsaved,
|
||||||
|
version,
|
||||||
..
|
..
|
||||||
} = status
|
} = status
|
||||||
else {
|
else {
|
||||||
@@ -1244,6 +1247,8 @@ pub(crate) fn paint_status(ui: &Rc<Ui>, status: Response) {
|
|||||||
SyncState::SessionExpired { queued }
|
SyncState::SessionExpired { queued }
|
||||||
} else if tokens_unsaved {
|
} else if tokens_unsaved {
|
||||||
SyncState::TokensUnsaved
|
SyncState::TokensUnsaved
|
||||||
|
} else if pdfs_core::control::daemon_outdated(&version) {
|
||||||
|
SyncState::Outdated { running: version }
|
||||||
} else if paused {
|
} else if paused {
|
||||||
SyncState::Paused {
|
SyncState::Paused {
|
||||||
until: paused_until,
|
until: paused_until,
|
||||||
@@ -1315,6 +1320,12 @@ pub(crate) enum SyncState {
|
|||||||
/// The keyring refuses the session's refreshed tokens. Sync goes on, but
|
/// The keyring refuses the session's refreshed tokens. Sync goes on, but
|
||||||
/// a reboot before the keyring takes them signs the user out.
|
/// a reboot before the keyring takes them signs the user out.
|
||||||
TokensUnsaved,
|
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
|
/// The user paused syncing; nothing goes up until it resumes. Outranks
|
||||||
/// everything else, because it is the reason for everything else.
|
/// everything else, because it is the reason for everything else.
|
||||||
Paused {
|
Paused {
|
||||||
@@ -1350,6 +1361,7 @@ fn paint_sync_status(ui: &Rc<Ui>, state: SyncState) {
|
|||||||
let paused = matches!(state, SyncState::Paused { .. });
|
let paused = matches!(state, SyncState::Paused { .. });
|
||||||
let connected = !matches!(state, SyncState::Disconnected | SyncState::NotResponding);
|
let connected = !matches!(state, SyncState::Disconnected | SyncState::NotResponding);
|
||||||
let expired = matches!(state, SyncState::SessionExpired { .. });
|
let expired = matches!(state, SyncState::SessionExpired { .. });
|
||||||
|
let outdated = matches!(state, SyncState::Outdated { .. });
|
||||||
let (icon, class, title, detail) = match state {
|
let (icon, class, title, detail) = match state {
|
||||||
SyncState::SessionExpired { queued } => (
|
SyncState::SessionExpired { queued } => (
|
||||||
"dialog-password-symbolic",
|
"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",
|
"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 } => (
|
SyncState::Paused { until, queued } => (
|
||||||
"media-playback-pause-symbolic",
|
"media-playback-pause-symbolic",
|
||||||
Some("warning"),
|
Some("warning"),
|
||||||
@@ -1443,6 +1469,7 @@ fn paint_sync_status(ui: &Rc<Ui>, state: SyncState) {
|
|||||||
image.add_css_class(class);
|
image.add_css_class(class);
|
||||||
}
|
}
|
||||||
ui.locations.card.sign_in.set_visible(expired);
|
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_title.set_label(&title);
|
||||||
ui.status.status_detail.set_visible(detail.is_some());
|
ui.status.status_detail.set_visible(detail.is_some());
|
||||||
ui.status
|
ui.status
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ use ksni::{MenuItem, Status, ToolTip, Tray};
|
|||||||
use pdfs_core::auth;
|
use pdfs_core::auth;
|
||||||
use pdfs_core::config::AppDirs;
|
use pdfs_core::config::AppDirs;
|
||||||
use pdfs_core::control::{
|
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;
|
use pdfs_core::service;
|
||||||
|
|
||||||
@@ -86,6 +87,9 @@ struct DriveState {
|
|||||||
paused: bool,
|
paused: bool,
|
||||||
/// Queued changes that keep failing, for the "N issues" item.
|
/// Queued changes that keep failing, for the "N issues" item.
|
||||||
failing: u64,
|
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
|
/// 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(),
|
sync: String::new(),
|
||||||
paused: false,
|
paused: false,
|
||||||
failing: 0,
|
failing: 0,
|
||||||
|
outdated: false,
|
||||||
},
|
},
|
||||||
// No daemon: describe login state instead so the menu is still useful.
|
// No daemon: describe login state instead so the menu is still useful.
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -202,6 +207,7 @@ fn poll_state(socket: &Path, default_mountpoint: &Path) -> DriveState {
|
|||||||
sync: String::new(),
|
sync: String::new(),
|
||||||
paused: false,
|
paused: false,
|
||||||
failing: 0,
|
failing: 0,
|
||||||
|
outdated: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,6 +224,7 @@ fn unexpected_reply(default_mountpoint: &Path) -> DriveState {
|
|||||||
sync: String::new(),
|
sync: String::new(),
|
||||||
paused: false,
|
paused: false,
|
||||||
failing: 0,
|
failing: 0,
|
||||||
|
outdated: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +246,7 @@ fn status_state(
|
|||||||
paused,
|
paused,
|
||||||
session_expired,
|
session_expired,
|
||||||
tokens_unsaved,
|
tokens_unsaved,
|
||||||
|
version,
|
||||||
..
|
..
|
||||||
} = status
|
} = status
|
||||||
else {
|
else {
|
||||||
@@ -246,9 +254,10 @@ fn status_state(
|
|||||||
};
|
};
|
||||||
let (pinned, online, failing_ops, paused) = (*pinned, *online, *failing_ops, *paused);
|
let (pinned, online, failing_ops, paused) = (*pinned, *online, *failing_ops, *paused);
|
||||||
let queued = i18n::pending_summary(*pending_uploads, *pending_changes);
|
let queued = i18n::pending_summary(*pending_uploads, *pending_changes);
|
||||||
|
let outdated = daemon_outdated(version);
|
||||||
let phase = if *session_expired {
|
let phase = if *session_expired {
|
||||||
Phase::SessionExpired
|
Phase::SessionExpired
|
||||||
} else if *tokens_unsaved {
|
} else if *tokens_unsaved || outdated {
|
||||||
Phase::Attention
|
Phase::Attention
|
||||||
} else {
|
} else {
|
||||||
phase_of(
|
phase_of(
|
||||||
@@ -265,6 +274,9 @@ fn status_state(
|
|||||||
_ if *session_expired => gettext("Session expired — sign in again"),
|
_ if *session_expired => gettext("Session expired — sign in again"),
|
||||||
// Sync goes on meanwhile, but a reboot ends it until the next login.
|
// Sync goes on meanwhile, but a reboot ends it until the next login.
|
||||||
_ if *tokens_unsaved => gettext("Sign-in not saved — unlock your keyring"),
|
_ 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 paused => gettext("Sync paused"),
|
||||||
_ if failing_ops > 0 => ngettext_f(
|
_ if failing_ops > 0 => ngettext_f(
|
||||||
"{n} change needs attention",
|
"{n} change needs attention",
|
||||||
@@ -302,6 +314,7 @@ fn status_state(
|
|||||||
mountpoint: PathBuf::from(mountpoint),
|
mountpoint: PathBuf::from(mountpoint),
|
||||||
sync: sync_line(items, jobs),
|
sync: sync_line(items, jobs),
|
||||||
failing: failing_ops,
|
failing: failing_ops,
|
||||||
|
outdated,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,6 +458,13 @@ impl Tray for DriveTray {
|
|||||||
_ => items.push(item(&gettext("Open Proton Drive"), |_| open_manager(&[]))),
|
_ => 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 {
|
if self.state.mounted {
|
||||||
items.push(
|
items.push(
|
||||||
StandardItem {
|
StandardItem {
|
||||||
@@ -626,9 +646,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn status(extra: &str) -> Response {
|
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!(
|
serde_json::from_str(&format!(
|
||||||
r#"{{"Status":{{"username":"u","mountpoint":"/m","pinned":0,"used":0,"budget":0,
|
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()
|
.unwrap()
|
||||||
}
|
}
|
||||||
@@ -660,6 +685,16 @@ mod tests {
|
|||||||
assert_eq!(state.line, "Sign-in not saved — unlock your keyring");
|
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]
|
#[test]
|
||||||
fn a_daemon_that_does_not_say_has_a_live_session() {
|
fn a_daemon_that_does_not_say_has_a_live_session() {
|
||||||
let state = status_state(&status(""), &[], &[], Path::new("/m"));
|
let state = status_state(&status(""), &[], &[], Path::new("/m"));
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ a live account since.
|
|||||||
|
|
||||||
| Entry | Title |
|
| 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` |
|
| [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 |
|
| [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 |
|
| [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.
|
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`
|
### B204 — A write over a partial edit whose base was never read fails with `EIO`
|
||||||
|
|
||||||
**Status:** Fixed, not verified live
|
**Status:** Fixed, not verified live
|
||||||
|
|||||||
+41
-2
@@ -11,12 +11,32 @@ scratch (user data in `staging/` and `recovery/` is never touched by this).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [3.3.0] — 2026-10-11
|
||||||
|
|
||||||
|
Schema: **42**, unchanged.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **A Nix flake for NixOS.** `nix build github:narrrl/proton-drive-linux` builds the client, and
|
- **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. See
|
the flake's NixOS module installs it and runs the mount in every graphical session. Pin the
|
||||||
[Install → NixOS](INSTALL.md#nixos-flake).
|
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))
|
([#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
|
### Fixed
|
||||||
- **Writing to a file again soon after a partial write could fail with an I/O error.** When the
|
- **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
|
first write changed only part of a file this client had not read, a second write, or saving
|
||||||
@@ -26,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
|
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.
|
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))
|
([#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
|
## [3.2.0] — 2026-10-08
|
||||||
|
|
||||||
@@ -1722,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
|
- The outstanding FUSE defects tracked in `docs/BUGS.md`, plus a truncate defect, validated
|
||||||
by a new POSIX compliance suite for the filesystem.
|
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.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
|
[3.1.2]: https://github.com/narrrl/proton-drive-linux/releases/tag/v3.1.2
|
||||||
|
|||||||
+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
|
1. checks that the tag, the workspace version in `Cargo.toml` and `pkgver` in
|
||||||
`packaging/PKGBUILD` agree;
|
`packaging/PKGBUILD` agree;
|
||||||
2. runs the quality gates;
|
2. runs the quality gates;
|
||||||
3. builds the release binaries and packages a `.tar.gz`, a `.deb` and a GPG-signed `.rpm`;
|
3. builds the release binaries and packages a `.tar.gz`, a `.deb` and a GPG-signed `.rpm`, and
|
||||||
4. publishes them as a GitHub release;
|
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
|
5. calls `.github/workflows/aur.yml`, which updates and pushes the AUR recipes and commits them
|
||||||
back to `main`.
|
back to `main`.
|
||||||
|
|
||||||
To prepare a release:
|
To prepare a release:
|
||||||
|
|
||||||
```bash
|
```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
|
cargo update --workspace
|
||||||
# 2. Move the [Unreleased] notes in docs/CHANGELOG.md under the new version.
|
# 2. Move the [Unreleased] notes in docs/CHANGELOG.md under the new version.
|
||||||
# 3. Commit, tag and push.
|
# 3. Commit, tag and push.
|
||||||
|
|||||||
@@ -103,6 +103,15 @@ GNOME Keyring for you:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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:
|
To try it without changing your system, run the app straight from the flake:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
# Build + install: cd packaging && makepkg -fi
|
# Build + install: cd packaging && makepkg -fi
|
||||||
|
|
||||||
pkgname=proton-drive-linux
|
pkgname=proton-drive-linux
|
||||||
pkgver=3.2.0
|
pkgver=3.3.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Unofficial Proton Drive client for Linux: files-on-demand FUSE mount, CLI, GTK4 app + tray"
|
pkgdesc="Unofficial Proton Drive client for Linux: files-on-demand FUSE mount, CLI, GTK4 app + tray"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pkgbase = proton-drive-for-linux-bin
|
pkgbase = proton-drive-for-linux-bin
|
||||||
pkgdesc = Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)
|
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
|
pkgrel = 1
|
||||||
url = https://proton-drive.narl.io
|
url = https://proton-drive.narl.io
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
@@ -21,21 +21,21 @@ pkgbase = proton-drive-for-linux-bin
|
|||||||
optdepends = xdg-utils: "Open folder" action in the tray menu
|
optdepends = xdg-utils: "Open folder" action in the tray menu
|
||||||
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
|
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
|
||||||
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
|
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
|
||||||
provides = proton-drive-for-linux=3.2.0
|
provides = proton-drive-for-linux=3.2.1
|
||||||
provides = proton-drive-linux=3.2.0
|
provides = proton-drive-linux=3.2.1
|
||||||
provides = pdfs
|
provides = pdfs
|
||||||
conflicts = proton-drive-linux
|
conflicts = proton-drive-linux
|
||||||
conflicts = proton-drive-for-linux
|
conflicts = proton-drive-for-linux
|
||||||
conflicts = proton-drive-for-linux-git
|
conflicts = proton-drive-for-linux-git
|
||||||
options = !strip
|
options = !strip
|
||||||
options = !debug
|
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 = 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.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-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.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-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.0.svg::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.0/packaging/io.narl.proton-drive-linux.svg
|
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.0.service::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.0/packaging/proton-drive.service
|
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.0::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.0/LICENSE
|
source = LICENSE-3.2.1::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.2.1/LICENSE
|
||||||
sha256sums = 3d498c9ea366c94e77e6f3c81b804b8b3497faecfa74480274d6ec9b5121665b
|
sha256sums = b031f15296a1947d4083f944ad0cd12a18e676cdb9746055d7673b3b3832c850
|
||||||
sha256sums = d190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123
|
sha256sums = d190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123
|
||||||
sha256sums = 1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0
|
sha256sums = 1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0
|
||||||
sha256sums = e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828
|
sha256sums = e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Nils Pukropp <contact@narl.io>
|
# Maintainer: Nils Pukropp <contact@narl.io>
|
||||||
|
|
||||||
pkgname=proton-drive-for-linux-bin
|
pkgname=proton-drive-for-linux-bin
|
||||||
pkgver=3.2.0
|
pkgver=3.2.1
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)"
|
pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)"
|
||||||
arch=('x86_64')
|
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"
|
"io.narl.proton-drive-linux-$pkgver.svg::$_raw/packaging/io.narl.proton-drive-linux.svg"
|
||||||
"proton-drive-$pkgver.service::$_raw/packaging/proton-drive.service"
|
"proton-drive-$pkgver.service::$_raw/packaging/proton-drive.service"
|
||||||
"LICENSE-$pkgver::$_raw/LICENSE")
|
"LICENSE-$pkgver::$_raw/LICENSE")
|
||||||
sha256sums=('3d498c9ea366c94e77e6f3c81b804b8b3497faecfa74480274d6ec9b5121665b'
|
sha256sums=('b031f15296a1947d4083f944ad0cd12a18e676cdb9746055d7673b3b3832c850'
|
||||||
'd190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123'
|
'd190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123'
|
||||||
'1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0'
|
'1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0'
|
||||||
'e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828'
|
'e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pkgbase = proton-drive-for-linux-git
|
pkgbase = proton-drive-for-linux-git
|
||||||
pkgdesc = Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (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
|
pkgrel = 1
|
||||||
url = https://proton-drive.narl.io
|
url = https://proton-drive.narl.io
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
@@ -24,8 +24,8 @@ pkgbase = proton-drive-for-linux-git
|
|||||||
optdepends = xdg-utils: "Open folder" action in the tray menu
|
optdepends = xdg-utils: "Open folder" action in the tray menu
|
||||||
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
|
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
|
||||||
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
|
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-for-linux=3.2.1.r0.g170c3f0
|
||||||
provides = proton-drive-linux=3.2.0.r0.g4c498d5
|
provides = proton-drive-linux=3.2.1.r0.g170c3f0
|
||||||
provides = pdfs
|
provides = pdfs
|
||||||
conflicts = proton-drive-linux
|
conflicts = proton-drive-linux
|
||||||
conflicts = proton-drive-for-linux
|
conflicts = proton-drive-for-linux
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Nils Pukropp <contact@narl.io>
|
# Maintainer: Nils Pukropp <contact@narl.io>
|
||||||
|
|
||||||
pkgname=proton-drive-for-linux-git
|
pkgname=proton-drive-for-linux-git
|
||||||
pkgver=3.2.0.r0.g4c498d5
|
pkgver=3.2.1.r0.g170c3f0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git)"
|
pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git)"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pkgbase = proton-drive-for-linux
|
pkgbase = proton-drive-for-linux
|
||||||
pkgdesc = Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray
|
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
|
pkgrel = 1
|
||||||
url = https://proton-drive.narl.io
|
url = https://proton-drive.narl.io
|
||||||
arch = x86_64
|
arch = x86_64
|
||||||
@@ -23,13 +23,13 @@ pkgbase = proton-drive-for-linux
|
|||||||
optdepends = xdg-utils: "Open folder" action in the tray menu
|
optdepends = xdg-utils: "Open folder" action in the tray menu
|
||||||
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
|
optdepends = fzf: search-as-you-type launcher (pdfs-prompt --fzf)
|
||||||
optdepends = foot: terminal that hosts pdfs-prompt --fzf when started from a keybinding
|
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
|
provides = pdfs
|
||||||
conflicts = proton-drive-linux
|
conflicts = proton-drive-linux
|
||||||
conflicts = proton-drive-for-linux-bin
|
conflicts = proton-drive-for-linux-bin
|
||||||
conflicts = proton-drive-for-linux-git
|
conflicts = proton-drive-for-linux-git
|
||||||
options = !lto
|
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
|
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 = a15ddb37808dc01bde3a3d092b03b5047cdecca37efd14d906c9bf353d242bd8
|
sha256sums = 275fefa2feacbd8319b3986205433729d6f5241b5bedfa4651b47bea9700b525
|
||||||
|
|
||||||
pkgname = proton-drive-for-linux
|
pkgname = proton-drive-for-linux
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Nils Pukropp <contact@narl.io>
|
# Maintainer: Nils Pukropp <contact@narl.io>
|
||||||
|
|
||||||
pkgname=proton-drive-for-linux
|
pkgname=proton-drive-for-linux
|
||||||
pkgver=3.2.0
|
pkgver=3.2.1
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray"
|
pkgdesc="Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray"
|
||||||
arch=('x86_64')
|
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.
|
# LTO has broken GTK/Rust links for this workspace; the release profile keeps it off.
|
||||||
options=('!lto')
|
options=('!lto')
|
||||||
source=("$pkgname-$pkgver.tar.gz::$_repo/archive/refs/tags/v$pkgver.tar.gz")
|
source=("$pkgname-$pkgver.tar.gz::$_repo/archive/refs/tags/v$pkgver.tar.gz")
|
||||||
sha256sums=('a15ddb37808dc01bde3a3d092b03b5047cdecca37efd14d906c9bf353d242bd8')
|
sha256sums=('275fefa2feacbd8319b3986205433729d6f5241b5bedfa4651b47bea9700b525')
|
||||||
|
|
||||||
_srcname="proton-drive-linux-$pkgver"
|
_srcname="proton-drive-linux-$pkgver"
|
||||||
|
|
||||||
|
|||||||
+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