Compare commits

..
Author SHA1 Message Date
nvrl ad648ab5cc feat(cli,core): shell completions with live values from the service
`pdfs completions <shell>` prints a completion script for bash, zsh, fish,
elvish or PowerShell, built on clap_complete's dynamic engine: on Tab the
shell calls `pdfs` back, which completes commands and options and also the
values the running service knows, each with a description: paths in My
files, synced-folder and queue ids, computers, trash, albums, items shared
with me, invitations, bookmarks, conflict copies, and a file's versions or
a share's members and public link once its path is typed.

A completer that gets no answer from the service within two seconds offers
nothing rather than hanging the prompt. Listings that ask Proton are kept
for 30 s in $XDG_RUNTIME_DIR (0600), so repeated Tabs do not wait again.
Completion is triggered by PDFS_COMPLETE rather than clap's generic
COMPLETE, because the same binary runs the daemon.

Share roles are now checked by the CLI before the request. The package
recipes install the bash, zsh and fish scripts via packaging/completions.sh.
2026-11-12 11:47:29 +01:00
nvrl 2b95708260 chore(release): 3.8.0 2026-11-12 10:22:04 +01:00
16 changed files with 61 additions and 153 deletions

No files matched your search

Generated
+8 -10
View File
@@ -3541,7 +3541,7 @@ checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
[[package]] [[package]]
name = "pdfs-cli" name = "pdfs-cli"
version = "3.7.0" version = "3.8.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -3558,11 +3558,10 @@ dependencies = [
[[package]] [[package]]
name = "pdfs-core" name = "pdfs-core"
version = "3.7.0" version = "3.8.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.23.1", "base64 0.23.1",
"chrono",
"dbus-secret-service-keyring-store", "dbus-secret-service-keyring-store",
"directories", "directories",
"ignore", "ignore",
@@ -3586,7 +3585,7 @@ dependencies = [
[[package]] [[package]]
name = "pdfs-fuse" name = "pdfs-fuse"
version = "3.7.0" version = "3.8.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.23.1", "base64 0.23.1",
@@ -3608,7 +3607,7 @@ dependencies = [
[[package]] [[package]]
name = "pdfs-gui" name = "pdfs-gui"
version = "3.7.0" version = "3.8.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-channel", "async-channel",
@@ -3848,14 +3847,13 @@ dependencies = [
[[package]] [[package]]
name = "proton-drive-rs" name = "proton-drive-rs"
version = "0.11.0" version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14a22e621abbc1c61b4640728a7de969c16b66d5d983b59c638d501fda25a7b9" checksum = "a8fa2a88130883010b32035e208aaa36fb275f6d7d78ed44a1069b1d33d24481"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.23.1", "base64 0.23.1",
"bytes", "bytes",
"chrono",
"futures", "futures",
"getrandom 0.4.3", "getrandom 0.4.3",
"hex", "hex",
@@ -3876,9 +3874,9 @@ dependencies = [
[[package]] [[package]]
name = "proton-sdk" name = "proton-sdk"
version = "0.11.0" version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd55a5e8b6600f3aa282ebbdf13ff366431ff9aae84d4eb20b0ef06d598919fe" checksum = "af0bf8d50fa829d1a65b09c6855b606578d02734d4089874c971d1063c23c353"
dependencies = [ dependencies = [
"aes-gcm 0.11.1", "aes-gcm 0.11.1",
"async-trait", "async-trait",
+3 -4
View File
@@ -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.7.0" version = "3.8.0"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
authors = ["Nils Pukropp <contact@narl.io>"] authors = ["Nils Pukropp <contact@narl.io>"]
@@ -15,9 +15,8 @@ keywords = ["proton", "drive", "fuse", "sync", "linux"]
categories = ["filesystem", "command-line-utilities"] categories = ["filesystem", "command-line-utilities"]
[workspace.dependencies] [workspace.dependencies]
proton-sdk = { version = "0.11.0" } proton-sdk = { version = "0.10.1" }
proton-drive-rs = { version = "0.11.0" } proton-drive-rs = { version = "0.10.1" }
chrono = { version = "0.4", default-features = false, features = ["std"] }
# Internal crates. # Internal crates.
pdfs-core = { path = "crates/pdfs-core" } pdfs-core = { path = "crates/pdfs-core" }
-1
View File
@@ -12,7 +12,6 @@ description = "Core auth/session/config for the Proton Drive Linux client"
[dependencies] [dependencies]
proton-sdk.workspace = true proton-sdk.workspace = true
proton-drive-rs.workspace = true proton-drive-rs.workspace = true
chrono.workspace = true
tokio.workspace = true tokio.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
-1
View File
@@ -29,7 +29,6 @@ pub mod similar;
pub mod syncignore; pub mod syncignore;
pub mod syncroot; pub mod syncroot;
pub mod takeout; pub mod takeout;
pub mod timestamp;
pub mod tray; pub mod tray;
pub mod update; pub mod update;
-33
View File
@@ -1,33 +0,0 @@
//! Convert the client's stored epoch seconds at the SDK boundary.
use proton_drive_rs::ModificationTime;
/// A UTC modification time, or `None` when outside the SDK's supported range.
pub fn modification_time(seconds: i64) -> Option<ModificationTime> {
chrono::DateTime::from_timestamp(seconds, 0).map(|time| time.fixed_offset())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn epoch_seconds_preserve_the_instant_including_before_1970() {
for seconds in [-1, 0, 1_709_208_000, 1_782_482_460] {
let time = modification_time(seconds).unwrap();
assert_eq!(time.timestamp(), seconds);
assert_eq!(time.offset().local_minus_utc(), 0);
assert_eq!(time.timestamp_subsec_nanos(), 0);
}
assert_eq!(
modification_time(-1).unwrap().to_rfc3339(),
"1969-12-31T23:59:59+00:00"
);
}
#[test]
fn out_of_range_seconds_omit_the_timestamp() {
assert!(modification_time(i64::MIN).is_none());
assert!(modification_time(i64::MAX).is_none());
}
}
-2
View File
@@ -939,8 +939,6 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
}); });
let metadata = proton_drive_rs::PhotoUploadMetadata { let metadata = proton_drive_rs::PhotoUploadMetadata {
capture_time, capture_time,
last_modification_time: capture_time
.and_then(pdfs_core::timestamp::modification_time),
..Default::default() ..Default::default()
}; };
let thumbnails = core.upload_thumbnails(Path::new(&source_path), &name); let thumbnails = core.upload_thumbnails(Path::new(&source_path), &name);
+4 -10
View File
@@ -352,13 +352,7 @@ impl DriveApi for ProtonDriveClient {
name: &str, name: &str,
last_modification_time: Option<i64>, last_modification_time: Option<i64>,
) -> Result<NodeUid> { ) -> Result<NodeUid> {
ProtonDriveClient::create_folder( ProtonDriveClient::create_folder(self, parent_uid, name, last_modification_time).await
self,
parent_uid,
name,
last_modification_time.and_then(pdfs_core::timestamp::modification_time),
)
.await
} }
async fn upload_file( async fn upload_file(
@@ -390,7 +384,7 @@ impl DriveApi for ProtonDriveClient {
reader, reader,
intended_size, intended_size,
thumbnails, thumbnails,
last_modification_time.and_then(pdfs_core::timestamp::modification_time), last_modification_time,
aead, aead,
) )
.await .await
@@ -415,7 +409,7 @@ impl DriveApi for ProtonDriveClient {
reader, reader,
intended_size, intended_size,
thumbnails, thumbnails,
last_modification_time.and_then(pdfs_core::timestamp::modification_time), last_modification_time,
aead, aead,
) )
.await .await
@@ -435,7 +429,7 @@ impl DriveApi for ProtonDriveClient {
reader, reader,
intended_size, intended_size,
thumbnails, thumbnails,
last_modification_time.and_then(pdfs_core::timestamp::modification_time), last_modification_time,
) )
.await .await
} }
-1
View File
@@ -200,7 +200,6 @@ impl Core {
let reader = CountingReader::new(std::io::BufReader::new(file), &guard); let reader = CountingReader::new(std::io::BufReader::new(file), &guard);
let metadata = PhotoUploadMetadata { let metadata = PhotoUploadMetadata {
capture_time: Some(candidate.wanted), capture_time: Some(candidate.wanted),
last_modification_time: pdfs_core::timestamp::modification_time(candidate.wanted),
..Default::default() ..Default::default()
}; };
let uploaded = self.rt.block_on(photos.upload_photo_from( let uploaded = self.rt.block_on(photos.upload_photo_from(
+3 -9
View File
@@ -344,9 +344,6 @@ impl Core {
); );
let metadata = PhotoUploadMetadata { let metadata = PhotoUploadMetadata {
capture_time: photo.capture_time, capture_time: photo.capture_time,
last_modification_time: photo
.capture_time
.and_then(pdfs_core::timestamp::modification_time),
..Default::default() ..Default::default()
}; };
// A thumbnail is made from a file, so a photo that can have one // A thumbnail is made from a file, so a photo that can have one
@@ -368,13 +365,10 @@ impl Core {
let thumbnails = self.upload_thumbnails(path, &photo.name); let thumbnails = self.upload_thumbnails(path, &photo.name);
// No sidecar and no date in the name: the photo's own // No sidecar and no date in the name: the photo's own
// EXIF, before Drive files it under today. // EXIF, before Drive files it under today.
let capture_time = metadata
.capture_time
.or_else(|| pdfs_core::exiftime::capture_time_of(path));
let metadata = PhotoUploadMetadata { let metadata = PhotoUploadMetadata {
capture_time, capture_time: metadata
last_modification_time: capture_time .capture_time
.and_then(pdfs_core::timestamp::modification_time), .or_else(|| pdfs_core::exiftime::capture_time_of(path)),
..metadata ..metadata
}; };
std::fs::File::open(path) std::fs::File::open(path)
+38 -17
View File
@@ -11,27 +11,48 @@ scratch (user data in `staging/` and `recovery/` is never touched by this).
## [Unreleased] ## [Unreleased]
### Changed ## [3.8.0] — 2026-11-12
- Update `proton-sdk` and `proton-drive-rs` to 0.11.0. Schema: **45**. Synced folders now tell device backups apart from folders attached to My files.
Convert epoch timestamps at the SDK boundary and explicitly preserve photo modification Existing folders stay device backups. Older builds cannot open the upgraded database.
times now that they are independent of capture times. See [the SDK review](SDK-0.11.0.md)
for features and breaking changes.
Schema: **45**. Synced folders now distinguish device backups from My files attachments.
Existing folders remain device backups; older builds cannot open the upgraded database.
### Added ### Added
- Shell completion for `pdfs` in bash, zsh and fish, installed by every package, plus elvish - **Sync a local folder with a folder you already have in My files** (#67, #77).
and PowerShell through `pdfs completions SHELL`. Besides commands and options it completes `pdfs sync attach ~/Work --to Work` keeps `~/Work` an ordinary local folder and syncs it both
paths in My files and the ids and uids the running service knows (synced folders, the queue, ways with My files/Work. The first sync uploads files that are only on this computer, downloads
computers, trash, albums, shares, invitations, bookmarks, conflicts and versions), each with files that are only on Proton Drive, and keeps both versions of a file that differs: the one on
a description. Share roles are now checked by the CLI (`viewer`, `editor`, `admin`) before Proton Drive keeps its name, and the local one becomes a conflict copy next to it and is
the request. uploaded too. Attached folders are always kept on this computer, never online only. They are
- `pdfs sync attach PATH --to REMOTE` connects an existing local directory to an existing listed apart from device backups and are not part of the device profile used to restore another
My files folder. The first sync preserves one-sided files and differing versions (#67). computer. Stopping one keeps both copies, and `--delete-remote` is refused for it. If the folder
Attachments stay in mirror mode, are excluded from device profiles, and cannot delete on Proton Drive is trashed or moved out of My files, syncing stops with an error and the local
the remote root when syncing is stopped. files stay. Attaching a folder is done from a terminal for now.
- **Tab completion for `pdfs`** in bash, zsh and fish, installed by every package. Besides
commands and options it completes paths in My files (`pdfs pin Doc<Tab>`) and what the running
service knows, each with a description: synced folders, queued changes, computers, trash, albums,
items shared with you, invitations, bookmarks and conflict copies, and once a path is typed, the
file's versions or the share's members and link. Without the service it still completes commands
and options. Elvish and PowerShell load it from `pdfs completions SHELL`, and so does a source
build; the [command-line reference](CLI.md#shell-completion) has the line for each shell.
`pdfs share` and `pdfs share-role` now check the role (`viewer`, `editor` or `admin`) before
sending the request, and `pdfs public-link` does the same for `viewer` and `editor`.
### Fixed
- **My files comes back after the service was killed for hanging** (B279). A killed service left
its folders behind as dead mounts, and every restart then failed with `File exists` and retried
forever without opening anything, until the mounts were cleared by hand. The service now clears
a dead mount before it opens the folder. On a large account a restart that did open My files was
killed again, because loading the account took longer than the 2 minutes the service has to
report in. Startup now gets 10 minutes, and the 2 minutes apply again once the service answers.
- **Uploading many large photos no longer runs the computer out of memory** (B280). Every image
upload decodes the whole picture to make its thumbnail, and up to 16 did so at once. A batch of
100-megapixel photos took about 9 GiB, filled the swap, and the service was killed on every
restart. Thumbnails now share 512 MiB between them: phone photos are still made side by side,
and a very large photo waits and is made on its own.
## [3.7.0] — 2026-11-02 ## [3.7.0] — 2026-11-02
+1 -1
View File
@@ -102,7 +102,7 @@ That URL follows `main`. To stay on releases, pin the input to a release tag ins
the tag to update: the tag to update:
```nix ```nix
inputs.proton-drive-linux.url = "github:narrrl/proton-drive-linux/v3.7.0"; inputs.proton-drive-linux.url = "github:narrrl/proton-drive-linux/v3.8.0";
``` ```
Each release tag is built with Nix before the release is published. Each release tag is built with Nix before the release is published.
-60
View File
@@ -1,60 +0,0 @@
# SDK 0.11.0 review
Reviewed the sibling `../proton-sdk-rs` checkout at `84039a1` against 0.10.1
(`48dd410`), including its changelog and source diff.
## Features
- Folder creation, streaming uploads, draft replacement, and revision uploads
preserve the supplied timestamp's UTC offset and fractional seconds.
- `ModificationTime` is exported as an alias for
`chrono::DateTime<chrono::FixedOffset>`.
- Photo modification time can be set independently of capture time. Downloaded
claimed modification timestamps remain verbatim strings.
- Neither SDK crate defines Cargo feature flags. No feature opt-in is needed.
The Drive crate now depends directly on `chrono` 0.4; the SDK already used it.
The minimum Rust version remains 1.96, matching this workspace.
## Breaking changes and adaptation
The modification-time argument changes from `Option<i64>` to
`Option<ModificationTime>` on `create_folder`, `upload_file_from`,
`upload_file_replacing_draft_from`, `upload_new_revision`, and
`upload_new_revision_from`. `None` remains valid.
The real `DriveApi` adapter converts the client's stored epoch seconds to UTC
through `pdfs_core::timestamp::modification_time`. Internal storage, the trait,
and the simulated Drive continue using epoch seconds. Out-of-range timestamps
are omitted rather than panicking. Existing upload paths still supply whole
seconds; this bump alone does not add subsecond storage or offset tracking.
`PhotoUploadMetadata` gains `last_modification_time`, and the SDK no longer
copies `capture_time` into it. Our literals already use `Default`, so this would
compile while silently changing uploaded metadata. Control uploads, Takeout
imports (including the EXIF fallback), and redating now set both explicitly to
preserve the previous behavior when a capture time is known. Without a known
capture time, modification time stays omitted as before.
No other public API changes were found in the reviewed diff. No database or
recovery migration is required by this SDK bump.
## Dependencies
The workspace requires the published 0.11.0 versions of both crates from
crates.io. No local patches or sibling checkout are needed to build it.
## Verification
The published crates' source files match the reviewed sibling checkout at
`84039a1`. On the final branch based on `main`, these checks passed:
```sh
cargo test --workspace --locked
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --locked -- -D warnings
```
The test suite reported 1,135 passed and 67 ignored; ignored tests were not run.
Tests ran outside the sandbox because control-socket tests require socket
permissions. No live account or mounted-filesystem acceptance testing was
performed.
+1 -1
View File
@@ -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.7.0 pkgver=3.8.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 -1
View File
@@ -10,7 +10,7 @@
# Match Arch PKGBUILD `!lto` — LTO has broken some GTK/Rust links in practice. # Match Arch PKGBUILD `!lto` — LTO has broken some GTK/Rust links in practice.
%global _lto_cflags %{nil} %global _lto_cflags %{nil}
%{!?version: %global version 3.7.0} %{!?version: %global version 3.8.0}
%global forge https://github.com/narrrl/proton-drive-linux %global forge https://github.com/narrrl/proton-drive-linux
# Files come from the checkout, or from the unpacked tag archive. # Files come from the checkout, or from the unpacked tag archive.
%global src %{?git_dir}%{!?git_dir:.} %global src %{?git_dir}%{!?git_dir:.}
+1 -1
View File
@@ -6,7 +6,7 @@
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: proton-drive-linux 3.7.0\n" "Project-Id-Version: proton-drive-linux 3.8.0\n"
"Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n"
"POT-Creation-Date: 2026-11-01 17:51+0100\n" "POT-Creation-Date: 2026-11-01 17:51+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+1 -1
View File
@@ -91,7 +91,7 @@ SECTIONS = [
] ]
# Too long or too internal for the wiki; links to them go to GitHub. # Too long or too internal for the wiki; links to them go to GitHub.
EXCLUDED = {"BUGS.md", "CHANGELOG.md", "SDK-0.11.0.md"} EXCLUDED = {"BUGS.md", "CHANGELOG.md"}
# Files outside docs/ whose links are checked but not published. # Files outside docs/ whose links are checked but not published.
CHECKED = ["README.md", "CONTRIBUTING.md", "SECURITY.md"] CHECKED = ["README.md", "CONTRIBUTING.md", "SECURITY.md"]