diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 6140d48..0d9cf21 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,6 +3,9 @@ contact_links: - name: Security vulnerability url: https://github.com/narrrl/proton-drive-linux/security/advisories/new about: Report privately — never in a public issue. + - name: Troubleshooting and FAQ + url: https://github.com/narrrl/proton-drive-linux/wiki/Troubleshooting + about: Fixes for common problems, and what to include in a bug report. - name: Known issues url: https://github.com/narrrl/proton-drive-linux/blob/main/docs/BUGS.md about: The authoritative ledger of tracked bugs and their status. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e5b6771..a2d39c1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -21,6 +21,7 @@ cargo test --workspace --locked - [ ] Touches recovery, staging, or anything that can be the only copy of user data - [ ] Changes packaging, the systemd unit, or desktop entries - [ ] Changes the control protocol (daemon and front ends updated together) +- [ ] Adds or renames a page in `docs/` (listed in `SECTIONS` in `scripts/publish-wiki.sh`, which publishes it to the wiki) - [ ] None of the above diff --git a/.github/workflows/copr.yml b/.github/workflows/copr.yml new file mode 100644 index 0000000..be7648e --- /dev/null +++ b/.github/workflows/copr.yml @@ -0,0 +1,66 @@ +name: COPR + +on: + # Called by release.yml once the GitHub release and its assets exist: COPR + # downloads the tag archive and the vendored crates from the release. + workflow_call: + inputs: + version: + required: true + type: string + secrets: + COPR_CONFIG: + required: true + workflow_dispatch: + inputs: + version: + description: 'Release to build, such as 3.5.0. It must already have its vendor tarball.' + required: true + type: string + +permissions: + contents: read + +concurrency: + group: copr + cancel-in-progress: false + +jobs: + build: + name: Build in COPR + runs-on: ubuntu-24.04 + container: fedora:latest + env: + VERSION: ${{ inputs.version }} + COPR_PROJECT: narl-rs/proton-drive-linux + + steps: + - name: Install packaging tools + run: dnf -y install copr-cli rpm-build rpmdevtools + + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Configure COPR access + env: + COPR_CONFIG: ${{ secrets.COPR_CONFIG }} + run: | + install -d -m700 ~/.config + printf '%s\n' "$COPR_CONFIG" > ~/.config/copr + chmod 600 ~/.config/copr + + # COPR rebuilds the source package without our --define, so the version + # has to be written into the spec itself. + - name: Build the source package + run: | + version="${VERSION#v}" + spec=packaging/proton-drive-linux.spec + sed -i -E "s/^(%\{!\?version: %global version )[^}]*\}/\1${version}}/" "$spec" + grep -qF "%global version ${version}}" "$spec" + mkdir -p rpmbuild/SOURCES + spectool -g -C rpmbuild/SOURCES "$spec" + rpmbuild -bs --define "_topdir $PWD/rpmbuild" "$spec" + + # Waits for the build, so a failed COPR build fails this job. + - name: Submit to COPR + run: copr-cli build "$COPR_PROJECT" rpmbuild/SRPMS/*.src.rpm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b6d542..4942d99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,8 +52,10 @@ jobs: tag_version="${GITHUB_REF_NAME#v}" manifest_version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1)" pkgbuild_version="$(sed -n 's/^pkgver=//p' packaging/PKGBUILD | head -n1)" + spec_version="$(sed -n 's/^%{!?version: %global version \([^}]*\)}/\1/p' packaging/proton-drive-linux.spec)" test "$tag_version" = "$manifest_version" test "$tag_version" = "$pkgbuild_version" + test "$tag_version" = "$spec_version" - name: Release quality gates run: | @@ -129,6 +131,12 @@ jobs: # Create tarball tar -czf proton-drive-linux-${{ env.VERSION }}-x86_64.tar.gz -C dist/binaries . + # Every crate the build needs, so COPR can build the spec without network. + - name: Package vendored crates (.tar.xz) + run: | + cargo vendor --locked dist/vendor > /dev/null + tar -cJf proton-drive-linux-vendor-${{ env.VERSION }}.tar.xz -C dist vendor + - name: Package Debian package (.deb) run: | mkdir -p deb/DEBIAN @@ -230,6 +238,7 @@ jobs: path: | *.deb *.tar.gz + *.tar.xz *.rpm if-no-files-found: error @@ -297,6 +306,7 @@ jobs: files: | *.deb *.tar.gz + *.tar.xz *.rpm draft: false prerelease: false @@ -312,3 +322,21 @@ jobs: version: ${{ github.ref_name }} secrets: AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + + copr: + name: Build COPR packages + needs: publish + if: startsWith(github.ref, 'refs/tags/') + uses: ./.github/workflows/copr.yml + with: + version: ${{ github.ref_name }} + secrets: + COPR_CONFIG: ${{ secrets.COPR_CONFIG }} + + wiki: + name: Publish the wiki + needs: publish + if: startsWith(github.ref, 'refs/tags/') + uses: ./.github/workflows/wiki.yml + with: + ref: ${{ github.ref_name }} diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml new file mode 100644 index 0000000..ace5612 --- /dev/null +++ b/.github/workflows/wiki.yml @@ -0,0 +1,77 @@ +name: Wiki + +# docs/ is the source of the GitHub wiki (docs/DEVELOPMENT.md#common-changes). +# Every change to the documentation checks its links; release.yml calls this +# once the release is published, so the wiki documents the latest release. Run +# it from the Actions tab to publish main in between. +on: + push: + branches: [main] + paths: + - 'docs/**' + - 'images/**' + - 'README.md' + - 'CONTRIBUTING.md' + - 'SECURITY.md' + - 'scripts/publish-wiki.sh' + - '.github/workflows/wiki.yml' + pull_request: + paths: + - 'docs/**' + - 'images/**' + - 'README.md' + - 'CONTRIBUTING.md' + - 'SECURITY.md' + - 'scripts/publish-wiki.sh' + - '.github/workflows/wiki.yml' + workflow_call: + inputs: + ref: + description: 'Tag or branch to publish' + required: true + type: string + workflow_dispatch: + inputs: + ref: + description: 'Tag or branch to publish' + required: false + default: 'main' + type: string + +permissions: + contents: read + +jobs: + check: + name: Check documentation links + if: inputs.ref == '' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + + - name: Check links + run: scripts/publish-wiki.sh --check "$RUNNER_TEMP" + + publish: + name: Publish to the wiki + # Set only by release.yml and the Actions tab, so pushes and pull requests + # never publish. + if: inputs.ref != '' + runs-on: ubuntu-24.04 + concurrency: + group: wiki-publish + cancel-in-progress: false + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + + - name: Publish + env: + WIKI_REMOTE: https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.wiki.git + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' + scripts/publish-wiki.sh --push "$RUNNER_TEMP" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b578671..0fac1e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,10 +6,10 @@ deliberately high — everything else is ordinary Rust review. ## Before you start -- Read [`AGENTS.md`](AGENTS.md) for repository conventions, - [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) for building, running and debugging from source, - and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for how the daemon, front ends, and SDK fit - together. [`docs/README.md`](docs/README.md) indexes the rest. +- Read [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) for building, running and debugging from + source, and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for how the daemon, front ends, and + SDK fit together. [`docs/README.md`](docs/README.md) indexes the rest. The same pages are on + the [wiki](https://github.com/narrrl/proton-drive-linux/wiki). - Check [`docs/BUGS.md`](docs/BUGS.md) — it is the authoritative issue ledger, and your problem may already be tracked there with context. - For anything larger than a fix, open an issue first so the design can be discussed before you @@ -58,8 +58,10 @@ runs are destructive: use a dedicated test account, never one holding data you c catalog in `po/LINGUAS` in the same change. See [`docs/TRANSLATING.md`](docs/TRANSLATING.md). The CLI and daemon messages stay in English. - **Document what users see.** A new command, setting or behaviour belongs in the matching page - under `docs/` (user guide, CLI reference or configuration) and in the `[Unreleased]` section of - [`docs/CHANGELOG.md`](docs/CHANGELOG.md). + under `docs/` (a user guide topic, the CLI reference or configuration) and in the `[Unreleased]` + section of [`docs/CHANGELOG.md`](docs/CHANGELOG.md). Each release publishes `docs/` to the + wiki. A new page goes into `SECTIONS` in `scripts/publish-wiki.sh`, and + `scripts/publish-wiki.sh --check` must pass. CI runs it on every change to `docs/`. ## Commits and pull requests diff --git a/README.md b/README.md index 04a592c..eecceac 100644 --- a/README.md +++ b/README.md @@ -73,16 +73,30 @@ scriptable CLI. | Distribution | Package | |---|---| -| 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) | +| Fedora 44 or newer | [`narl-rs/proton-drive-linux`](https://copr.fedorainfracloud.org/coprs/narl-rs/proton-drive-linux/) in COPR, updated with `dnf upgrade` | | 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) | +| Debian, Ubuntu 24.04 or newer | `.deb` from the [latest release](https://github.com/narrrl/proton-drive-linux/releases/latest) | +| NixOS | The flake in this repository: see [Installation → NixOS](docs/INSTALL.md#nixos-flake) | | Anything else | [Build from source](docs/INSTALL.md#build-from-source) | +**Fedora** + ```bash -sudo apt install ./proton-drive-linux_*.deb # Debian, Ubuntu -sudo dnf install ./proton-drive-linux-*.rpm # Fedora -paru -S proton-drive-for-linux # Arch +sudo dnf copr enable narl-rs/proton-drive-linux +sudo dnf install proton-drive-linux +``` + +**Arch Linux** + +```bash +paru -S proton-drive-for-linux +``` + +**Debian and Ubuntu:** download the `.deb` from the +[latest release](https://github.com/narrrl/proton-drive-linux/releases/latest), then: + +```bash +sudo apt install ./proton-drive-linux_*_amd64.deb ``` Then open **Proton Drive** from your applications menu and sign in. The app starts the background @@ -97,14 +111,20 @@ systemctl --user enable --now proton-drive.service pdfs status ``` -[docs/INSTALL.md](docs/INSTALL.md) has the requirements, optional extras (tray on GNOME, RAW and -video thumbnails), headless use and uninstalling. +New to the client? [Getting started](docs/GETTING_STARTED.md) walks through the first steps. +[Installation](docs/INSTALL.md) has the optional extras (tray on GNOME, RAW and video +thumbnails), updating, headless use and uninstalling. ## Documentation +The documentation is in [`docs/`](docs/README.md) and on the +[wiki](https://github.com/narrrl/proton-drive-linux/wiki). + | | | |---|---| +| [Getting started](docs/GETTING_STARTED.md) | From installing to your first synced folder | | [User guide](docs/USER_GUIDE.md) | Everything the app, tray and launcher do | +| [FAQ](docs/FAQ.md) | Short answers to common questions | | [Command-line reference](docs/CLI.md) | Every `pdfs` command and scripting with `--json` | | [Configuration](docs/CONFIGURATION.md) | `config.json`, file locations, environment variables, the service | | [Troubleshooting](docs/TROUBLESHOOTING.md) | Diagnosing problems and fixing common ones | @@ -151,7 +171,8 @@ keys before it leaves the machine, through a Rust implementation of Proton's Dri Files you open are decrypted into a local cache so programs can read them. What that means for the security of the machine is spelled out in the -[threat model](docs/ARCHITECTURE.md#10-threat-model-what-this-client-writes-to-disk-in-plaintext). +[security model](docs/SECURITY_MODEL.md#what-is-kept-on-this-computer), and in full detail in +the [threat model](docs/ARCHITECTURE.md#10-threat-model-what-this-client-writes-to-disk-in-plaintext). ## Contributing diff --git a/crates/pdfs-core/src/config.rs b/crates/pdfs-core/src/config.rs index 4206655..5de4454 100644 --- a/crates/pdfs-core/src/config.rs +++ b/crates/pdfs-core/src/config.rs @@ -190,6 +190,24 @@ pub struct AppConfig { /// the field. #[serde(default)] pub local_first: Option, + /// Ask GitHub once a day whether a newer release is out, and say so in the + /// desktop app. Off by default: GitHub then sees the address and version + /// asking. Never used where a package manager installs updates (see + /// [`crate::update::updates_managed`]). Defaulted for configs predating the + /// field. + #[serde(default)] + pub update_check: bool, + /// When the update check last asked GitHub, in Unix seconds, so a restart + /// does not ask again within [`crate::update::CHECK_INTERVAL`]. Defaulted + /// for configs predating the field. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub update_last_checked: Option, + /// The latest release that check found, such as `3.6.0`, so the app can + /// say so again after a restart without asking. Older than or equal to the + /// running version once the update is installed. Defaulted for configs + /// predating the field. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub update_latest: Option, } /// The My Files page's view choices, remembered between runs. @@ -753,6 +771,9 @@ mod tests { language: Some("de".to_string()), online_map: true, local_first: Some(false), + update_check: true, + update_last_checked: Some(1_760_000_000), + update_latest: Some("3.6.0".to_string()), }; let json = serde_json::to_string(&config).unwrap(); let decoded: AppConfig = serde_json::from_str(&json).unwrap(); @@ -764,6 +785,9 @@ mod tests { assert_eq!(decoded.proton_theme, Some(true)); assert!(decoded.tray_hidden); assert!(decoded.online_map); + assert!(decoded.update_check); + assert_eq!(decoded.update_last_checked, Some(1_760_000_000)); + assert_eq!(decoded.update_latest.as_deref(), Some("3.6.0")); assert_eq!(decoded.language.as_deref(), Some("de")); assert_eq!(decoded.upload_limit, Some(500_000)); assert_eq!(decoded.download_limit, None); diff --git a/crates/pdfs-core/src/lib.rs b/crates/pdfs-core/src/lib.rs index 7857db4..ed19c4f 100644 --- a/crates/pdfs-core/src/lib.rs +++ b/crates/pdfs-core/src/lib.rs @@ -28,6 +28,7 @@ pub mod syncignore; pub mod syncroot; pub mod takeout; pub mod tray; +pub mod update; pub use access::{Access, access_for, perm_bits}; pub use error::{CoreError, CoreResult, Error, Result}; diff --git a/crates/pdfs-core/src/update.rs b/crates/pdfs-core/src/update.rs new file mode 100644 index 0000000..5c0cd7a --- /dev/null +++ b/crates/pdfs-core/src/update.rs @@ -0,0 +1,178 @@ +//! The opt-in check for a newer release ([`crate::config::AppConfig::update_check`]). +//! +//! Only the decisions live here: what GitHub's answer says, which version is +//! newer, when to ask again, and whether a package manager already owns +//! updates. The desktop app does the request itself. + +use std::path::Path; +use std::time::Duration; + +/// GitHub's latest published release. Drafts and pre-releases never show up +/// here, so a tagged release candidate does not reach anyone. +pub const RELEASES_LATEST_API: &str = + "https://api.github.com/repos/narrrl/proton-drive-linux/releases/latest"; + +/// How often the app asks, across restarts. +pub const CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); + +/// Installed next to the binaries' `share/` by packages whose package manager +/// brings new releases (COPR, the AUR, Nix). The release `.deb`, `.rpm` and +/// `.tar.gz` do not ship it, since nothing updates them. +pub const MANAGED_MARKER: &str = "share/proton-drive-linux/managed-updates"; + +/// The version of the release in a `/releases/latest` answer, without the +/// tag's `v`. `None` for an answer that does not parse or a tag that is not +/// `X.Y.Z`, so a changed API can never claim an update. +pub fn latest_version(body: &[u8]) -> Option { + let json: serde_json::Value = serde_json::from_slice(body).ok()?; + let tag = json.get("tag_name")?.as_str()?; + let version = tag.strip_prefix('v').unwrap_or(tag); + parse(version).map(|_| version.to_owned()) +} + +/// Whether `version` is a later release than `current`. Anything that is not +/// `X.Y.Z` on either side is not. +pub fn is_newer(version: &str, current: &str) -> bool { + matches!((parse(version), parse(current)), (Some(v), Some(c)) if v > c) +} + +/// The release page of `version` on GitHub. +pub fn release_url(version: &str) -> String { + format!("https://github.com/narrrl/proton-drive-linux/releases/tag/v{version}") +} + +/// `X.Y.Z` as numbers, so `3.10.0` sorts after `3.9.0`. +fn parse(version: &str) -> Option<(u64, u64, u64)> { + let mut parts = version.split('.').map(|p| p.parse::().ok()); + let triple = (parts.next()??, parts.next()??, parts.next()??); + parts.next().is_none().then_some(triple) +} + +/// How long until the next check is due, given the last one in Unix seconds. +/// Zero when it is due now. A clock set back past the last check counts as +/// due, so a wrong clock cannot stop checks. +pub fn next_check_in(last_checked: Option, now: i64) -> Duration { + let interval = CHECK_INTERVAL.as_secs() as i64; + match last_checked { + Some(last) if (0..interval).contains(&(now - last)) => { + Duration::from_secs((interval - (now - last)) as u64) + } + _ => Duration::ZERO, + } +} + +/// Whether the binary at `exe` was installed by a package manager that updates +/// it, which [`MANAGED_MARKER`] next to its `bin/` says. +pub fn updates_managed(exe: &Path) -> bool { + exe.parent() + .and_then(Path::parent) + .is_some_and(|prefix| prefix.join(MANAGED_MARKER).exists()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn latest(tag: &str) -> Vec { + format!( + r#"{{"tag_name":"{tag}","html_url":"https://github.com/narrrl/proton-drive-linux/releases/tag/{tag}","draft":false}}"# + ) + .into_bytes() + } + + #[test] + fn the_latest_release_is_read_without_its_v() { + assert_eq!(latest_version(&latest("v3.6.0")).as_deref(), Some("3.6.0")); + assert_eq!(latest_version(&latest("3.6.0")).as_deref(), Some("3.6.0")); + } + + #[test] + fn an_answer_that_does_not_parse_has_no_version() { + assert_eq!(latest_version(b"not json"), None); + assert_eq!(latest_version(b"{}"), None); + assert_eq!(latest_version(br#"{"tag_name":7}"#), None); + assert_eq!(latest_version(&latest("nightly")), None); + assert_eq!(latest_version(&latest("v4.0.0-rc1")), None); + assert_eq!(latest_version(&latest("v4.0")), None); + assert_eq!(latest_version(&latest("v4.0.0.1")), None); + } + + #[test] + fn only_a_later_release_is_newer() { + assert!(is_newer("3.6.0", "3.5.0")); + assert!(!is_newer("3.5.0", "3.5.0")); + assert!(!is_newer("3.4.1", "3.5.0")); + assert!(!is_newer("2.99.99", "3.0.0")); + } + + #[test] + fn versions_compare_as_numbers() { + assert!(is_newer("3.10.0", "3.9.0")); + assert!(is_newer("3.5.10", "3.5.9")); + assert!(is_newer("4.0.0", "3.99.99")); + } + + #[test] + fn a_version_that_is_not_x_y_z_is_never_newer() { + assert!(!is_newer("4.0.0", "3.5.0-dev")); + assert!(!is_newer("garbage", "3.5.0")); + } + + #[test] + fn the_release_page_uses_the_tag() { + assert_eq!( + release_url("3.6.0"), + "https://github.com/narrrl/proton-drive-linux/releases/tag/v3.6.0" + ); + } + + #[test] + fn a_check_is_due_once_a_day() { + let day = CHECK_INTERVAL.as_secs() as i64; + assert_eq!(next_check_in(None, 1_000), Duration::ZERO); + assert_eq!(next_check_in(Some(1_000), 1_000), CHECK_INTERVAL); + assert_eq!( + next_check_in(Some(1_000), 1_000 + day - 1), + Duration::from_secs(1) + ); + assert_eq!(next_check_in(Some(1_000), 1_000 + day), Duration::ZERO); + assert_eq!( + next_check_in(Some(1_000), 999), + Duration::ZERO, + "a clock set back must not stop checks" + ); + } + + /// A unique temp directory removed on drop; avoids a dev-dependency, as in + /// [`crate::syncignore`]'s tests. + struct TempDir(std::path::PathBuf); + impl TempDir { + fn new() -> Self { + use std::sync::atomic::{AtomicU64, Ordering}; + static N: AtomicU64 = AtomicU64::new(0); + let p = std::env::temp_dir().join(format!( + "pdfs-update-{}-{}", + std::process::id(), + N.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir_all(&p).unwrap(); + TempDir(p) + } + } + impl Drop for TempDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + #[test] + fn the_marker_next_to_bin_means_a_package_manager_updates() { + let prefix = TempDir::new(); + let exe = prefix.0.join("bin/pdfs-app"); + assert!(!updates_managed(&exe)); + let marker = prefix.0.join(MANAGED_MARKER); + std::fs::create_dir_all(marker.parent().unwrap()).unwrap(); + std::fs::write(&marker, "").unwrap(); + assert!(updates_managed(&exe)); + } +} diff --git a/crates/pdfs-gui/src/app/main.rs b/crates/pdfs-gui/src/app/main.rs index 164410a..6df2ce5 100644 --- a/crates/pdfs-gui/src/app/main.rs +++ b/crates/pdfs-gui/src/app/main.rs @@ -10,6 +10,7 @@ pub(crate) mod pages; pub(crate) mod reload; #[path = "../theme.rs"] pub(crate) mod theme; +pub(crate) mod update; pub(crate) mod widgets; use compat::*; @@ -33,6 +34,7 @@ use pages::trash::*; use pages::verify::*; use reload::{LoadTicket, Loader, replace_items}; use theme::{load_resources, proton_theme_active, set_proton_theme}; +use update::*; use widgets::compat::*; use widgets::crumb::*; use widgets::details::*; @@ -155,6 +157,7 @@ struct Ui { pub(crate) locations: LocationsState, pub(crate) activity: ActivityState, pub(crate) takeout: TakeoutState, + pub(crate) update: UpdateState, } impl Ui { @@ -365,7 +368,8 @@ fn build_window(app: &adw::Application) { // Sidebar: the signed-in destinations. Selecting a row swaps the page stack; // `sync_sidebar` pushes the other way when navigation happens elsewhere (e.g. // login lands on Files). - let (sidebar_page, sidebar_list) = build_sidebar(&main_widgets.footer); + let update = UpdateState::new(); + let (sidebar_page, sidebar_list) = build_sidebar(&main_widgets.footer, &update.banner); // Every page brings its own header bar (see `page_frame`), so the content // side is the bare stack. @@ -432,6 +436,7 @@ fn build_window(app: &adw::Application) { theme_row: main_widgets.theme_row.clone(), online_map_row: main_widgets.online_map_row.clone(), tray_row: main_widgets.tray_row.clone(), + update_row: main_widgets.update_row.clone(), language_row: main_widgets.language_row.clone(), settings_suppress: Cell::new(false), budget_source: RefCell::new(None), @@ -654,6 +659,7 @@ fn build_window(app: &adw::Application) { running: Cell::new(false), dry_run: Cell::new(false), }, + update, }); wire_login(&ui); if let Some(e) = keyring_error { @@ -700,6 +706,7 @@ fn build_window(app: &adw::Application) { wire_activity(&ui, &activity_widgets); wire_takeout(&ui, &takeout_widgets); wire_retry(&ui); + wire_update(&ui); // Lazily load the Files / Photos pages the first time they're shown, so the // network round-trip only happens on demand rather than on every refresh. @@ -871,10 +878,13 @@ const DESTINATIONS: [(&str, &str, &str); 7] = [ /// Index of the first row in the sidebar's sync section. const SIDEBAR_SYNC_SECTION: i32 = 6; -/// The navigation sidebar: the destinations over a footer carrying the sync -/// status, the account quota and the account menu. Returns the page (for the +/// The navigation sidebar: the destinations under the update banner and over a +/// footer carrying the sync status, the account quota and the account menu. Returns the page (for the /// split view) and the list (to drive + reflect the current page). -fn build_sidebar(footer: >k4::Box) -> (adw::NavigationPage, gtk4::ListBox) { +fn build_sidebar( + footer: >k4::Box, + update_banner: &adw::Banner, +) -> (adw::NavigationPage, gtk4::ListBox) { let list = gtk4::ListBox::new(); list.set_selection_mode(gtk4::SelectionMode::Single); list.add_css_class("navigation-sidebar"); @@ -927,6 +937,7 @@ fn build_sidebar(footer: >k4::Box) -> (adw::NavigationPage, gtk4::ListBox) { .build(); let toolbar = adw::ToolbarView::new(); toolbar.add_top_bar(&header); + toolbar.add_top_bar(update_banner); toolbar.set_content(Some(&scroll)); toolbar.add_bottom_bar(footer); diff --git a/crates/pdfs-gui/src/app/pages/status.rs b/crates/pdfs-gui/src/app/pages/status.rs index 17961ac..a3a3879 100644 --- a/crates/pdfs-gui/src/app/pages/status.rs +++ b/crates/pdfs-gui/src/app/pages/status.rs @@ -52,6 +52,9 @@ pub(crate) struct StatusState { pub(crate) online_map_row: adw::SwitchRow, /// "Show tray icon" toggle. Guarded by [`Self::settings_suppress`]. pub(crate) tray_row: adw::SwitchRow, + /// "Check for updates" toggle, not shown where a package manager brings + /// updates. Guarded by [`Self::settings_suppress`]. + pub(crate) update_row: adw::SwitchRow, /// Language picker: "System Default", then [`i18n::LANGUAGES`]. Guarded by /// [`Self::settings_suppress`]. pub(crate) language_row: adw::ComboRow, @@ -164,6 +167,7 @@ pub(crate) struct MainWidgets { pub(crate) theme_row: adw::SwitchRow, pub(crate) online_map_row: adw::SwitchRow, pub(crate) tray_row: adw::SwitchRow, + pub(crate) update_row: adw::SwitchRow, pub(crate) language_row: adw::ComboRow, } @@ -250,6 +254,17 @@ pub(crate) fn build_main_page() -> MainWidgets { let language_row = build_language_row(); appearance_group.add(&language_row); + let updates_group = adw::PreferencesGroup::builder() + .title(gettext("Updates")) + .build(); + let update_row = adw::SwitchRow::builder() + .title(gettext("Check for updates")) + .subtitle(gettext( + "Ask GitHub once a day whether a new version is out. GitHub learns your IP address and which version you use.", + )) + .build(); + updates_group.add(&update_row); + let general = adw::PreferencesPage::builder() .title(gettext("General")) .icon_name("preferences-system-symbolic") @@ -258,6 +273,10 @@ pub(crate) fn build_main_page() -> MainWidgets { general.add(&location_group); general.add(&network_group); general.add(&appearance_group); + // A package manager brings new versions, so there is nothing to tell. + if !updates_managed_here() { + general.add(&updates_group); + } // ---- Preferences: Storage let storage_group = adw::PreferencesGroup::builder() @@ -442,6 +461,7 @@ pub(crate) fn build_main_page() -> MainWidgets { theme_row, online_map_row, tray_row, + update_row, language_row, } } @@ -541,6 +561,7 @@ pub(crate) fn wire_settings( .set_active(config.proton_theme.unwrap_or(false)); ui.status.online_map_row.set_active(config.online_map); ui.status.tray_row.set_active(!config.tray_hidden); + ui.status.update_row.set_active(config.update_check); ui.status .language_row .set_selected(language_index(config.language.as_deref())); @@ -690,6 +711,26 @@ pub(crate) fn wire_settings( } }); + // Update check: saved, and the first check runs right away if one is due. + let ui_update = ui.clone(); + ui.status.update_row.connect_active_notify(move |row| { + if ui_update.status.settings_suppress.get() { + return; + } + let mut config = ui_update.dirs.load_config(); + config.update_check = row.is_active(); + if let Err(e) = ui_update.dirs.save_config(&config) { + toast_error( + &ui_update, + &gettext("Couldn't save the update setting"), + &e.to_string(), + ); + return; + } + show_latest(&ui_update); + start_update_checks(&ui_update); + }); + // Tray icon: the tray reads the same flag at start, so hiding it here also // keeps the login autostart from bringing it back. let ui_tray = ui.clone(); diff --git a/crates/pdfs-gui/src/app/update.rs b/crates/pdfs-gui/src/app/update.rs new file mode 100644 index 0000000..b364757 --- /dev/null +++ b/crates/pdfs-gui/src/app/update.rs @@ -0,0 +1,168 @@ +//! Tells the user when a newer release is out, if they turned on Preferences → +//! Updates. The decisions are [`pdfs_core::update`]'s; this asks GitHub and +//! shows the banner. + +use crate::*; + +use std::time::{SystemTime, UNIX_EPOCH}; + +use pdfs_core::update::{ + RELEASES_LATEST_API, is_newer, latest_version, next_check_in, release_url, updates_managed, +}; + +/// How long one request to GitHub may take. +const FETCH_TIMEOUT: Duration = Duration::from_secs(10); + +/// How long to wait after a failed check, such as one made offline, before +/// trying again. Not saved, so a restart tries again straight away. +const RETRY_AFTER: Duration = Duration::from_secs(60 * 60); + +pub(crate) struct UpdateState { + /// Over the sidebar, so it shows whichever page is open. + pub(crate) banner: adw::Banner, + /// The check loop is running. Turning the setting off and on again while it + /// sleeps must not start a second one. + running: Cell, + /// The release page the banner's button opens. + url: RefCell, +} + +impl UpdateState { + pub(crate) fn new() -> Self { + Self { + banner: adw::Banner::builder() + .button_label(gettext("Download")) + .build(), + running: Cell::new(false), + url: RefCell::new(String::new()), + } + } +} + +/// Whether a package manager brings new releases to this install. The app then +/// never asks, and Preferences does not offer to. +pub(crate) fn updates_managed_here() -> bool { + std::env::current_exe().is_ok_and(|exe| updates_managed(&exe)) +} + +/// Wire the banner, show a release an earlier check found, and start checking +/// if the user asked for it. +pub(crate) fn wire_update(ui: &Rc) { + let ui_open = ui.clone(); + ui.update + .banner + .connect_button_clicked(move |_| open_uri(&ui_open.update.url.borrow())); + show_latest(ui); + start_update_checks(ui); +} + +/// Start the daily check, unless it is off, managed elsewhere, or running. +pub(crate) fn start_update_checks(ui: &Rc) { + if updates_managed_here() + || !ui.dirs.load_config().update_check + || ui.update.running.replace(true) + { + return; + } + let ui = ui.clone(); + glib::spawn_future_local(async move { + // Also kept here, so a config that cannot be saved does not make every + // pass ask GitHub again. + let mut checked = None; + loop { + let config = ui.dirs.load_config(); + if !config.update_check { + break; + } + let wait = next_check_in(config.update_last_checked.max(checked), unix_now()); + if !wait.is_zero() { + glib::timeout_future(wait).await; + continue; + } + match fetch_latest().await { + Ok(latest) => { + checked = Some(unix_now()); + let mut config = ui.dirs.load_config(); + config.update_last_checked = checked; + config.update_latest = latest; + if let Err(e) = ui.dirs.save_config(&config) { + tracing::warn!("update check: cannot save the result: {e}"); + } + show_latest(&ui); + } + Err(e) => { + tracing::warn!("update check: {e}"); + glib::timeout_future(RETRY_AFTER).await; + } + } + } + ui.update.running.set(false); + }); +} + +/// Reveal the banner if the last check found a release newer than this one, +/// and hide it otherwise. +pub(crate) fn show_latest(ui: &Ui) { + let config = ui.dirs.load_config(); + let newer = config + .update_latest + .filter(|latest| config.update_check && is_newer(latest, env!("CARGO_PKG_VERSION"))); + match newer { + Some(version) => { + ui.update.banner.set_title(&gettext_f( + "Proton Drive {version} is available", + &[("version", &version)], + )); + *ui.update.url.borrow_mut() = release_url(&version); + ui.update.banner.set_revealed(!updates_managed_here()); + } + None => ui.update.banner.set_revealed(false), + } +} + +fn unix_now() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0, |d| d.as_secs() as i64) +} + +/// The latest release's version, asked on a worker thread so the window never +/// waits on GitHub. `Ok(None)` when the answer names no `X.Y.Z` release. +async fn fetch_latest() -> Result, String> { + let (tx, rx) = async_channel::bounded(1); + std::thread::spawn(move || { + let _ = tx.send_blocking(fetch_latest_blocking()); + }); + rx.recv().await.map_err(|e| e.to_string())? +} + +fn fetch_latest_blocking() -> Result, String> { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| format!("no runtime: {e}"))?; + let client = reqwest::Client::builder() + .user_agent(concat!( + "proton-drive-linux/", + env!("CARGO_PKG_VERSION"), + " (", + env!("CARGO_PKG_REPOSITORY"), + ")" + )) + .timeout(FETCH_TIMEOUT) + .build() + .map_err(|e| format!("no HTTP client: {e}"))?; + runtime.block_on(async { + let reply = client + .get(RELEASES_LATEST_API) + .header("Accept", "application/vnd.github+json") + .send() + .await + .map_err(|e| e.to_string())?; + if !reply.status().is_success() { + return Err(format!("GitHub answered {}", reply.status())); + } + let body = reply.bytes().await.map_err(|e| e.to_string())?; + Ok(latest_version(&body)) + }) +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7666c06..25c9050 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -16,6 +16,22 @@ scratch (user data in `staging/` and `recovery/` is never touched by this). service does not answer, a new **Keys** section asks Proton which account and address keys open, which account key each address key depends on, and which address key the My Files share is encrypted to. It shows key IDs only, no secrets (#40). +- **A COPR repository for Fedora**, so `dnf upgrade` brings each new release: + `sudo dnf copr enable narl-rs/proton-drive-linux`. Every release also ships the vendored crates + as `proton-drive-linux-vendor-.tar.xz`, which the COPR build uses offline (#42). +- **The app can tell you about a new release.** Turn on **Check for updates** in Preferences, and + the app asks GitHub once a day and shows a banner with a **Download** button when a newer + version is out. It is off by default, and not offered for packages that a package manager + updates (COPR, the AUR, Nix) (#42). + +### Changed +- **Reorganised documentation, now also on the + [wiki](https://github.com/narrrl/proton-drive-linux/wiki).** A new + [Getting started](https://github.com/narrrl/proton-drive-linux/wiki/Getting-Started) page and + an [FAQ](https://github.com/narrrl/proton-drive-linux/wiki/FAQ). The user guide is split into + pages for files, synced folders, Photos, sharing, computers, and the launcher and tray. + Installation puts the COPR repository first for Fedora and explains updating and uninstalling + for each kind of package. Each release publishes `docs/` to the wiki. ## [3.5.0] — 2026-10-16 diff --git a/docs/CLI.md b/docs/CLI.md index 2f49571..1931d42 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -4,7 +4,8 @@ talks to the running service (`proton-drive.service`); `login`, `logout`, `mount`, `daemon` and `diagnose` work without it. -`pdfs --help` prints the full help of any command. This page groups them by task. +`pdfs --help` prints the full help of any command. This page groups them by task. For +what the commands do in the app, see the [user guide](USER_GUIDE.md). - [Conventions](#conventions) - [Account and service](#account-and-service) @@ -35,7 +36,9 @@ talks to the running service (`proton-drive.service`); `login`, `logout`, `mount | Command | Description | |---|---| -| `pdfs login [-u EMAIL]` | Sign in (password, then a 2FA code if the account has one) and store the session in the system keyring. Restarts the service if it is running. | +| `pdfs login [-u EMAIL] [--remember]` | Sign in (password, then a 2FA code if the account has one) and store the session in the system keyring. Restarts the service if it is running. `--remember` keeps a two-password account unlocked across logins. | +| `pdfs unlock [--remember]` | Enter the mailbox password of a two-password account, so the service can open your files. Needed once per login session, or once with `--remember`. | +| `pdfs lock` | Forget the unlock held in memory; the account is locked again. | | `pdfs logout` | End the session on Proton and remove the stored session. | | `pdfs status` | Account, mount, queue and cache state. | | `pdfs quota` | Storage used across all Proton products. | @@ -43,7 +46,8 @@ talks to the running service (`proton-drive.service`); `login`, `logout`, `mount | `pdfs daemon [MOUNTPOINT]` | Run the service in the foreground: wait for a session, mount, stay mounted. `proton-drive.service` runs this. | | `pdfs mount [MOUNTPOINT]` | Mount once and block until unmounted. Defaults to `~/ProtonDrive`. | -`pdfs login` cannot show a CAPTCHA. If Proton asks for one, sign in with `pdfs-app` instead. +If Proton asks for a CAPTCHA, `pdfs login` prints the address of the page. Open it in your +browser, complete it, then press Enter. ## Files @@ -86,7 +90,7 @@ commands are for scripts and for cases where the mount is not available. | `pdfs sync export ID DEST` | Save a copy of a queued upload's content. `DEST` may be a folder; an existing file is never overwritten. The upload stays queued. | | `pdfs sync discard ID [--yes]` | Drop a queued change and undo it here: a new file or folder goes, anything else goes back to what Proton Drive has. Content not exported first is lost. | | `pdfs sync retry [ID]` | Retry one queued operation, or every failed one. | -| `pdfs sync restore [--device UID] [--yes]` | Re-attach this computer's backed-up folders to local folders and download them. `--device` restores another computer's folders. See [RECOVERY.md](RECOVERY.md). | +| `pdfs sync restore [--device UID] [--yes]` | Re-attach this computer's backed-up folders to local folders and download them. `--device` restores another computer's folders. See [Recovery](RECOVERY.md). | ## Conflicts @@ -104,7 +108,7 @@ commands are for scripts and for cases where the mount is not available. | `pdfs devices rm UID` | Remove a computer and its backup from the account. | | `pdfs devices join UID [--yes]` | Sync the same folders as another computer. This machine syncs into that computer's backup alongside it, then offers its folders like `pdfs sync restore`. Both computers keep them in step. `--yes` skips the confirmation and accepts every proposed path. | | `pdfs devices adopt UID` | Make this machine continue another computer's backup, for example after a reinstall. `--clear` returns to matching by hostname. | -| `pdfs devices migrate UID [--yes]` | Move another computer's folders into this computer's backup, then restore them here like `pdfs sync restore`. The other computer stays registered, without those folders. `--yes` skips the confirmation and accepts every proposed path. See [RECOVERY.md](RECOVERY.md). | +| `pdfs devices migrate UID [--yes]` | Move another computer's folders into this computer's backup, then restore them here like `pdfs sync restore`. The other computer stays registered, without those folders. `--yes` skips the confirmation and accepts every proposed path. See [Recovery](RECOVERY.md). | ## Trash @@ -177,7 +181,7 @@ commands are for scripts and for cases where the mount is not available. | `pdfs cache vacuum` | Compact the database. Needs free space for a second copy of it while it runs. | | `pdfs cache clear` | Delete cached file content. Files kept available offline stay. | -[TROUBLESHOOTING.md](TROUBLESHOOTING.md) explains when to use each of these. +[Troubleshooting](TROUBLESHOOTING.md) explains when to use each of these. ## Scripting with `--json` diff --git a/docs/COMPUTERS.md b/docs/COMPUTERS.md new file mode 100644 index 0000000..4f4a8f2 --- /dev/null +++ b/docs/COMPUTERS.md @@ -0,0 +1,60 @@ +# Computers + +Every computer that backs up folders to your account has its own entry in Proton Drive. The +**Computers** page lists this computer and every other one, including computers that run the +official Windows or macOS app. + +- [What you can do](#what-you-can-do) +- [Syncing several computers](#syncing-several-computers) +- [Replacing or reinstalling a computer](#replacing-or-reinstalling-a-computer) + +![The Computers page](../images/computers.png) + +## What you can do + +**On this computer:** + +- See its synced folders and their state, and add more with **Add Folder to Sync**. See + [Synced folders](SYNCED_FOLDERS.md). +- **Restore Folders…** brings back folders that are backed up under this computer but not synced + here yet, for example after a reinstall. + +**On another computer**, from its menu: + +| Choose | When | What happens | +|---|---|---| +| **Browse Files** | You need a file from it | Its backup opens read-only, like My files | +| **Restore to This Computer…** | You want a copy of its folders here | Its folders download here and stay part of its backup | +| **Sync With This Computer…** | Both computers should share the same folders, such as a desktop and a laptop | Both keep the folders in step. See [below](#syncing-several-computers) | +| **Continue This Backup Here…** | This computer replaces it, after a reinstall or with new hardware | This computer takes over its backup. See [Recovery](RECOVERY.md) | +| **Move to This Computer…** | You switch for good, for example from Windows to Linux | Its folders move into this computer's backup and are offered for syncing here. The other computer stays in the list, without those folders | + +`pdfs devices list` shows the same computers with their IDs, for the commands in the +[command-line reference](CLI.md#computers). + +## Syncing several computers + +Two or more computers can sync the same folders through one backup in Proton Drive. + +1. Sync the folders on the first computer as usual. +2. On the second computer, open **Computers**, open the menu of the first computer and choose + **Sync With This Computer…**. From a terminal: `pdfs devices join UID`, with the ID from + `pdfs devices list`. +3. Choose which of its folders to sync here, and where. + +From then on, both computers sync into that backup: + +- A folder you add later on either computer lands there too. A folder with the same name joins + the one already there. +- A change on one computer reaches the other within about two minutes. +- When both computers change the same file within that time, each keeps the other's version under + the original name, and its own as a conflict copy named after it. See + [Conflicts](SYNCED_FOLDERS.md#conflicts). + +Folders that were synced on this computer before it joined stay in its own backup. + +## Replacing or reinstalling a computer + +Your synced folders are on Proton Drive, so a new or reinstalled computer can pick up where the +old one stopped. [Recovering on a new computer](RECOVERY.md) walks through it step by step, and +explains what is lost with a machine and what is not. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f121275..f3acf22 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -39,7 +39,7 @@ Inside `content/`: > not reached Proton Drive and exists nowhere else. Everything in these directories except the keyring is **decrypted**. See -[ARCHITECTURE.md §10](ARCHITECTURE.md#10-threat-model-what-this-client-writes-to-disk-in-plaintext). +the [security model](SECURITY_MODEL.md#what-is-kept-on-this-computer). ## `config.json` @@ -61,6 +61,7 @@ fix it. "proton_theme": true, "online_map": false, "tray_hidden": false, + "update_check": false, "files_view": { "list": true, "sort": "modified", "descending": true, "folders_first": true }, "window": { "width": 980, "height": 680, "maximized": false }, "open_with": { @@ -87,6 +88,9 @@ fix it. | `proton_theme` | bool | follow system | General → Appearance | Proton's colours instead of the system theme | | `online_map` | bool | `false` | General → Appearance | Street map for Places from OpenFreeMap | | `tray_hidden` | bool | `false` | General → Appearance | Hide the tray icon | +| `update_check` | bool | `false` | General → Updates | Ask GitHub once a day for a newer release and show it over the sidebar. Not offered where a package manager installs updates | +| `update_last_checked` | Unix seconds | none | — | Written by the update check, so a restart does not ask again within a day | +| `update_latest` | version | none | — | The latest release the update check found | | `files_view` | object | grid, by name | My files view menu | `list`, `sort` (`name`, `size`, `modified`), `descending`, `folders_first` | | `window` | object | 980 × 680 | Resizing the app window | `width`, `height` (the unmaximized size), `maximized` | | `open_with` | object | `xdg-open` | — | See [`open_with`](#open_with) | @@ -109,7 +113,7 @@ Gitignore-style rules applied to every synced folder, in addition to each folder ``` Set it to `[]` to turn the defaults off. See -[USER_GUIDE.md](USER_GUIDE.md#excluding-files-with-pdfsignore) for how ignoring behaves. +[Synced folders](SYNCED_FOLDERS.md#excluding-files-with-pdfsignore) for how excluding behaves. ### `conflict_sweep` @@ -198,7 +202,7 @@ Command-line flags win over the file: `--gtk`, `--dmenu`, `--fzf` and `--menu`. | `PDFS_STAND_IN_WIDGETS` | app | `1` uses the libadwaita 1.5 fallback widgets on newer systems (development) | The tests have their own variables (`PDFS_ACCEPTANCE_*`, `PDFS_SIM_*`, `PDFS_MIGRATE_DB`), -listed in [TESTING.md](TESTING.md). +listed in [Testing](TESTING.md). ## The systemd service diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index d91bf05..3b1804d 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -141,31 +141,50 @@ file to `po/POTFILES.in`, run `po/update.sh`, and translate the new entries in e unit test, a simulation scenario or a `regression B` acceptance case, and cite the entry in the code as `docs/BUGS.md B`. Do not cite planning notes that are not in the repository. -**A user-visible change.** Update the matching page (user guide, CLI reference, configuration) -and the `[Unreleased]` section of [CHANGELOG.md](CHANGELOG.md). `scripts/publish-wiki.sh` copies -the six user pages (`INSTALL`, `USER_GUIDE`, `CLI`, `CONFIGURATION`, `TROUBLESHOOTING`, `RECOVERY`) -to the GitHub wiki, so keep their file names. +**A user-visible change.** Update the matching page (a user guide topic, the CLI reference, +configuration) and the `[Unreleased]` section of [CHANGELOG.md](CHANGELOG.md). Link between pages +with relative links such as `[Conflicts](SYNCED_FOLDERS.md#conflicts)`, and show screenshots from +`images/` with `../images/.png`. + +**The wiki.** `docs/` is the source of the [GitHub wiki](https://github.com/narrrl/proton-drive-linux/wiki). +`scripts/publish-wiki.sh` turns each page listed in its `SECTIONS` into a wiki page, rewrites the +links, copies the images and writes the sidebar. `docs/README.md` becomes the wiki's Home. +`BUGS.md` and `CHANGELOG.md` stay out of it; the wiki links to them on GitHub. + +- `scripts/publish-wiki.sh --check` checks every link and heading anchor in `docs/`, `README.md`, + `CONTRIBUTING.md` and `SECURITY.md`. It fails on a page in `docs/` that is not in `SECTIONS` + or `EXCLUDED`. `.github/workflows/wiki.yml` runs it for every change to the documentation. +- Each release publishes the wiki from the tag. Run the **Wiki** workflow from the Actions tab to + publish `main` in between, for example after fixing a mistake. +- `scripts/publish-wiki.sh [WORKDIR]` builds and commits the wiki in a local clone without + pushing, so you can look at the result first. `--push` pushes it. + +Never edit the wiki in the browser: the next publish replaces every page. ## Releasing Releases are cut from `main` by pushing a `v*` tag. `.github/workflows/release.yml` then: -1. checks that the tag, the workspace version in `Cargo.toml` and `pkgver` in - `packaging/PKGBUILD` agree; +1. checks that the tag, the workspace version in `Cargo.toml`, `pkgver` in + `packaging/PKGBUILD` and the default version in `packaging/proton-drive-linux.spec` agree; 2. runs the quality gates; -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, and - pushes it to the `proton-drive-linux` Cachix cache (secret `CACHIX_AUTH_TOKEN`); +3. builds the release binaries and packages a `.tar.gz`, a `.deb`, a GPG-signed `.rpm` and a + `.tar.xz` of the vendored crates, and builds the Nix flake at the tag, which is what a flake + input pinned to `vX.Y.Z` gets, and pushes it to the `proton-drive-linux` Cachix cache (secret + `CACHIX_AUTH_TOKEN`); 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`. + back to `main`; +6. calls `.github/workflows/copr.yml`, which builds the release in the + [COPR repository](../packaging/copr/README.md) (secret `COPR_CONFIG`); +7. calls `.github/workflows/wiki.yml`, which publishes `docs/` at the tag to the wiki. To prepare a release: ```bash -# 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. +# 1. Bump the version in Cargo.toml, packaging/PKGBUILD, packaging/proton-drive-linux.spec, +# 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. diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000..5824354 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,104 @@ +# FAQ + +Short answers to common questions, with links to the details. + +- [Is this an official Proton app?](#is-this-an-official-proton-app) +- [If I delete a file in `~/ProtonDrive`, is it deleted on Proton Drive?](#if-i-delete-a-file-in-protondrive-is-it-deleted-on-proton-drive) +- [Online only, available offline or synced folder: which do I want?](#online-only-available-offline-or-synced-folder-which-do-i-want) +- [Does it work offline?](#does-it-work-offline) +- [How do I get updates?](#how-do-i-get-updates) +- [Why is there no tray icon?](#why-is-there-no-tray-icon) +- [What is stored unencrypted on my computer?](#what-is-stored-unencrypted-on-my-computer) +- [Does it send telemetry?](#does-it-send-telemetry) +- [Can I use it on several computers?](#can-i-use-it-on-several-computers) +- [Can I continue a backup from Windows or macOS?](#can-i-continue-a-backup-from-windows-or-macos) +- [Can I run it on a server without a desktop?](#can-i-run-it-on-a-server-without-a-desktop) +- [Which architectures and languages are supported?](#which-architectures-and-languages-are-supported) +- [How do I report a bug?](#how-do-i-report-a-bug) + +## Is this an official Proton app? + +No. It is an independent, open-source client, not affiliated with or endorsed by Proton AG. It +uses a Rust port of Proton's own Drive SDK, and everything stays end-to-end encrypted. It has had +no independent security audit. See the [security model](SECURITY_MODEL.md). + +## If I delete a file in `~/ProtonDrive`, is it deleted on Proton Drive? + +Yes. `~/ProtonDrive` is your Proton Drive, not a copy. A deleted file moves to the Proton Drive +Trash, and you can restore it from **Trash** in the app or the web. See [Trash](FILES.md#trash). + +Stopping the service is not the same: once the service stops, `~/ProtonDrive` is just an empty +folder, and your files stay on Proton Drive. + +## Online only, available offline or synced folder: which do I want? + +- **Online only** for most of your Drive. Files take no space until you open them. +- **Available offline** for files in `~/ProtonDrive` you need without a connection. +- **Synced folder** for a folder that lives on this computer, such as `~/Documents`, and should be + backed up. + +[Getting started](GETTING_STARTED.md#3-choose-how-files-are-kept) compares them side by side. + +## Does it work offline? + +Yes, for files that are on this computer: available-offline files, synced folders, and recently +opened files still in the cache. You can also create, rename, move and delete files offline. The +changes are queued and sent when the connection is back. See +[Changes waiting for Proton Drive](SYNCED_FOLDERS.md#changes-waiting-for-proton-drive). + +## How do I get updates? + +Through your package manager. The Fedora COPR repository, the AUR and the Nix flake get every +release, so `dnf upgrade`, your AUR helper or a flake update brings it. The `.deb` has to be +downloaded again for each release. To be told about new releases, turn on **Check for updates** +in **Preferences**. The app never updates itself. See [Installation → Updating](INSTALL.md#updating). + +## Why is there no tray icon? + +GNOME needs the +[AppIndicator extension](https://extensions.gnome.org/extension/615/appindicator-support/) to show +tray icons. If you hid the icon from its own menu, turn it back on under +**Preferences → General → Appearance**. + +## What is stored unencrypted on my computer? + +To show your files, the client decrypts them on this computer. Recently opened files, files kept +offline, changes waiting to upload, and the names of your files and folders are stored without +extra encryption. Use full-disk or home-folder encryption. The +[security model](SECURITY_MODEL.md#what-is-kept-on-this-computer) has the full list. + +## Does it send telemetry? + +No. The client connects only to Proton, and, if you turn them on, to OpenFreeMap for the street map +in Photos and to GitHub to check for updates. See +[Network connections](SECURITY_MODEL.md#network-connections). + +## Can I use it on several computers? + +Yes. Each computer has its own backup in Proton Drive, and two or more computers can also sync the +same folders. See [Syncing several computers](COMPUTERS.md#syncing-several-computers). + +## Can I continue a backup from Windows or macOS? + +Yes. On the **Computers** page, choose **Move to This Computer…** on the old computer. Its folders +move into this computer's backup on Proton Drive, without uploading anything again. See +[Moving another computer's backup here](RECOVERY.md#moving-another-computers-backup-here). + +## Can I run it on a server without a desktop? + +Yes, with `pdfs daemon` or `pdfs mount`. It still needs a Secret Service provider such as GNOME +Keyring or KeePassXC to hold the session. See +[Headless and server use](INSTALL.md#headless-and-server-use). + +## Which architectures and languages are supported? + +Release packages are for x86_64. Other architectures can [build from source](INSTALL.md#build-from-source). + +The app, tray and launcher are available in English, German, Spanish, French, Italian, Dutch, +Polish and Brazilian Portuguese. [Translating](TRANSLATING.md) explains how to add a language. + +## How do I report a bug? + +Run `pdfs diagnose`, then [open an issue](https://github.com/narrrl/proton-drive-linux/issues/new/choose) +with what [Reporting a problem](TROUBLESHOOTING.md#reporting-a-problem) lists. Report security +problems privately, as described in [SECURITY.md](../SECURITY.md). diff --git a/docs/FILES.md b/docs/FILES.md new file mode 100644 index 0000000..87b3976 --- /dev/null +++ b/docs/FILES.md @@ -0,0 +1,88 @@ +# Files and offline access + +Your Proton Drive appears on this computer as a folder, `~/ProtonDrive`. Every file and folder in +it is listed straight away, but a file only downloads when a program opens it. This page covers +working in that folder, keeping files on this computer, the Trash, and earlier versions of a +file. + +- [Your Drive as a folder](#your-drive-as-a-folder) +- [Keep files available offline](#keep-files-available-offline) +- [Browse in the app](#browse-in-the-app) +- [Trash](#trash) +- [Earlier versions of a file](#earlier-versions-of-a-file) +- [Choose which program opens a file](#choose-which-program-opens-a-file) + +## Your Drive as a folder + +`~/ProtonDrive` is your Proton Drive, called **My files** in the app. It is not a copy. A file you +change there changes on Proton Drive, and a file you delete there goes to the Proton Drive +Trash. Every program can use it, from your file manager to a terminal. + +- **Only what you read is downloaded.** A video player that seeks, or a tool that reads only the + start of a file, downloads only those parts. Videos and music start playing before the whole + file is down. +- **Changes are instant, online or offline.** Creating, renaming, moving and deleting return at + once. When a program saves and closes a file, the new content is stored on this computer and + sent to Proton Drive in the background. Changes survive a lost connection, a crash or a reboot. + See [Changes waiting for Proton Drive](SYNCED_FOLDERS.md#changes-waiting-for-proton-drive). +- **Recently opened files are cached** up to a size limit (5 GiB by default, set in + **Preferences → Storage**). The oldest are removed first to make room. +- **Shared with me** is a folder inside `~/ProtonDrive` that holds what other people share with + you. Shares where you are a viewer are read-only. + +Proton Drive cannot store symbolic links, hard links, device files or FIFOs. A program that tries +to create one gets an error at once. For a project that needs them, use a +[synced folder](SYNCED_FOLDERS.md) and exclude those paths. + +To move the folder somewhere else, change **Preferences → General → Location**. + +## Keep files available offline + +Files you mark **Available offline** are downloaded completely and stay on this computer, so +they open without a connection. + +- **In the app:** select a file or folder in **My files** and turn on **Available offline**. +- **From a terminal:** `pdfs pin ~/ProtonDrive/Documents/Taxes` + +For a folder, every file in it and in its subfolders downloads in the background. The tray and +the app show the progress. **Preferences → Storage → Available offline** and `pdfs pins` list +what is kept. + +Turn it off again with the same switch, or `pdfs unpin PATH`. This frees the space. The files stay +on Proton Drive. A file inside a folder that is kept offline stays with its folder: turn the +folder off instead. + +## Browse in the app + +![My files in the app](../images/files.png) + +The **My files** page shows the same files without going through the folder. It has a grid and +a list view, sorting, thumbnails (including camera RAW previews), multi-select, and drag and drop +to upload. **Details** (Alt+Enter) shows the size, dates, sharing and version history of the +selected item. + +## Trash + +Deleted files go to the Trash, both from `~/ProtonDrive` and from the app. The **Trash** page +lists them with the date they were deleted. + +- **Restore** puts an item back where it was, together with any trashed folders above it. +- **Delete Forever** and **Empty Trash** are permanent. + +From a terminal: `pdfs trash` lists the Trash with an ID for each item, and `pdfs restore`, +`pdfs delete-forever` and `pdfs empty-trash` take those IDs. + +## Earlier versions of a file + +Proton Drive keeps earlier versions of every file. Open them with **Details → Versions…** in the +app, or `pdfs versions list PATH`. You can: + +- **Restore** a version. This happens on Proton Drive, so nothing is uploaded again. +- **Save** a version to a separate file, without touching the current one. +- **Delete** an old version permanently. The current version cannot be deleted. + +## Choose which program opens a file + +The app and the search launcher open files with your desktop's default program (`xdg-open`). To +open some file types differently, for example text files in Neovim inside a terminal, add +`open_with` rules to `config.json`. See [Configuration → `open_with`](CONFIGURATION.md#open_with). diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 0000000..3d0e002 --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,72 @@ +# Getting started + +From installing to your first synced folder in a few minutes. + +> [!IMPORTANT] +> Proton Drive for Linux is an unofficial client, built by the community and not by Proton. Try it +> first with files you also have somewhere else. + +## 1. Install + +| Distribution | Command | +|---|---| +| Fedora 44 or newer | `sudo dnf copr enable narl-rs/proton-drive-linux`
`sudo dnf install proton-drive-linux` | +| Arch Linux | `paru -S proton-drive-for-linux` (or any other AUR helper) | +| Debian, Ubuntu 24.04 or newer | Download the `.deb` from the [latest release](https://github.com/narrrl/proton-drive-linux/releases/latest), then `sudo apt install ./proton-drive-linux_*_amd64.deb` | +| NixOS | See [Installation → NixOS](INSTALL.md#nixos-flake) | + +On GNOME, also install the +[AppIndicator extension](https://extensions.gnome.org/extension/615/appindicator-support/) to see +the tray icon. [Installation](INSTALL.md) has every option, building from source, and the optional +extras. + +## 2. Sign in + +![The sign-in window](../images/login.png) + +1. Open **Proton Drive** from your applications menu. +2. Sign in with your Proton email and password, and the two-factor code if your account has one. + If Proton asks you to prove you are human (common on a VPN), solve the challenge in the window + that opens. +3. Wait a moment. Your Proton Drive appears as the folder `~/ProtonDrive`. + +The app starts the Proton Drive service for you, and the service starts by itself whenever you +log in. Your session is kept in the system keyring (GNOME Keyring, KWallet or KeePassXC), so you +sign in only once. + +## 3. Choose how files are kept + +There are three ways to have your files on this computer. You can mix them freely. + +| | Takes disk space | Works offline | Use it for | +|---|---|---|---| +| **Online only** (the default in `~/ProtonDrive`) | Only for files you opened recently | Only files still in the cache | Most of your Drive: archives, media, anything large | +| **Available offline** | Yes | Yes | Files and folders you need on the road | +| **Synced folder** | Yes, a full copy | Yes | Folders that live on this computer, such as `~/Documents`, backed up to Proton Drive | + +- **Online only:** open `~/ProtonDrive` in your file manager. Files download when you open them. +- **Available offline:** select a file or folder in the app and turn on **Available offline**. + [More about offline files](FILES.md#keep-files-available-offline). +- **Synced folder:** in the app, open **Computers** and choose **Add Folder to Sync**. + [More about synced folders](SYNCED_FOLDERS.md). + +## 4. Know where to look + +- **Tray icon:** shows whether everything is up to date, and pauses or resumes sync. +- **Sync → Overview** in the app: what is uploading, what is waiting, and anything that needs you. +- **Search launcher:** bind `pdfs-prompt` to a keyboard shortcut to find any file in Proton Drive + or your home folder. See [Search launcher and tray](LAUNCHER_AND_TRAY.md). + +> [!WARNING] +> `~/ProtonDrive` is your Proton Drive, not a copy. Deleting a file there moves it to the Proton +> Drive Trash on every device. You can restore it from **Trash** in the app. + +## Next steps + +- [User guide](USER_GUIDE.md): everything the app, tray and launcher do. +- [FAQ](FAQ.md): short answers to common questions. +- [Troubleshooting](TROUBLESHOOTING.md): when something does not work. +- [Recovering on a new computer](RECOVERY.md): read this before you rely on the client for + backups. +- Prefer a terminal? `pdfs status` shows the state of everything, and the + [command-line reference](CLI.md) lists every command. diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 8ec4eb7..6a6f63a 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -1,70 +1,53 @@ # Installation -This page covers installing Proton Drive for Linux from a package or from source, the first -sign-in, running without a desktop, and removing the client again. +Install Proton Drive for Linux from your distribution's package, sign in, and keep it up to date. +This page also covers building from source, running without a desktop, and removing the client +again. -- [Requirements](#requirements) - [Install a package](#install-a-package) -- [Build from source](#build-from-source) +- [Optional extras](#optional-extras) - [First run](#first-run) -- [Headless and server use](#headless-and-server-use) - [Updating](#updating) - [Uninstalling](#uninstalling) - -## Requirements - -| | Minimum | -|---|---| -| Architecture | x86_64 (release packages); other architectures build from source | -| Kernel | FUSE 3 (`fuse3` package, `/dev/fuse` available to your user) | -| GTK / libadwaita | GTK 4.12, libadwaita 1.5 (1.8 or newer gets the newest widgets) | -| WebKitGTK | 6.0 — used only for the CAPTCHA page during sign-in | -| Secret Service | GNOME Keyring, KWallet, KeePassXC, or another provider of `org.freedesktop.secrets` | -| Rust (source builds) | 1.96 | - -The session is stored only in the Secret Service. Without a running provider, sign-in succeeds but -cannot be saved, and the service cannot start unattended. - -Optional runtime extras: - -| Package | Adds | -|---|---| -| `exiftool` (`perl-image-exiftool`, `libimage-exiftool-perl`) | Thumbnails for camera RAW files | -| `ffmpeg` | Video thumbnails | -| `iso-codes` | Country names in your language on the Places map | -| `xdg-utils` | "Open Folder" in the tray, and the default file opener | -| `fzf` plus a terminal (foot, kitty, alacritty, …) | `pdfs-prompt --fzf`, the search-as-you-type launcher | -| AppIndicator extension (GNOME only) | The tray icon. KDE Plasma and most other desktops show it natively | +- [Headless and server use](#headless-and-server-use) +- [Build from source](#build-from-source) +- [What the packages install](#what-the-packages-install) ## Install a package -Every [release](https://github.com/narrrl/proton-drive-linux/releases/latest) ships a `.deb`, a -signed `.rpm` and a `.tar.gz` of the raw binaries. All packages install the same files: +| Distribution | Package | Updates | +|---|---|---| +| [Fedora 44 or newer](#fedora-44-or-newer) | COPR repository `narl-rs/proton-drive-linux` | With `dnf upgrade` | +| [Arch Linux](#arch-linux-aur) | `proton-drive-for-linux` in the AUR | With your AUR helper | +| [Debian, Ubuntu 24.04 or newer](#debian-and-ubuntu-2404-or-newer) | `.deb` from the release page | Download each release | +| [NixOS](#nixos-flake) | The flake in this repository | With `nix flake update` | +| [Anything else](#other-distributions) | `.tar.gz` from the release page, or [build from source](#build-from-source) | By hand | -| Path | Purpose | -|---|---| -| `/usr/bin/pdfs` | Command-line client and background service | -| `/usr/bin/pdfs-app` | Desktop app | -| `/usr/bin/pdfs-tray` | Tray icon | -| `/usr/bin/pdfs-prompt` | Search launcher | -| `/usr/lib/systemd/user/proton-drive.service` | User service that keeps the drive mounted | -| `/etc/xdg/autostart/io.narl.proton-drive-linux-tray.desktop` | Starts the tray at login | -| `/usr/share/locale/*/LC_MESSAGES/pdfs.mo` | Translations | - -### Debian and Ubuntu (24.04 or newer) - -```bash -sudo apt install ./proton-drive-linux_*_amd64.deb -``` +Release packages are for x86_64. You also need a Secret Service provider, such as GNOME +Keyring, KWallet or KeePassXC, to keep your session. Most desktops have one already. ### Fedora (44 or newer) +Enable the [COPR repository](https://copr.fedorainfracloud.org/coprs/narl-rs/proton-drive-linux/) +and install the package: + +```bash +sudo dnf copr enable narl-rs/proton-drive-linux +sudo dnf install proton-drive-linux +``` + +COPR builds every release for Fedora 44, 45 and Rawhide, so `sudo dnf upgrade` keeps the client +up to date along with the rest of your system. + +You can also download the signed `.rpm` from the +[latest release](https://github.com/narrrl/proton-drive-linux/releases/latest). It installs the +same files, but you have to download each new version yourself: + ```bash sudo dnf install ./proton-drive-linux-*.x86_64.rpm ``` -For a Fedora package built from your own checkout, see -[Build an RPM locally](#build-an-rpm-locally). +To build an RPM from your own checkout, see [Build an RPM locally](#build-an-rpm-locally). ### Arch Linux (AUR) @@ -72,21 +55,33 @@ Three packages cover the usual choices. They conflict with each other, so instal | Package | Contents | |---|---| -| [`proton-drive-for-linux`](https://aur.archlinux.org/packages/proton-drive-for-linux) | Latest tagged release, built from source | -| [`proton-drive-for-linux-bin`](https://aur.archlinux.org/packages/proton-drive-for-linux-bin) | Latest tagged release, prebuilt binaries | -| [`proton-drive-for-linux-git`](https://aur.archlinux.org/packages/proton-drive-for-linux-git) | `main`, built from git | +| [`proton-drive-for-linux`](https://aur.archlinux.org/packages/proton-drive-for-linux) | Latest release, built from source | +| [`proton-drive-for-linux-bin`](https://aur.archlinux.org/packages/proton-drive-for-linux-bin) | Latest release, prebuilt binaries | +| [`proton-drive-for-linux-git`](https://aur.archlinux.org/packages/proton-drive-for-linux-git) | The newest code on `main`, built from git | ```bash paru -S proton-drive-for-linux # or yay, or any other AUR helper ``` -The recipes live in [`packaging/aur/`](../packaging/aur/README.md). +The recipes are in [`packaging/aur/`](../packaging/aur/README.md). + +### Debian and Ubuntu (24.04 or newer) + +Download the `.deb` from the [latest release](https://github.com/narrrl/proton-drive-linux/releases/latest) +and install it: + +```bash +sudo apt install ./proton-drive-linux_*_amd64.deb +``` + +There is no APT repository yet, so repeat this for each new release. Turn on **Check for +updates** in **Preferences** to be told when one is out. ### 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: +The repository is a flake. Add it as an input and enable the module. The module installs the +package, starts the user service in every graphical session, and turns on the `fusermount3` +wrapper and GNOME Keyring for you: ```nix { @@ -107,7 +102,7 @@ That URL follows `main`. To stay on releases, pin the input to a release tag ins the tag to update: ```nix -inputs.proton-drive-linux.url = "github:narrrl/proton-drive-linux/v3.3.0"; +inputs.proton-drive-linux.url = "github:narrrl/proton-drive-linux/v3.5.0"; ``` Each release tag is built with Nix before the release is published. @@ -128,29 +123,176 @@ nix.settings = { ``` The cache holds the build against the `nixpkgs` this flake locks. Setting -`inputs.proton-drive-linux.inputs.nixpkgs.follows = "nixpkgs"` builds against yours instead, which -the cache does not have, so Nix compiles it. `nix run` and `nix shell` ask whether to use the -cache the first time; answer yes, or run `cachix use proton-drive-linux`. +`inputs.proton-drive-linux.inputs.nixpkgs.follows = "nixpkgs"` builds against yours instead, +which the cache does not have, so Nix compiles it. `nix run` and `nix shell` ask whether to use +the cache the first time; answer yes, or run `cachix use proton-drive-linux`. -To try it without changing your system, run the app straight from the flake: +To try it without changing your system, run it 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 +The mount 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 is 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 -[Using a binary outside `/usr/bin`](#using-a-binary-outside-usrbin). +Unpack the `.tar.gz` from the [latest release](https://github.com/narrrl/proton-drive-linux/releases/latest) +into a directory on your `PATH`, then install the service as shown in +[Using a binary outside `/usr/bin`](#using-a-binary-outside-usrbin). The binaries need GTK 4.12, +libadwaita 1.5, WebKitGTK 6.0 and FUSE 3; see [Requirements](#requirements). + +## Optional extras + +The packages pull in what the client needs. These add features if you install them: + +| Package | Adds | +|---|---| +| `exiftool` (`perl-image-exiftool`, `libimage-exiftool-perl`) | Thumbnails for camera RAW files | +| `ffmpeg` | Video thumbnails | +| `iso-codes` | Country names in your language on the Places map | +| `xdg-utils` | "Open Folder" in the tray, and the default file opener | +| `fzf` and a terminal (foot, kitty, alacritty, …) | `pdfs-prompt --fzf`, the search-as-you-type launcher | +| [AppIndicator extension](https://extensions.gnome.org/extension/615/appindicator-support/) (GNOME only) | The tray icon. KDE Plasma and most other desktops show it without help | + +## First run + +### With the desktop app + +1. Open **Proton Drive** from your applications menu, or run `pdfs-app`. +2. Sign in with your Proton email and password, and your two-factor code if the account has one. + If Proton asks for a CAPTCHA (common on a VPN or a new network), the app shows the challenge in + a window and continues after you solve it. +3. The app enables and starts `proton-drive.service`. Your Drive appears at `~/ProtonDrive`. + +The service belongs to your desktop session. It starts after you log in and stops when you log +out. To put the folder somewhere else, change **Preferences → General → Location**. + +[Getting started](GETTING_STARTED.md) explains what to do next. + +### With the command line + +`pdfs login` stores the session but does not enable the service, so a setup that runs the service +another way stays untouched. Enable it yourself: + +```bash +pdfs login # email, password, 2FA code +systemctl --user enable --now proton-drive.service +pdfs status # account, mount and queue state +``` + +`pdfs login` prints the address of the CAPTCHA page if Proton asks for one: open it in your +browser, solve it, then press Enter. + +## Updating + +| Installed from | Update with | +|---|---| +| COPR (Fedora) | `sudo dnf upgrade` | +| AUR | `paru -Syu`, or your AUR helper's equivalent | +| Nix flake | `nix flake update proton-drive-linux`, or change the pinned tag, then rebuild | +| `.deb`, release `.rpm`, `.tar.gz` | Download the new release and install it over the old one | + +The service switches to the new version the next time it starts. To switch at once: + +```bash +systemctl --user restart proton-drive.service +``` + +With the `.deb`, the release `.rpm` or the `.tar.gz`, nothing tells you about a new release. +Turn on **Preferences → General → Check for updates**: the app asks GitHub once a day and shows a +banner over the sidebar when a newer version is out. The app never installs updates itself. +Packages that a package manager updates do not offer the setting. A package marks itself that way +by installing the empty file `share/proton-drive-linux/managed-updates` under the prefix of its +`bin/` directory, for example `/usr/share/proton-drive-linux/managed-updates`. + +Coming from 2.8.0 or older, re-enable the service once. Those releases started it before the +desktop session was ready, and on KDE that crashes `ksecretd` at every login. Signing in again or +toggling **Start on login** does this as well: + +```bash +systemctl --user reenable proton-drive.service +``` + +Database upgrades run by themselves and only move forward. A newer release's database does not +open in an older release, so going back to an older version means clearing the local database +(see [Troubleshooting](TROUBLESHOOTING.md#start-over-with-a-clean-local-state)). + +## Uninstalling + +> [!WARNING] +> Check that nothing is waiting to upload before you remove anything. `pdfs sync queue` must be +> empty. Changes that have not reached Proton Drive exist only on this computer. + +1. Stop the service and sign out: + + ```bash + pdfs sync queue # must list nothing + systemctl --user disable --now proton-drive.service + pdfs logout # removes the session from the keyring + ``` + +2. Remove the package: + + | Installed from | Remove with | + |---|---| + | COPR (Fedora) | `sudo dnf remove proton-drive-linux`, then `sudo dnf copr remove narl-rs/proton-drive-linux` | + | Release `.rpm` | `sudo dnf remove proton-drive-linux` | + | `.deb` | `sudo apt remove proton-drive-linux` | + | AUR | `sudo pacman -R proton-drive-for-linux` (or the `-bin` or `-git` package) | + | NixOS | Remove the module and the flake input, then rebuild | + + Removing the package also stops the service of every logged-in user, in case step 1 was + skipped. + +3. If you want no trace left, remove the local data: + + ```bash + rm -rf ~/.config/proton-drive-linux ~/.local/state/proton-drive-linux ~/.cache/proton-drive-linux + ``` + +4. Sign the device out in your Proton account settings. If you no longer want this computer's + backups, remove it from **Computers** in the Proton Drive web app. + +> [!CAUTION] +> Do not delete `~/ProtonDrive` while it is mounted. It is your Proton Drive, not a copy: every +> file deleted there is moved to the Proton Drive trash. Once the service is stopped, the folder +> is empty, and removing it is safe. + +## Headless and server use + +The packaged service starts with a desktop session. On a machine without one, run the service +directly or from your own systemd unit: + +```bash +pdfs daemon # waits for a stored session, mounts, stays mounted +pdfs mount /srv/proton # mounts once at a given path, blocks until unmounted +``` + +Both still need a Secret Service provider for the session. On a server, run +`gnome-keyring-daemon --unlock` or KeePassXC's Secret Service integration inside the user's D-Bus +session. ## Build from source +### Requirements + +| | Minimum | +|---|---| +| Architecture | Any that Rust and GTK support. Release packages are x86_64 only | +| Kernel | FUSE 3 (`fuse3` package, `/dev/fuse` available to your user) | +| GTK / libadwaita | GTK 4.12, libadwaita 1.5 (1.8 or newer gets the newest widgets) | +| WebKitGTK | 6.0, used only for the CAPTCHA page during sign-in | +| Secret Service | GNOME Keyring, KWallet, KeePassXC, or another provider of `org.freedesktop.secrets` | +| Rust | 1.96 | + +The session is stored only in the Secret Service. Without a running provider, sign-in succeeds +but cannot be saved, and the service cannot start unattended. + ### Build dependencies **Debian / Ubuntu 24.04+** @@ -173,8 +315,8 @@ sudo dnf install -y cargo rust gettext pkgconf-pkg-config fuse3-devel gtk4-devel sudo pacman -S --needed rust gettext pkgconf fuse3 gtk4 libadwaita webkitgtk-6.0 libsecret dbus ``` -Install Rust 1.96 or newer with [rustup](https://rustup.rs) if your distribution ships an older -toolchain. +If your distribution ships a Rust older than 1.96, install a newer one with +[rustup](https://rustup.rs). ### Compile @@ -185,7 +327,7 @@ cargo build --release --locked po/build.sh target/locale # optional: compile the translations ``` -The four binaries are in `target/release/`: `pdfs`, `pdfs-app`, `pdfs-tray` and `pdfs-prompt`. +The four programs are in `target/release/`: `pdfs`, `pdfs-app`, `pdfs-tray` and `pdfs-prompt`. ### Build a package from the working tree @@ -211,7 +353,7 @@ sudo dnf install packaging/out/x86_64/proton-drive-linux-*.rpm ### Using a binary outside `/usr/bin` -The service unit starts `/usr/bin/pdfs daemon`. If you installed the binaries somewhere else, for +The service unit starts `/usr/bin/pdfs daemon`. If you installed the programs somewhere else, for example `~/.local/bin`, install the unit for your user and point it at your binary: ```bash @@ -223,98 +365,20 @@ systemctl --user daemon-reload ``` To start the tray at login, copy `packaging/io.narl.proton-drive-linux-tray.desktop` to -`~/.config/autostart/`. For the app launcher entry, copy `packaging/io.narl.proton-drive-linux.desktop` -to `~/.local/share/applications/` and the icon `packaging/io.narl.proton-drive-linux.svg` to -`~/.local/share/icons/hicolor/scalable/apps/`. +`~/.config/autostart/`. For the entry in your applications menu, copy +`packaging/io.narl.proton-drive-linux.desktop` to `~/.local/share/applications/` and the icon +`packaging/io.narl.proton-drive-linux.svg` to `~/.local/share/icons/hicolor/scalable/apps/`. -## First run +## What the packages install -### With the desktop app +Every package installs the same files: -1. Open **Proton Drive** from your application menu, or run `pdfs-app`. -2. Sign in with your Proton email and password, and your two-factor code if the account has one. - If Proton asks for a CAPTCHA (common on a VPN or a new network), the app shows the challenge in - a window and continues after you solve it. -3. The app enables and starts `proton-drive.service`. Your Drive appears at `~/ProtonDrive`. - -The service is tied to your graphical session. It starts after you log in and stops when you log -out. Change the folder location under **Preferences → General → Location**. - -### With the command line - -`pdfs login` stores the session but does not enable the service, so a setup that runs the daemon -another way stays untouched. Enable the service yourself: - -```bash -pdfs login # email, password, 2FA code -systemctl --user enable --now proton-drive.service -pdfs status # account, mount and queue state -``` - -The CLI cannot show a CAPTCHA. If sign-in reports one, sign in once with `pdfs-app` instead; the -session it stores is used by the CLI and the service as well. - -## Headless and server use - -The service unit is bound to `graphical-session.target`. On a machine without a desktop session, -run the daemon directly or from your own unit: - -```bash -pdfs daemon # waits for a stored session, mounts, stays mounted -pdfs mount /srv/proton # mounts once at a given path, blocks until unmounted -``` - -Both still need a Secret Service provider. On a server, run -`gnome-keyring-daemon --unlock` or KeePassXC's Secret Service integration inside the user's D-Bus -session. - -## Updating - -Install the new package over the old one. The service picks up the new binary the next time it -starts; to switch at once: - -```bash -systemctl --user restart proton-drive.service -``` - -Coming from 2.8.0 or older, re-enable the service once. Those releases hooked it into -`default.target`, which starts it before the desktop session is ready; on KDE that crashes -`ksecretd` at every login. Signing in again or toggling "Start on login" does this as well: - -```bash -systemctl --user reenable proton-drive.service -``` - -Database migrations run automatically and only move forward. A database written by a newer -release refuses to open in an older one, so downgrading means clearing the local database (see -[Troubleshooting](TROUBLESHOOTING.md#start-over-with-a-clean-local-state)). - -## Uninstalling - -> [!WARNING] -> Check that nothing is waiting to upload before you remove local state. `pdfs sync queue` must be -> empty. Queued writes exist only on this computer until they reach Proton Drive. - -```bash -pdfs sync queue # must list nothing -systemctl --user disable --now proton-drive.service -pdfs logout # removes the session from the keyring -``` - -Then remove the package (`sudo apt remove proton-drive-linux`, `sudo dnf remove -proton-drive-linux`, or `sudo pacman -R proton-drive-for-linux`). Removing the package also stops -the service of every logged-in user, in case a step above was skipped. - -> [!CAUTION] -> Do not delete `~/ProtonDrive` while it is mounted. It is your Proton Drive, not a copy: every -> file deleted there is moved to the Proton Drive trash. Once the service is stopped the folder is -> an empty mount point, and removing it is safe. - -If you want no trace left, remove the local data as well: - -```bash -rm -rf ~/.config/proton-drive-linux ~/.local/state/proton-drive-linux ~/.cache/proton-drive-linux -``` - -Sign out of the device in your Proton account settings as well, and remove the computer from -**Computers** in the Proton Drive web app if you no longer want its backups. +| Path | Purpose | +|---|---| +| `/usr/bin/pdfs` | Command-line client and background service | +| `/usr/bin/pdfs-app` | Desktop app | +| `/usr/bin/pdfs-tray` | Tray icon | +| `/usr/bin/pdfs-prompt` | Search launcher | +| `/usr/lib/systemd/user/proton-drive.service` | User service that keeps the drive mounted | +| `/etc/xdg/autostart/io.narl.proton-drive-linux-tray.desktop` | Starts the tray at login | +| `/usr/share/locale/*/LC_MESSAGES/pdfs.mo` | Translations | diff --git a/docs/LAUNCHER_AND_TRAY.md b/docs/LAUNCHER_AND_TRAY.md new file mode 100644 index 0000000..4022f34 --- /dev/null +++ b/docs/LAUNCHER_AND_TRAY.md @@ -0,0 +1,86 @@ +# Search launcher and tray + +The search launcher finds files in Proton Drive and in your home folder from a keyboard shortcut. +The tray icon shows whether everything is up to date and gives quick access to the app. + +- [Search launcher](#search-launcher) +- [Using your own launcher](#using-your-own-launcher) +- [Search as you type with fzf](#search-as-you-type-with-fzf) +- [Tray](#tray) + +## Search launcher + +![The search launcher](../images/prompt_gtk.png) + +`pdfs-prompt` searches Proton Drive and your home folder together, in one list with the best +matches first. It copes with prefixes, abbreviations, typos and swapped letters, and also matches +the names of parent folders. + +To set it up, bind `pdfs-prompt` to a keyboard shortcut in your desktop's settings. + +The window stays loaded after the first use, so later shortcuts open it instantly. Folders, music +and videos open straight from `~/ProtonDrive`, so players can start before the whole file is down. +Other files from Proton Drive download first, then open. + +To make even the first shortcut instant, start the launcher hidden when you log in. +`pdfs-prompt --preload` loads the window without showing it, and does nothing if the launcher is +already running. For example, in Hyprland: + +```ini +exec-once = pdfs-prompt --preload +bind = SUPER, space, exec, pdfs-prompt +``` + +Which program opens a file can be changed per file type. See +[Choose which program opens a file](FILES.md#choose-which-program-opens-a-file). + +## Using your own launcher + +`pdfs-prompt --dmenu` shows the results in fuzzel, rofi, wofi, tofi, bemenu or dmenu, whichever is +installed: + +```bash +pdfs-prompt --dmenu +pdfs-prompt --dmenu --menu 'fuzzel --dmenu --width 60' +pdfs-prompt --dmenu --query invoice +``` + +These launchers filter a fixed list and cannot ask for new results while you type, so the search +takes two steps: + +1. The first list, `Search Drive ›`, shows your available-offline files. Type a search and press + Enter. +2. The second list, `Drive: invoice ›`, shows the results. Enter opens one. Typing something else + searches again. + +## Search as you type with fzf + +![The search launcher in fzf](../images/prompt_fzf.png) + +`pdfs-prompt --fzf` searches again on every key press, like the built-in window. It needs `fzf`. +Started from a keyboard shortcut, it opens a terminal by itself (foot, ghostty, kitty, alacritty, +wezterm or xterm) with the app ID `pdfs-prompt`, so one window rule covers all of them. For +example, in Hyprland: + +```ini +windowrulev2 = float, class:^(pdfs-prompt)$ +windowrulev2 = size 900 500, class:^(pdfs-prompt)$ +windowrulev2 = center, class:^(pdfs-prompt)$ +bind = SUPER, space, exec, pdfs-prompt --fzf +``` + +To make `--dmenu` or `--fzf` the default without changing your shortcut, set `prompt.mode` in +`config.json` (see [Configuration → `prompt`](CONFIGURATION.md#prompt)). `--gtk` always opens the +built-in window. + +## Tray + +`pdfs-tray` starts when you log in and shows whether your files are up to date, syncing, paused or +offline. Its menu opens the app or `~/ProtonDrive`, pauses or resumes sync, and stops or connects +the service. + +- **GNOME** shows tray icons only with the + [AppIndicator extension](https://extensions.gnome.org/extension/615/appindicator-support/). + KDE Plasma and most other desktops show it without help. +- **Hide Tray Icon** in its menu removes it until you turn it back on under + **Preferences → General → Appearance → Show tray icon**. diff --git a/docs/PHOTOS.md b/docs/PHOTOS.md new file mode 100644 index 0000000..80ee746 --- /dev/null +++ b/docs/PHOTOS.md @@ -0,0 +1,77 @@ +# Photos + +The **Photos** page of the app shows your Proton Photos library. Photos are stored apart from +My files, so they do not appear in `~/ProtonDrive`. + +- [Timeline](#timeline) +- [Albums](#albums) +- [Places](#places) +- [Viewer and duplicates](#viewer-and-duplicates) +- [Importing from Google Photos](#importing-from-google-photos) +- [Fixing photos filed under the wrong date](#fixing-photos-filed-under-the-wrong-date) + +![The Photos timeline](../images/photos.png) + +## Timeline + +- The timeline groups photos by day, month or year as you zoom. Drag the scrubber on the right + edge to jump through the years. +- **On this day** shows photos taken on the same date in earlier years. +- **Filters** narrow the timeline to images, videos or RAW files, to favorites, or to photos that + are in no album. +- **RAW + JPEG pairs** of the same shot show as one tile. The viewer switches between the two, and + moving the tile to the Trash removes both. +- **Upload** adds photos from this computer. + +## Albums + +**Albums** lists your own albums and the albums others share with you. Create, rename and delete +albums, and add or remove photos. Deleting an album keeps its photos in the timeline. + +## Places + +![Places on the map](../images/photos_map.png) + +**Places** groups photos by the town they were taken in, as cards or on a map. Town names come +from [GeoNames](https://www.geonames.org/) data (CC BY 4.0) and the map from +[Natural Earth](https://www.naturalearthdata.com/) (public domain). Both ship with the app, so +nothing is fetched online. + +A street map from [OpenFreeMap](https://openfreemap.org/) (© OpenStreetMap contributors) can be +turned on under **Preferences → General → Appearance**. It is off by default, because the map +server learns which areas you look at. + +## Viewer and duplicates + +- The viewer marks favorites, shows EXIF details, shows a photo on the map, and runs a slideshow + (F5). +- **Find Duplicates…** in the page menu lists byte-identical copies. Turn on **Similar** to also + find resized or re-encoded copies and burst shots. + +## Importing from Google Photos + +1. Request a [Google Takeout](https://takeout.google.com/) export of Google Photos and download + every `.zip` of it. +2. In the app, open the **Upload** menu on the Photos page and choose **Import from Google + Photos…**. Select all the `.zip` files at once, because a photo and its details are often in + different parts of the export. + +From a terminal: + +```bash +pdfs import-google-photos --dry-run ~/Downloads/takeout-*.zip # show what would happen +pdfs import-google-photos --wait ~/Downloads/takeout-*.zip +``` + +The import recreates your albums and takes capture dates and locations from the export. Photos +already in your library are skipped, so an interrupted import can simply be run again. +`pdfs import-status` shows how far an import is, and `pdfs cancel-import` stops it. + +## Fixing photos filed under the wrong date + +Photos from an earlier import sometimes sit under the day they were uploaded instead of the day +they were taken. `pdfs redate-photos --dry-run` finds those whose file name carries a different +date, and `pdfs redate-photos` fixes them. + +Proton cannot change a capture date in place. So each fix uploads the photo again with the right +date and moves the original to the Trash. Favorites and albums are kept. diff --git a/docs/README.md b/docs/README.md index afe77f7..8a474a6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,19 +1,48 @@ -# Documentation +# Proton Drive for Linux documentation -## For users +Proton Drive for Linux is an unofficial Proton Drive client for the Linux desktop. Your Drive +appears as a folder that downloads files when you open them, local folders are backed up and kept +in sync, and a GTK app with a tray icon and a search launcher covers Photos, sharing and more. A +command-line client does all of it from a terminal. + +These pages are also published as the +[wiki](https://github.com/narrrl/proton-drive-linux/wiki), generated from the +[`docs/`](https://github.com/narrrl/proton-drive-linux/tree/main/docs) folder of the repository. + +## Where to start + +- **New here?** [Getting started](GETTING_STARTED.md) takes you from installing to your first + synced folder. +- **Installing or updating?** [Installation](INSTALL.md) covers Fedora (COPR), Arch (AUR), + Debian and Ubuntu, NixOS, and building from source. +- **Something not working?** [Troubleshooting](TROUBLESHOOTING.md) starts with `pdfs diagnose` + and goes through common problems. +- **New computer, or the old one is gone?** [Recovering on a new computer](RECOVERY.md) gets your + synced folders back. +- **Quick question?** Try the [FAQ](FAQ.md). + +## Using it + +| Page | What it covers | +|---|---| +| [User guide](USER_GUIDE.md) | How the pieces fit together, the words the app uses, preferences | +| [Files and offline access](FILES.md) | The `~/ProtonDrive` folder, keeping files available offline, the Trash, earlier versions | +| [Synced folders](SYNCED_FOLDERS.md) | Backing up local folders, excluding files, changes waiting to upload, conflicts, pausing | +| [Photos](PHOTOS.md) | Timeline, albums, places, duplicates, importing from Google Photos | +| [Sharing](SHARING.md) | Invitations, public links, items shared with you | +| [Computers](COMPUTERS.md) | Other computers in your account, syncing several computers | +| [Search launcher and tray](LAUNCHER_AND_TRAY.md) | Finding files from a keyboard shortcut, the tray icon | + +## Reference | Page | What it covers | |---|---| -| [Installation](INSTALL.md) | Packages, building from source, first sign-in, updating and uninstalling | -| [User guide](USER_GUIDE.md) | My files, available offline, synced folders, sync issues and conflicts, Photos, sharing, Trash and versions, Computers, the search launcher, the tray | | [Command-line reference](CLI.md) | Every `pdfs` command, grouped by task, and scripting with `--json` | | [Configuration](CONFIGURATION.md) | `config.json`, environment variables, file locations, the systemd service | -| [Troubleshooting](TROUBLESHOOTING.md) | Diagnosing problems, common symptoms, starting over, reporting a bug | -| [Recovering on a new computer](RECOVERY.md) | What survives a lost machine and how to restore your synced folders | | [Security model](SECURITY_MODEL.md) | What protects your files, what is kept on this computer, what the client connects to | | [Changelog](CHANGELOG.md) | What changed in each release | -## For contributors +## Contributing | Page | What it covers | |---|---| @@ -24,5 +53,5 @@ | [Roadmap](ROADMAP.md) | Planned work and verification still owed | | [Bug ledger](BUGS.md) | Every tracked defect with its analysis and status | -Also see [CONTRIBUTING.md](../CONTRIBUTING.md) and [SECURITY.md](../SECURITY.md) in the repository -root. +Also see [CONTRIBUTING.md](../CONTRIBUTING.md), and [SECURITY.md](../SECURITY.md) to report a +vulnerability privately. diff --git a/docs/RECOVERY.md b/docs/RECOVERY.md index b6a58f4..3fc7c24 100644 --- a/docs/RECOVERY.md +++ b/docs/RECOVERY.md @@ -3,6 +3,10 @@ Your laptop is lost, stolen or reinstalled. This page explains what survives, and how to get your synced folders back on the replacement machine. +**In short:** sign the old machine out of your Proton account. Then, on the new one, install the +client and sign in. On the **Computers** page, choose **Continue This Backup Here…** on the old +computer, then **Restore Folders…** on this one. + - [1. What survives](#1-what-survives) - [2. Restore your synced folders](#2-restore-your-synced-folders) - [3. How the restore works](#3-how-the-restore-works) @@ -52,7 +56,7 @@ be decrypted. ### Step 1: Install and sign in -Install the client as described in [INSTALL.md](INSTALL.md) and sign in with `pdfs-app`. Signing +Install the client as described in [Installation](INSTALL.md) and sign in with `pdfs-app`. Signing in from the app also starts the service. ### Step 2: Continue the old computer's backup @@ -100,7 +104,7 @@ progress on **Sync → Overview** or with `pdfs sync list`. > Restore into empty or missing directories. If a directory already holds files, for example > from another backup, the client cannot tell which side is newer. It keeps both: your local file > is renamed to a `(sync-conflict …)` copy and the version from Proton Drive is downloaded. See -> [Conflicts](USER_GUIDE.md#conflicts). +> [Conflicts](SYNCED_FOLDERS.md#conflicts). ### Step 4: Restore the rest by hand @@ -191,4 +195,4 @@ This section is for the curious and for anyone debugging a restore. | No warning at shutdown or sign-out while changes are queued | Check `pdfs sync queue` before you retire a machine | | The full disaster drill (delete all local state, restore, compare byte for byte) has not been run against a live account | The individual steps are tested; the whole sequence is not | -Planned work is tracked in [ROADMAP.md](ROADMAP.md). +Planned work is tracked in the [roadmap](ROADMAP.md). diff --git a/docs/SECURITY_MODEL.md b/docs/SECURITY_MODEL.md index 89be444..a7095b2 100644 --- a/docs/SECURITY_MODEL.md +++ b/docs/SECURITY_MODEL.md @@ -60,8 +60,12 @@ The client connects only to: - **Proton**, for the Drive and account API, and for the CAPTCHA page when Proton asks for one. - **OpenFreeMap**, for the street map of Places in Photos, only if you turn it on in Preferences. It is off by default, because the tile server learns which parts of the world you look at. The default map is drawn from data shipped with the app. +- **GitHub**, once a day, to ask for the latest release, only if you turn on **Check for updates** + in Preferences. It is off by default, because GitHub learns your IP address and, from the user + agent, which version you run. Packages updated by a package manager (COPR, the AUR, Nix) do + not offer the setting and never ask. -There is no telemetry, no analytics and no update check. The SDK can report timings of its own +There is no telemetry and no analytics. The SDK can report timings of its own operations, and the client only writes them to its local log. ## What this client does not protect against @@ -75,7 +79,9 @@ operations, and the client only writes them to its local log. ## Packages -- The `.rpm` is signed with GPG. +- The `.rpm` on the release page is signed with GPG. +- The COPR packages are built by Fedora's COPR service from the tagged source and the vendored + crates of the release, and are signed with the COPR project's own key. - The `.deb` and the `.tar.gz` are built by GitHub Actions from the tagged source, and are not signed. - The AUR packages build from the tagged source or from `main`, or install the release binaries diff --git a/docs/SHARING.md b/docs/SHARING.md new file mode 100644 index 0000000..a1e5d65 --- /dev/null +++ b/docs/SHARING.md @@ -0,0 +1,52 @@ +# Sharing + +Share files and folders with other people, by invitation or with a public link, and use what +others share with you. + +- [Invite people](#invite-people) +- [Create a public link](#create-a-public-link) +- [See what you share](#see-what-you-share) +- [Shared with you](#shared-with-you) + +## Invite people + +1. Select a file or folder and choose **Share…**. +2. Enter one or more email addresses and choose **Can view** or **Can edit**. +3. Send the invitation. People appear as members once they accept. + +The admin role, which may also manage the share, is available from the command line +(`--role admin`). + +From a terminal: + +```bash +pdfs share Documents/Plans alex@example.com --role editor +pdfs members Documents/Plans # members, pending invitations and the public link +``` + +`pdfs share-role` changes someone's role, and `pdfs unshare` removes one person or stops sharing +altogether. See the [command-line reference](CLI.md#sharing). + +## Create a public link + +In **Share…**, create a public link. Anyone with the link can open the item. You can protect the +link with a password and let it expire on a date. From a terminal, use `pdfs public-link create` +and `pdfs public-link remove`. + +## See what you share + +**Shared by me** lists everything you share. Filter it by links, by people, or by expired links. +Copy a link that is still valid, or stop several shares at once. `pdfs shared` prints the same +list. + +## Shared with you + +**Shared with me** lists what other people share with you, who shared it, and what you may do with +it. The same items are in the `Shared with me` folder inside `~/ProtonDrive`. Items where you are +a viewer are read-only. + +- **Invitations** wait in a banner at the top of the page until you accept or reject them. From a + terminal: `pdfs invitations list`, then `accept ID` or `reject ID`. +- **Saved public links** have their own view. `pdfs bookmarks` manages them. +- **Leave…** a share you no longer need, or run `pdfs leave UID` with the ID from + `pdfs shared-with-me`. diff --git a/docs/SYNCED_FOLDERS.md b/docs/SYNCED_FOLDERS.md new file mode 100644 index 0000000..aed940b --- /dev/null +++ b/docs/SYNCED_FOLDERS.md @@ -0,0 +1,163 @@ +# Synced folders + +A synced folder is an ordinary folder on this computer, for example `~/Documents`, that is backed +up to Proton Drive and kept in step with it. This page covers adding one, what to do with files +that should not be backed up, changes that are still waiting to upload, conflicts, and pausing. + +- [Add a folder](#add-a-folder) +- [Synced or online only](#synced-or-online-only) +- [Move files between synced folders and My files](#move-files-between-synced-folders-and-my-files) +- [Stop syncing a folder](#stop-syncing-a-folder) +- [Excluding files with `.pdfsignore`](#excluding-files-with-pdfsignore) +- [Changes waiting for Proton Drive](#changes-waiting-for-proton-drive) +- [Conflicts](#conflicts) +- [Pausing and limiting sync](#pausing-and-limiting-sync) + +![Synced folders in the app](../images/sync.png) + +## Add a folder + +- **In the app:** open **Computers** and choose **Add Folder to Sync**. +- **From a terminal:** `pdfs sync add ~/Documents` + +The folder is backed up under this computer's name. It shows on the **Computers** page of the app +and in the Proton Drive web app. + +From then on, changes go both ways: + +- A file you change here uploads a few seconds after the program closes it. A file that a + program still holds open for writing waits until it is closed. +- A file changed somewhere else downloads on the next sync pass. + +**Sync → Folders** in the app, and `pdfs sync list`, show every synced folder and its state. + +To sync the same folders on two computers, see [Syncing several computers](COMPUTERS.md#syncing-several-computers). + +## Synced or online only + +A synced folder normally keeps a full copy on this computer. You can switch it to **Online +only**: the files stay visible in the same place, but take no disk space until you open them, the +same as in `~/ProtonDrive`. + +- **In the app:** use the **Synced / Online only** switch on the folder's card. +- **From a terminal:** `pdfs sync mode ID ondemand`, and `pdfs sync mode ID mirror` to switch + back. `pdfs sync list` shows the ID. + +The switch waits until everything in the folder is on Proton Drive. It refuses while the folder +holds something that never goes there, such as an excluded `.git/` folder, a symbolic link, or a +folder you may not read. The message names what is in the way. + +`pdfs locations` lists every place on this computer the Proton Drive service uses. + +## Move files between synced folders and My files + +`~/ProtonDrive`, each online-only folder and each synced folder are separate places for your +system. Moving a file between them with `mv` or a file manager copies and then deletes: every file +is downloaded, uploaded again, and the original goes to the Trash. + +Use **Move to…** in the app or `pdfs move` instead. The move happens on Proton Drive and takes +seconds, whatever the size: + +```sh +pdfs move ~/Documents/Scans ~/ProtonDrive/Archive +pdfs move ~/Documents/a.pdf ~/Documents/b.pdf ~/ProtonDrive/Archive +``` + +The last path is the destination folder. Everything before it moves there. + +A move out of a synced folder waits until its files are on Proton Drive. A file that is still +uploading, has changes that are not uploaded yet, or is excluded by `.pdfsignore` stops the move, +and the message names it. Files you already opened in an online-only folder are not downloaded +again when they move into a synced one. + +## Stop syncing a folder + +**Stop Syncing** in the app, or `pdfs sync rm ID`, keeps both the local files and the copy on +Proton Drive. To delete the copy on Proton Drive as well, tick **Also delete from Proton Drive**, +or add `--delete-remote`. + +## Excluding files with `.pdfsignore` + +Synced folders skip build output, dependency folders and editor leftovers. Two sets of rules +apply together: + +1. A `.pdfsignore` file at the top of the synced folder (`.protonignore` works too). It uses the + same syntax as `.gitignore`, including `!` to include something again: + + ```gitignore + build/ + *.log + !important.log + ``` + +2. The `ignore_patterns` list in `config.json`, for every synced folder (see + [Configuration](CONFIGURATION.md#ignore_patterns)). When you have not set it, it skips + `.git/`, `.hg/`, `.svn/`, `node_modules/`, `target/`, `.venv/`, `__pycache__/`, `*~`, + `*.swp`, `*.tmp`, `.DS_Store` and `Thumbs.db`. + +Rules are read again on every sync pass. Excluding never deletes anything: a file that becomes +excluded is no longer synced, and its copy on Proton Drive stays. + +A folder you may not read, such as a cache that a container created as root, is skipped the same +way, and the activity feed mentions it once. Add it to `.pdfsignore` to stop the message. + +## Changes waiting for Proton Drive + +A change you make exists only on this computer until it reaches Proton Drive. `pdfs status`, the +tray and **Sync → Overview** count what is still waiting, and `pdfs sync queue` lists it. + +Sometimes Proton Drive refuses a change: the storage is full, a share was taken away, the folder +is gone, the folder holds too many items, or Proton Drive does not accept the name. The change +then needs you. It shows at once in **Sync → Overview**, in the tray and in `pdfs sync issues`, +with what it means and what you can do. The file or folder also gets a warning badge in +**Files**. Hover over it to see why. + +Nothing is deleted. The change stays queued and is tried again. You can: + +- **Retry** it now, for example after freeing space: the same list, or `pdfs sync retry`. +- **Export** a file's content to keep your version: `pdfs sync export ID FILE`. +- **Discard** the change: `pdfs sync discard ID`. It is undone here and never sent. A new file or + folder goes away, and anything else goes back to what Proton Drive has. Export first if you want + to keep the content. + +Two cases are not refused, and the activity log records both: + +- A new file or folder whose name is already taken on Proton Drive, by something this computer + had not seen yet, lands under a `(sync-conflict …)` name. See [Conflicts](#conflicts). +- A new file whose folder was deleted on another device in the meantime lands at the top of My + files, so its content is not lost. + +> [!IMPORTANT] +> Before you switch off or wipe a computer for good, check that nothing is waiting. Changes that +> have not reached Proton Drive are lost with the machine. See +> [Recovery](RECOVERY.md#lost-with-the-machine). + +## Conflicts + +![Sync history with resolved conflicts](../images/conflicts.png) + +When the same file changed on this computer and somewhere else since the last sync, neither +version is thrown away: + +- The other version keeps the original name. +- Your local version is kept as a copy named like + `report (sync-conflict laptop 1759068180).odt`. `laptop` is the computer that made the copy, + and the number is the time of the conflict. Copies made before 3.5.0 have no computer name. + +Conflicts show under **Sync → Overview → Needs attention** and in `pdfs conflicts`. For each one, +keep the original, keep the copy, or keep both under a new name: + +```bash +pdfs conflicts +pdfs conflicts resolve "Documents/report (sync-conflict laptop 1759068180).odt" --keep copy +``` + +The version you do not keep goes to the Trash, so a wrong choice can be undone. + +## Pausing and limiting sync + +- **Pause** from the tray, the **Sync** page, or `pdfs sync pause`. Uploads and synced folders + wait, and opening files keeps working. `pdfs sync pause --for 1h` resumes by itself, and + `pdfs sync resume` continues at once. +- **Limit bandwidth** under **Preferences → General → Network**, or with + `pdfs sync limit --up 2M --down 10M`. `0` removes a limit. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 53b9b92..5059b1b 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -62,7 +62,7 @@ journalctl --user -u proton-drive.service -b -p warning # warnings since boot ``` For more detail, raise the log level with a drop-in (see -[CONFIGURATION.md](CONFIGURATION.md#the-systemd-service)) and restart the service. +[Configuration](CONFIGURATION.md#the-systemd-service)) and restart the service. ### Service state @@ -73,97 +73,124 @@ pdfs status ## Signing in -**"Proton is asking for a CAPTCHA"** — Proton wants human verification, usually on a VPN or an -unfamiliar network. `pdfs login` prints the verification page's address: open it in your browser, -complete it, then press Enter. The app shows the page in its own window. +### Proton is asking for a CAPTCHA -**"Verify in Your Browser"** — WebKit, which shows the CAPTCHA page in the app, could not start -its sandbox. Hardened systems such as secureblue keep its bubblewrap from the journal socket, and -the log then shows `bwrap: Can't get type of source /run/systemd/journal/socket: Permission -denied` and `Failed to fully launch dbus-proxy`. Choose **Open Verification Page**, complete the -page in your browser, then choose **Continue**. If that sign-in is refused too, check that `bwrap` -and `xdg-dbus-proxy` are installed. As a last resort, sign in once with the sandbox switched off -for this one run: +Proton wants to check that you are human, usually on a VPN or an unfamiliar network. The app +shows the page in its own window. `pdfs login` prints the page's address instead: open it in your +browser, complete it, then press Enter. + +### "Verify in Your Browser" + +WebKit, which shows the CAPTCHA page in the app, could not start its sandbox. Hardened systems +such as secureblue keep its bubblewrap away from the journal socket. The log then shows +`bwrap: Can't get type of source /run/systemd/journal/socket: Permission denied` and +`Failed to fully launch dbus-proxy`. + +1. Choose **Open Verification Page** and complete the page in your browser. +2. Choose **Continue**. + +If that sign-in is refused too, check that `bwrap` and `xdg-dbus-proxy` are installed. As a last +resort, quit the app and sign in once with the sandbox switched off for this one run: ```bash WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 pdfs-app ``` -Quit the app first, so this starts a new instance. The page then runs without WebKit's isolation, -so use this only for the sign-in. The stored session keeps working without it. Signing in from -your usual network, without a VPN, often avoids the CAPTCHA in the first place. +The page then runs without WebKit's isolation, so use this only for the sign-in. The stored +session keeps working without it. Signing in from your usual network, without a VPN, often avoids +the CAPTCHA in the first place. -**"This version of the app is no longer supported"** or **"Proton is temporarily refusing -sign-in from this device"** — Proton blocks sign-in for a while after too many attempts or from a -network it distrusts, and sends the first message even when the release is current. Do not retry -for several hours, up to a day, then sign in once. If it still fails, run -`RUST_LOG=debug pdfs login` and look for `code=` in the `proton api request failed` line: 2028 is -this block, 5003 is a release Proton no longer accepts. +### "This version of the app is no longer supported" or "Proton is temporarily refusing sign-in from this device" -**Sign-in works but is forgotten after a restart** — no Secret Service provider is running, or its -keyring is locked. Install and unlock GNOME Keyring, KWallet or KeePassXC (with Secret Service -enabled). `pdfs diagnose` shows `keyring session` as failing in that case. When the app finds -a saved sign-in it cannot read, it says so above the sign-in form and opens on its own once the -keyring is unlocked. You don't have to sign in again. +Proton blocks sign-in for a while after too many attempts, or from a network it distrusts. It sends +the first message even when your version is current. Do not retry for several hours, up to a day, +then sign in once. -**Asked for the mailbox password after every reboot** — the account has a separate mailbox -password (two-password mode). Such accounts are locked by default: the unlock lasts until you log -out or restart. To stay unlocked, turn on **Keep unlocked on this computer** in the unlock dialog, -or run `pdfs unlock --remember`. Either one stores the unlock in the system keyring. -[SECURITY_MODEL.md](SECURITY_MODEL.md) explains what that trades away. +If it still fails, run `RUST_LOG=debug pdfs login` and look for `code=` in the +`proton api request failed` line. 2028 is this block; 5003 is a release Proton no longer accepts. + +### Sign-in works but is forgotten after a restart + +No Secret Service provider is running, or its keyring is locked. Install and unlock GNOME +Keyring, KWallet or KeePassXC (with Secret Service turned on). `pdfs diagnose` then shows +`keyring session` as failing. + +When the app finds a saved sign-in it cannot read, it says so above the sign-in form, and opens +by itself once the keyring is unlocked. You do not have to sign in again. + +### Asked for the mailbox password after every reboot + +The account has a separate mailbox password (two-password mode). Such accounts are locked by +default: the unlock lasts until you log out or restart. To stay unlocked, turn on **Keep unlocked +on this computer** in the unlock dialog, or run `pdfs unlock --remember`. Either one stores the +unlock in the system keyring. The [security model](SECURITY_MODEL.md) explains what that trades +away. ## The Proton Drive folder > [!TIP] > When the service hangs, any program that touches the Proton Drive folder can hang with it, -> including `ls` and your shell's prompt. Prefix exploratory commands with `timeout 5`. +> including `ls` and your shell's prompt. Put `timeout 5` in front of commands you try. -**`Transport endpoint is not connected`** — the service stopped without unmounting. Unmount and -restart: +### `Transport endpoint is not connected` + +The service stopped without unmounting. Unmount and restart: ```bash fusermount3 -uz ~/ProtonDrive systemctl --user restart proton-drive.service ``` -**`fetch My Files root: … failed to decrypt: missing key`** in the service log — the account -keys cannot open your Drive volume, which happens after a password reset. Proton locks the volume -that was encrypted to the old keys. From 3.4.1 the client starts a new, empty My Files volume -in that case. Your old files stay in the locked volume: open [drive.proton.me](https://drive.proton.me) -and restore them there with your previous password. If the volume is not locked and the error -stays, run `pdfs diagnose` and post its **Keys** section in an issue. +### The folder is empty or missing -**The folder is empty or missing** — check that the service runs and is signed in -(`pdfs status`). The service waits for a stored session before it mounts. +Check that the service runs and is signed in, with `pdfs status`. The service waits for a stored +session before it shows your files. -**Everything in the folder hangs** — find out what the service is doing, then restart it: +### `fetch My Files root: … failed to decrypt: missing key` in the log + +Your account keys cannot open your Drive, which happens after a password reset: Proton locks the +files that were encrypted with the old keys. Since 3.4.1 the client then starts a new, empty My +files. Your old files are still there, locked: open [drive.proton.me](https://drive.proton.me) and +restore them with your previous password. If your files are not locked and the error stays, run +`pdfs diagnose` and post its **Keys** section in an issue. + +### Everything in the folder hangs + +Find out what the service is doing, then restart it: ```bash timeout 10 pdfs diagnostics systemctl --user restart proton-drive.service ``` -The service watchdog restarts a service that stops answering for two minutes by itself. Attach the -`pdfs diagnostics` output to a bug report; it shows which worker is stuck. +The service restarts by itself when it stops answering for two minutes. Attach the +`pdfs diagnostics` output to a bug report; it shows which part is stuck. -**A change made on another device does not show up** — changes arrive through Proton's event -stream, normally within seconds. To fetch a folder again at once: +### A change made on another device does not show up + +Changes from other devices normally arrive within seconds. To fetch a folder again at once: ```bash pdfs refresh Documents ``` -**"Permission denied" when writing** — the item is in a share where you are a viewer, or under -`Shared with me/`, which is read-only as a folder. Check your role with `pdfs shared-with-me`. +### "Permission denied" when writing -**`ln` or another program fails with "Operation not permitted"** — Proton Drive cannot store -symbolic links, hard links, device files or FIFOs, so creating one fails at once. The error is -intentional. Use a synced folder for projects that need them, or exclude those paths. +The item is in a share where you are a viewer, or directly under `Shared with me/`, which is +read-only. Check your role with `pdfs shared-with-me`. + +### `ln` or another program fails with "Operation not permitted" + +Proton Drive cannot store symbolic links, hard links, device files or FIFOs, so creating one fails +at once. This is on purpose. For projects that need them, use a +[synced folder](SYNCED_FOLDERS.md) and exclude those paths. ## Uploads and sync -**A change shows here but not on other devices** — changes are recorded on this computer first -and sent from a queue. List what is still waiting, and what needs attention: +### A change shows here but not on other devices + +Changes are recorded on this computer first and sent from a queue. List what is still waiting, +and what needs you: ```bash pdfs sync queue @@ -171,72 +198,89 @@ pdfs sync issues ``` The app shows the same on **Sync → Overview**. A change waits while the service is offline or -sync is paused (`pdfs status`), and backs off after a failure. To retry at once: +sync is paused (`pdfs status`), and waits longer after each failure. To retry at once: ```bash -pdfs sync retry # every failed operation -pdfs sync retry 42 # one operation +pdfs sync retry # every failed change +pdfs sync retry 42 # one change ``` A change Proton Drive refuses (storage full, access taken away, folder gone) is listed by `pdfs sync issues` with what to do. It stays queued until you fix the cause or drop it. Dropping -it undoes it here; save a file's content first if you want to keep it: +it undoes it here, so save a file's content first if you want to keep it: ```bash pdfs sync export 42 ~/Desktop/ pdfs sync discard 42 ``` -If you suspect the local-first behaviour of 3.0.0 itself, `"local_first": false` in `config.json` -makes the Proton Drive folder wait for Proton Drive again while online (see -[CONFIGURATION.md](CONFIGURATION.md#local_first)), and please report the problem. +[Changes waiting for Proton Drive](SYNCED_FOLDERS.md#changes-waiting-for-proton-drive) explains +the choices. -**A file in a synced folder does not upload** — check that: +If you suspect that the instant, queued changes introduced in 3.0.0 are the problem, +`"local_first": false` in `config.json` makes the Proton Drive folder wait for Proton Drive again +while online (see [Configuration](CONFIGURATION.md#local_first)). Please report the problem too. -- it is not matched by `.pdfsignore` or `ignore_patterns`; +### A file in a synced folder does not upload + +Check that: + +- it is not excluded by `.pdfsignore` or `ignore_patterns`; - no program still holds it open for writing (such files wait until closed); -- sync is not paused (`pdfs status`, tray); -- the folder has no error on **Sync → Folders**. +- sync is not paused (`pdfs status`, or the tray); +- the folder shows no error on **Sync → Folders**. -`pdfs sync now` starts a pass immediately. +`pdfs sync now` starts a sync pass at once. -**Files named `(sync-conflict …)` appeared** — the file changed in two places at once. Nothing was -lost; see [Conflicts](USER_GUIDE.md#conflicts) to pick a version. +### Files named `(sync-conflict …)` appeared -**Photos show the wrong date after a Google Takeout import** — run -`pdfs redate-photos --dry-run`, see [USER_GUIDE.md](USER_GUIDE.md#importing-from-google-photos). +The file changed in two places at once. Nothing was lost. See +[Conflicts](SYNCED_FOLDERS.md#conflicts) to pick a version. + +### Photos show the wrong date after a Google Takeout import + +Run `pdfs redate-photos --dry-run`. See +[Fixing photos filed under the wrong date](PHOTOS.md#fixing-photos-filed-under-the-wrong-date). ## Disk space | What | Check | Reduce | |---|---|---| -| Content cache | `pdfs cache inspect` | Lower **Preferences → Storage → Cache size limit**, or `pdfs cache clear` | +| Cache of opened files | `pdfs cache inspect` | Lower **Preferences → Storage → Cache size limit**, or `pdfs cache clear` | | Available-offline files | `pdfs pins` | `pdfs unpin PATH` | | Metadata database | `pdfs cache inspect` (reclaimable) | `pdfs cache vacuum` | | Synced folders | `pdfs sync list` | Switch large folders to online only: `pdfs sync mode ID ondemand` | -`pdfs cache vacuum` locks the database while it runs and needs free space for a second copy of it. +`pdfs cache vacuum` locks the database while it runs, and needs free space for a second copy of +it. ## Desktop integration -**No tray icon on GNOME** — install and enable the AppIndicator and KStatusNotifierItem Support -extension. Other desktops show the icon natively. If you hid it from its own menu, turn it back on -under **Preferences → General → Appearance**. +### No tray icon on GNOME -**The search launcher does nothing** — `pdfs-prompt` needs the service. Run it from a terminal to -see its error. For `--fzf`, install `fzf` and a supported terminal. +Install and turn on the +[AppIndicator and KStatusNotifierItem Support](https://extensions.gnome.org/extension/615/appindicator-support/) +extension. Other desktops show the icon without help. If you hid it from its own menu, turn it +back on under **Preferences → General → Appearance**. -**The app shows "Not responding"** — the service runs but does not answer within five seconds. -See [Everything in the folder hangs](#the-proton-drive-folder). +### The search launcher does nothing + +`pdfs-prompt` needs the service. Run it from a terminal to see its error. For `--fzf`, install +`fzf` and a supported terminal. + +### The app shows "Not responding" + +The service runs but does not answer within five seconds. See +[Everything in the folder hangs](#everything-in-the-folder-hangs). ## Start over with a clean local state -This removes the local database and cache so they are rebuilt from Proton Drive. It is the fix for -a damaged database and the only way to downgrade to an older release. +This removes the local database and cache, so they are built again from Proton Drive. It fixes a +damaged database, and it is the only way to go back to an older release. > [!WARNING] > Content in `staging/` and `recovery/` has not reached Proton Drive yet. Deleting it loses those -> changes permanently. Wait until `pdfs sync queue` is empty. +> changes for good. Wait until `pdfs sync queue` is empty. ```bash pdfs sync queue # must list nothing @@ -247,8 +291,8 @@ systemctl --user start proton-drive.service ``` Your session is in the keyring and your settings in `~/.config`, so you stay signed in. Synced -folders are not re-attached automatically; restore them with `pdfs sync restore` as described in -[RECOVERY.md](RECOVERY.md#2-restore-your-synced-folders). Delete the `.bak` directories once +folders are not attached again by themselves. Restore them with `pdfs sync restore`, as described +in [Recovery](RECOVERY.md#2-restore-your-synced-folders). Delete the `.bak` directories once everything is back. ## Reporting a problem @@ -259,7 +303,7 @@ Include: - the output of `pdfs diagnose`; - relevant log lines from `journalctl --user -u proton-drive.service`; - for a hang, the output of `timeout 10 pdfs diagnostics`; -- the steps that reproduce it. +- the steps that make it happen. Logs contain file names. Remove anything private before posting. Report security problems -privately as described in [SECURITY.md](../SECURITY.md). +privately, as described in [SECURITY.md](../SECURITY.md). diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index c8292d8..dad46f7 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -1,29 +1,27 @@ # User guide -How Proton Drive for Linux works and how to use it day to day. For installation see -[INSTALL.md](INSTALL.md); every command mentioned here is described in [CLI.md](CLI.md). +How Proton Drive for Linux works and how to use it day to day. To install it, see +[Installation](INSTALL.md). If you are new, [Getting started](GETTING_STARTED.md) is the quicker +read. -- [How it works](#how-it-works) -- [My files](#my-files) -- [Keeping files on this computer](#keeping-files-on-this-computer) -- [Syncing local folders](#syncing-local-folders) -- [Changes waiting for Proton Drive](#changes-waiting-for-proton-drive) -- [Conflicts](#conflicts) -- [Pausing and limiting sync](#pausing-and-limiting-sync) -- [Photos](#photos) -- [Sharing](#sharing) -- [Trash and version history](#trash-and-version-history) -- [Computers](#computers) -- [Search launcher](#search-launcher) -- [Choosing how files open](#choosing-how-files-open) -- [Tray](#tray) -- [Preferences](#preferences) +## Topics + +| Page | What it covers | +|---|---| +| [Files and offline access](FILES.md) | The `~/ProtonDrive` folder, keeping files available offline, the Trash, earlier versions | +| [Synced folders](SYNCED_FOLDERS.md) | Backing up local folders, excluding files, changes waiting to upload, conflicts, pausing | +| [Photos](PHOTOS.md) | Timeline, albums, places, duplicates, importing from Google Photos | +| [Sharing](SHARING.md) | Invitations, public links, items shared with you | +| [Computers](COMPUTERS.md) | Other computers in your account, syncing several computers | +| [Search launcher and tray](LAUNCHER_AND_TRAY.md) | Finding files from a keyboard shortcut, the tray icon | + +Every command mentioned in these pages is listed in the +[command-line reference](CLI.md). ## How it works A background service, `proton-drive.service`, runs `pdfs daemon`. It holds your session, talks to -Proton Drive, and keeps your Drive mounted as a folder. Everything else is a front end that asks -the service to do things over a local socket: +Proton Drive, and shows your Drive as a folder. Everything else asks the service to do things: | Program | What it is | |---|---| @@ -32,357 +30,30 @@ the service to do things over a local socket: | `pdfs-prompt` | A search launcher for a keyboard shortcut | | `pdfs` | The command-line client, for everything above and for scripts | -Your Drive, your file names and your photos are end-to-end encrypted by Proton. On this computer, -the service must decrypt what it shows you, and it keeps a decrypted cache and index on disk. Use -full-disk or home-directory encryption. [ARCHITECTURE.md §10](ARCHITECTURE.md#10-threat-model-what-this-client-writes-to-disk-in-plaintext) +Your files, file names and photos are end-to-end encrypted by Proton. To show them to you, the +service decrypts them on this computer and keeps a cache, which is not encrypted. Use full-disk or +home-folder encryption. The [security model](SECURITY_MODEL.md#what-is-kept-on-this-computer) lists exactly what is stored. -The app uses these words consistently, and so does this guide: +## Words used in the app + +The app uses these words, and so do these pages: | Term | Meaning | |---|---| -| **My files** | The root of your Drive | -| **Proton Drive folder** | Where My files appears on disk, `~/ProtonDrive` by default | -| **Online only** | A file or folder whose content downloads when opened | +| **My files** | Everything in your Proton Drive, apart from Photos and computer backups | +| **Proton Drive folder** | Where My files appears on this computer, `~/ProtonDrive` by default | +| **Online only** | A file or folder that downloads when you open it | | **Available offline** | A file, or a folder with everything in it, kept on this computer | | **Synced folder** | A local folder with a full copy on this computer, backed up to Proton Drive | | **Proton Drive service** | The background service (`proton-drive.service`) | -## My files - -My files is mounted at `~/ProtonDrive`. Every file and folder appears immediately, but content -downloads only when a program reads it: - -- **Reads fetch only what is needed.** A video player that seeks, or a tool that reads a file - header, downloads only those parts. Media streams while it plays. -- **Changes work at local speed.** Creating, renaming, moving and deleting files and folders - returns at once, online or offline. When a program closes a file it wrote, the new content is - stored on disk. Every change is queued and sent to Proton Drive in the background; the changes - to one file are sent in the order you made them. The queue survives network loss, restarts and - crashes. See [Changes waiting for Proton Drive](#changes-waiting-for-proton-drive). -- **Recently read content is cached** on disk up to the cache size limit (5 GiB by default) and - evicted oldest first. -- **Items shared with you** appear under `Shared with me/` inside the Proton Drive folder. Shares - where you are a viewer are read-only. - -The **My files** page of the app browses the same tree without going through the mount. It has a -grid and a list view, sorting, thumbnails (including camera RAW previews), multi-select and drag -and drop upload. **Details** (Alt+Enter) shows size, dates, sharing and version history for the -selected item. - -Symbolic links, hard links, device files and FIFOs cannot be stored on Proton Drive. Programs that -try to create them get a clean error, not a hang or `EIO`. - -## Keeping files on this computer - -Turn on **Available offline** for a file or folder in My files, or run `pdfs pin `. A file -is downloaded completely and never evicted from the cache. For a folder, every file in it and in -its subfolders downloads in the background, and the tray and the app show the progress. -`pdfs pins` lists what is kept, and **Preferences → Storage → Available offline** shows it in the -app. - -Turning it off (`pdfs unpin `) frees the space. The files stay on Proton Drive. A file inside -a folder kept offline stays with its folder: turn the folder off instead. - -## Syncing local folders - -A synced folder is an ordinary folder on this computer, for example `~/Documents`, that is -backed up to Proton Drive under this computer's name. It appears on the **Computers** page of the -app and in the web app. - -- Add one with **Add Folder to Sync** on the **Computers** page, or `pdfs sync add ~/Documents`. -- Changes flow both ways. Local edits upload a few seconds after the file is closed; remote edits - download on the next pass. -- A file that another program still holds open for writing is not uploaded until it is closed. -- **Sync → Folders** in the app, and `pdfs sync list`, show every folder and its state. - -Each synced folder can also be switched to **Online only**. The local copy is replaced by a mount -of the folder, so the files stay visible but take no disk space until opened. Switch with the -**Synced / Online only** switch on the folder's card, or `pdfs sync mode ondemand` and back -with `pdfs sync mode mirror`. The switch waits until everything in the folder is on Proton -Drive, and refuses while the folder holds something that never goes there: an ignored path such -as `.git/`, a symlink, or a folder you may not read. The message names it. `pdfs locations` lists every local place the service occupies. - -### Moving between synced folders and My files - -`~/ProtonDrive` and each online-only folder are separate mounts, and a mirrored folder is a plain -folder on disk. `mv`, and dragging in a file manager, cannot rename across them, so they copy -and then delete: every file is downloaded, uploaded again, and the original goes to the Trash. - -Use `pdfs move ` or **Move to…** in the app instead. The move happens on Proton -Drive, so it takes seconds whatever the size. For example: - -```sh -pdfs move ~/Documents/Scans ~/ProtonDrive/Archive -pdfs move ~/Documents/a.pdf ~/Documents/b.pdf ~/ProtonDrive/Archive -``` - -The last path is the destination folder; everything before it moves there. -Files already opened in an online-only folder are not downloaded again when -they move into a mirrored one. - -A move out of a synced folder waits until its files are on Proton Drive. A file that is still -uploading, has changes that are not uploaded, or is excluded by `.pdfsignore` stops the move, -and the message names it. - -Removing a synced folder (**Stop Syncing** or `pdfs sync rm `) keeps the local files and the -copy on Proton Drive. Add `--delete-remote` (or tick **Also delete from Proton Drive**) to delete -the copy on Proton Drive as well. - -### Excluding files with `.pdfsignore` - -Synced folders skip build output, dependency folders and editor leftovers. Rules come from two -places, and both apply: - -1. A `.pdfsignore` file at the root of the synced folder (`.protonignore` also works), using - gitignore syntax, including `!` to re-include: - - ```gitignore - build/ - *.log - !important.log - ``` - -2. The global `ignore_patterns` list in `config.json` (see - [CONFIGURATION.md](CONFIGURATION.md#ignore_patterns)). When unset, it defaults to `.git/`, - `.hg/`, `.svn/`, `node_modules/`, `target/`, `.venv/`, `__pycache__/`, `*~`, `*.swp`, `*.tmp`, - `.DS_Store` and `Thumbs.db`. - -Rules are re-read on every pass. Ignoring never deletes anything: a file that becomes ignored is -simply no longer tracked, and its copy on Proton Drive stays. - -A folder you may not read, such as a cache a container created as root, is skipped the same way, -and the activity feed names it once. Add it to `.pdfsignore` to stop that message. - -## Changes waiting for Proton Drive - -A change you made in the Proton Drive folder exists only on this computer until it reaches -Proton Drive. `pdfs status`, the tray and **Sync → Overview** count what is still waiting, and -`pdfs sync queue` lists it. - -Sometimes Proton Drive refuses a change: the storage is full, a share was taken away, the folder -is gone, the folder holds too many items, or Drive does not accept a name. The change then needs -your attention. It shows at once in **Sync → Overview**, in the tray, and in `pdfs sync issues`, -with what it means and what you can do. A file or folder with such a change carries a warning -badge in **Files**; hover it to see why. - -Nothing is deleted. The change stays queued and is retried. You can: - -- **Retry** it now, from the same list or with `pdfs sync retry`, for example after freeing space. -- **Export** a file's content to keep your version (`pdfs sync export `). -- **Discard** the change (`pdfs sync discard `). It is undone here and never sent: a new file - or folder goes away, anything else goes back to what Proton Drive has. Export first if you want - to keep the content. - -Two cases are not refused, and the activity log records both: - -- A new file or folder whose name is already taken on Proton Drive by something this computer had - not seen yet lands under a `(sync-conflict …)` name, as described below. -- A new file whose folder was deleted on another device meanwhile lands at the top of My files, - so its content is not lost. - -Before you shut down a computer for good, check that nothing is waiting: see -[RECOVERY.md](RECOVERY.md#lost-with-the-machine). - -## Conflicts - -When the same file changed on this computer and somewhere else since the last sync, neither side -is thrown away. The local version is kept as a copy named like -`report (sync-conflict laptop 1759068180).odt`, where `laptop` is the name of the computer that -made the copy and the number is the time of the conflict in seconds since 1970. The remote -version takes the original name. Copies made before 3.5.0 have no computer name. - -Conflicts appear under **Sync → Overview → Needs attention** and in `pdfs conflicts`. For each -one, keep the original, keep the copy, or keep both under a new name: - -```bash -pdfs conflicts -pdfs conflicts resolve "Documents/report (sync-conflict laptop 1759068180).odt" --keep copy -``` - -The version you do not keep goes to the Trash, so a wrong choice can be undone. - -## Pausing and limiting sync - -- **Pause** from the tray, the Sync page, or `pdfs sync pause`. Uploads and synced folders wait; - reading files keeps working. `pdfs sync resume` continues. -- **Limit bandwidth** under **Preferences → General → Network**, or with - `pdfs sync limit --up 2M --down 10M`. `0` removes a limit. - -## Photos - -The **Photos** page shows your Proton Photos library. Photos are stored separately from My files -and do not appear in the Proton Drive folder. - -- **Timeline** groups photos by day, month or year as you zoom. Drag the scrubber on the right edge - to jump through the years. **On this day** shows photos from the same date in earlier years. -- **Filters** narrow the timeline to images, videos or RAW files, to favorites, or to photos that - are in no album. -- **RAW + JPEG pairs** of the same shot show as one tile. The viewer switches between them, and - moving the tile to the Trash removes both. -- **Albums** lists your albums and albums shared with you. Create, rename and delete albums, and - add or remove photos. -- **Places** groups photos by the town they were taken in, as cards or on a map. Towns come from - bundled [GeoNames](https://www.geonames.org/) data (CC BY 4.0) and the map from - [Natural Earth](https://www.naturalearthdata.com/) (public domain), so nothing is fetched online. - An optional street map from [OpenFreeMap](https://openfreemap.org/) (© OpenStreetMap - contributors) can be enabled under **Preferences → General → Appearance**. It is off by default - because the tile server learns which areas you look at. -- **Viewer**: favorites, EXIF details, show on map, slideshow (F5). -- **Find Duplicates…** in the page menu lists byte-identical copies. The **Similar** switch also - finds resized or re-encoded copies and burst shots by comparing thumbnails. -- **Upload** adds photos. Its menu holds **Import from Google Photos…**. - -### Importing from Google Photos - -Request a Google Takeout export of Google Photos, then pass every `.zip` of it at once. A photo's -metadata often sits in a different part of the export than the photo itself. - -```bash -pdfs import-google-photos --dry-run ~/Downloads/takeout-*.zip # see what would happen -pdfs import-google-photos --wait ~/Downloads/takeout-*.zip -``` - -Albums are recreated, capture dates and locations are taken from the metadata, and photos already -on the account are skipped, so an interrupted import can be run again. `pdfs import-status` and -`pdfs cancel-import` follow or stop an import that runs in the background. - -If photos from an earlier import are filed under the day they were uploaded, -`pdfs redate-photos --dry-run` finds those whose file name carries a different date, and -`pdfs redate-photos` fixes them. Proton cannot change a capture date in place, so each fix -re-uploads the photo and moves the original to the Trash, keeping favorites and albums. - -## Sharing - -- **Share…** on a file or folder invites people by email as viewer or editor, or creates a public - link with an optional password and expiry. `pdfs share`, `pdfs members`, `pdfs share-role`, - `pdfs unshare` and `pdfs public-link` do the same from a terminal. -- **Shared by me** lists everything you share, filtered by links, people or expired links. Copy a - live link or stop several shares at once. -- **Shared with me** lists what others share with you, with who shared it and your access. - Pending invitations wait in a banner; `pdfs invitations` accepts or rejects them. Saved public - links (`pdfs bookmarks`) have their own view. - -## Trash and version history - -The **Trash** page lists trashed items with the date they were deleted. Restore puts an item back -where it was, together with trashed folders above it. **Delete Forever** and **Empty Trash** are -permanent. The CLI equivalents are `pdfs trash`, `pdfs restore`, `pdfs delete-forever` and -`pdfs empty-trash`. - -Proton Drive keeps earlier versions of every file. **Details → Versions…** in the app, or -`pdfs versions list `, shows them. You can: - -- **Restore** a version. This happens on the server; nothing is re-uploaded. -- **Save** a version to a separate local file without touching the current one. -- **Delete** an old version permanently. The current version cannot be deleted. - -## Computers - -**Computers** lists this computer and every other computer that backs up to your account. - -- **This computer** shows its synced folders with their state, and **Restore Folders…** brings - back folders that are backed up under it but not synced here yet. -- **Another computer** can be browsed read-only in the same list or grid as My files, and - **Restore to This Computer…** downloads its folders here. -- **Sync With This Computer…** makes this computer sync the same folders as another one, for - example a desktop and a laptop. Both keep the folders in step: a change or a deletion on one - reaches the other. See [Syncing several computers](#syncing-several-computers). -- **Continue This Backup Here…** makes this computer take over another computer's backup, for - example after reinstalling or replacing a machine. See [RECOVERY.md](RECOVERY.md). -- **Move to This Computer…** moves another computer's folders into this computer's backup, for - example when you switch from Windows to Linux, and then offers them for syncing here. The other - computer stays in the list, without those folders. - -### Syncing several computers - -Two or more computers can sync the same folders through one backup in Proton Drive. - -1. Sync the folders on the first computer as usual. -2. On the second computer, open **Computers**, open the menu of the first computer and choose - **Sync With This Computer…**. From a terminal: `pdfs devices join UID`, with the UID from - `pdfs devices list`. -3. Choose which of its folders to sync here and where, as in a restore. - -From then on, both computers sync into that backup. A folder you add later on either computer -lands there too, and a folder with the same name joins the one already there. An edit on one -computer reaches the other within about two minutes. When both computers change the same file in -that time, each keeps the other's version under the original name and its own as a conflict copy -named after it. Folders that were synced here before joining stay in this computer's own backup. - -## Search launcher - -`pdfs-prompt` searches Proton Drive and your home folder together, in one ranked list. It -tolerates prefixes, abbreviations, typos and swapped letters, and matches on parent folder names -as well. Bind it to a keyboard shortcut in your desktop settings. - -The window stays loaded after the first use, so later shortcuts open it instantly. Folders, audio -and video open straight from the Proton Drive folder, so players can start before the whole file -downloads. Other Drive files download first, then open. - -To make the first shortcut instant as well, start the launcher hidden with your session. -`pdfs-prompt --preload` loads the window without showing it and does nothing if the launcher is -already running. For Hyprland: - -```ini -exec-once = pdfs-prompt --preload -bind = SUPER, space, exec, pdfs-prompt -``` - -### Using your own launcher - -`pdfs-prompt --dmenu` shows the results in fuzzel, rofi, wofi, tofi, bemenu or dmenu, whichever is -installed: - -```bash -pdfs-prompt --dmenu -pdfs-prompt --dmenu --menu 'fuzzel --dmenu --width 60' -pdfs-prompt --dmenu --query invoice -``` - -A dmenu-style launcher filters a fixed list and cannot ask for new results per keystroke, so the -search takes two steps. The first list, `Search Drive ›`, shows your available-offline files; type -a query and press Enter to search. The second, `Drive: invoice ›`, shows the results; Enter opens -one, and typing something else searches again. - -### Search as you type with fzf - -`pdfs-prompt --fzf` re-queries the service on every keystroke, like the built-in window. When -started from a keybinding without a terminal, it opens one itself (foot, ghostty, kitty, -alacritty, wezterm or xterm) with the app ID `pdfs-prompt`, so one window rule covers all of them. -For Hyprland: - -```ini -windowrulev2 = float, class:^(pdfs-prompt)$ -windowrulev2 = size 900 500, class:^(pdfs-prompt)$ -windowrulev2 = center, class:^(pdfs-prompt)$ -bind = SUPER, space, exec, pdfs-prompt --fzf -``` - -To make `--dmenu` or `--fzf` the default without changing your keybinding, set `prompt.mode` in -`config.json` ([CONFIGURATION.md](CONFIGURATION.md#prompt)). `--gtk` forces the built-in window. - -## Choosing how files open - -Files open with `xdg-open` by default. The `open_with` section of `config.json` overrides that per -file type, for example text in Neovim inside a terminal. The rules apply to the search launcher -and to the app. See [CONFIGURATION.md](CONFIGURATION.md#open_with). - -## Tray - -`pdfs-tray` starts at login and shows whether files are up to date, syncing, paused or offline. -Its menu opens the app or the Proton Drive folder, pauses or resumes sync, and stops or connects -the service. **Hide Tray Icon** removes it until you turn it back on under -**Preferences → General → Appearance → Show tray icon**. - -GNOME needs the AppIndicator extension to show tray icons. - ## Preferences | Page | Settings | |---|---| -| **General** | Start on login · Proton Drive folder location · upload and download limits · Proton theme, street map for Places, tray icon · language | +| **General** | Start on login · Proton Drive folder location · upload and download limits · Proton theme, street map for Places, tray icon · language · check for updates | | **Storage** | Cache size limit · clear the cache · files kept available offline | -The account menu shows storage used and holds **Sign Out…**. Settings not exposed in the app are -listed in [CONFIGURATION.md](CONFIGURATION.md). +The account menu shows the storage you use and holds **Sign Out…**. Settings that are not in the +app are described in [Configuration](CONFIGURATION.md). diff --git a/packaging/aur/proton-drive-for-linux-bin/.SRCINFO b/packaging/aur/proton-drive-for-linux-bin/.SRCINFO index 3dc2204..ec3c521 100644 --- a/packaging/aur/proton-drive-for-linux-bin/.SRCINFO +++ b/packaging/aur/proton-drive-for-linux-bin/.SRCINFO @@ -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.4.1 + pkgver = 3.5.0 pkgrel = 1 url = https://proton-drive.narl.io install = proton-drive.install @@ -22,25 +22,25 @@ 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.4.1 - provides = proton-drive-linux=3.4.1 + provides = proton-drive-for-linux=3.5.0 + provides = proton-drive-linux=3.5.0 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.4.1-x86_64.tar.gz::https://github.com/narrrl/proton-drive-linux/releases/download/v3.4.1/proton-drive-linux-3.4.1-x86_64.tar.gz - source = io.narl.proton-drive-linux-3.4.1.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.4.1/packaging/io.narl.proton-drive-linux.desktop - source = io.narl.proton-drive-linux-tray-3.4.1.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.4.1/packaging/io.narl.proton-drive-linux-tray.desktop - source = io.narl.proton-drive-linux-3.4.1.svg::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.4.1/packaging/io.narl.proton-drive-linux.svg - source = proton-drive-3.4.1.service::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.4.1/packaging/proton-drive.service - source = LICENSE-3.4.1::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.4.1/LICENSE - sha256sums = dc547922859155558b139eacbb5414ef421d6afcbeee254213223dbc39489fdd + source = proton-drive-linux-3.5.0-x86_64.tar.gz::https://github.com/narrrl/proton-drive-linux/releases/download/v3.5.0/proton-drive-linux-3.5.0-x86_64.tar.gz + source = io.narl.proton-drive-linux-3.5.0.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.5.0/packaging/io.narl.proton-drive-linux.desktop + source = io.narl.proton-drive-linux-tray-3.5.0.desktop::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.5.0/packaging/io.narl.proton-drive-linux-tray.desktop + source = io.narl.proton-drive-linux-3.5.0.svg::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.5.0/packaging/io.narl.proton-drive-linux.svg + source = proton-drive-3.5.0.service::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.5.0/packaging/proton-drive.service + source = LICENSE-3.5.0::https://raw.githubusercontent.com/narrrl/proton-drive-linux/v3.5.0/LICENSE + sha256sums = 902f60de5f471cb15c84d0ecb29cb4644a0ac52ce36d2be54c16b4befeba8de0 sha256sums = d190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123 sha256sums = 1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0 sha256sums = e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828 - sha256sums = 77280bff5f693fbc5ffa9f3ccd06a43cfff96d770290aa8d7d0c28e9dde277be + sha256sums = 44c25dca6e759c46fca50b58099aa6194abe78293dee07d971bbbce0ff3ca58f sha256sums = 02c419e7f9af08dda720e4f852f3e57cf5a69e192605440f750e9d244fa2817c pkgname = proton-drive-for-linux-bin diff --git a/packaging/aur/proton-drive-for-linux-bin/PKGBUILD b/packaging/aur/proton-drive-for-linux-bin/PKGBUILD index 47cdbb4..b2c4568 100644 --- a/packaging/aur/proton-drive-for-linux-bin/PKGBUILD +++ b/packaging/aur/proton-drive-for-linux-bin/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Nils Pukropp pkgname=proton-drive-for-linux-bin -pkgver=3.4.1 +pkgver=3.5.0 pkgrel=1 pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (binaries)" arch=('x86_64') @@ -36,11 +36,11 @@ 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=('dc547922859155558b139eacbb5414ef421d6afcbeee254213223dbc39489fdd' +sha256sums=('902f60de5f471cb15c84d0ecb29cb4644a0ac52ce36d2be54c16b4befeba8de0' 'd190d6771fff0b975271fb62fd3fb0fa8bbf17d7278c10853c18557855b0c123' '1d95e250370220e3bb6aead6ce34d1d6cc759f42c4803b75f1667e78ff47c6c0' 'e598a93c2715c3a9fdc7fb937902083d4a1135e8fbdfe6fe7e37f0383ebbd828' - '77280bff5f693fbc5ffa9f3ccd06a43cfff96d770290aa8d7d0c28e9dde277be' + '44c25dca6e759c46fca50b58099aa6194abe78293dee07d971bbbce0ff3ca58f' '02c419e7f9af08dda720e4f852f3e57cf5a69e192605440f750e9d244fa2817c') package() { @@ -65,4 +65,7 @@ package() { cp -r --no-preserve=ownership "$srcdir/locale" "$pkgdir/usr/share/locale" install -Dm644 "$srcdir/LICENSE-$pkgver" "$pkgdir/usr/share/licenses/$pkgname/LICENSE" + + # pacman brings new releases, so the app never checks for them itself. + install -Dm644 /dev/null "$pkgdir/usr/share/proton-drive-linux/managed-updates" } diff --git a/packaging/aur/proton-drive-for-linux-git/.SRCINFO b/packaging/aur/proton-drive-for-linux-git/.SRCINFO index 18c9638..cf926de 100644 --- a/packaging/aur/proton-drive-for-linux-git/.SRCINFO +++ b/packaging/aur/proton-drive-for-linux-git/.SRCINFO @@ -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.4.1.r0.g18cc2ea + pkgver = 3.5.0.r0.geb25c16 pkgrel = 1 url = https://proton-drive.narl.io install = proton-drive.install @@ -25,8 +25,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.4.1.r0.g18cc2ea - provides = proton-drive-linux=3.4.1.r0.g18cc2ea + provides = proton-drive-for-linux=3.5.0.r0.geb25c16 + provides = proton-drive-linux=3.5.0.r0.geb25c16 provides = pdfs conflicts = proton-drive-linux conflicts = proton-drive-for-linux diff --git a/packaging/aur/proton-drive-for-linux-git/PKGBUILD b/packaging/aur/proton-drive-for-linux-git/PKGBUILD index 4ac4776..689c87f 100644 --- a/packaging/aur/proton-drive-for-linux-git/PKGBUILD +++ b/packaging/aur/proton-drive-for-linux-git/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Nils Pukropp pkgname=proton-drive-for-linux-git -pkgver=3.4.1.r0.g18cc2ea +pkgver=3.5.0.r0.geb25c16 pkgrel=1 pkgdesc="Unofficial Proton Drive client: FUSE files-on-demand mount, CLI, GTK4 app, tray (git)" arch=('x86_64') @@ -83,4 +83,7 @@ package() { install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" + + # pacman brings new releases, so the app never checks for them itself. + install -Dm644 /dev/null "$pkgdir/usr/share/proton-drive-linux/managed-updates" } diff --git a/packaging/aur/proton-drive-for-linux/.SRCINFO b/packaging/aur/proton-drive-for-linux/.SRCINFO index e6a4204..0ce7f91 100644 --- a/packaging/aur/proton-drive-for-linux/.SRCINFO +++ b/packaging/aur/proton-drive-for-linux/.SRCINFO @@ -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.4.1 + pkgver = 3.5.0 pkgrel = 1 url = https://proton-drive.narl.io install = proton-drive.install @@ -24,13 +24,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.4.1 + provides = proton-drive-linux=3.5.0 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.4.1.tar.gz::https://github.com/narrrl/proton-drive-linux/archive/refs/tags/v3.4.1.tar.gz - sha256sums = 7d36f39fa6a3b44f726f543c3a3f3f30e9c01c64bc6cffe24ca4092aff3e5711 + source = proton-drive-for-linux-3.5.0.tar.gz::https://github.com/narrrl/proton-drive-linux/archive/refs/tags/v3.5.0.tar.gz + sha256sums = 5eb0e001ec2301ffb8b2b8429e907fab32810d6eba541bf978a0ea97867d8bc9 pkgname = proton-drive-for-linux diff --git a/packaging/aur/proton-drive-for-linux/PKGBUILD b/packaging/aur/proton-drive-for-linux/PKGBUILD index 6974209..e203c31 100644 --- a/packaging/aur/proton-drive-for-linux/PKGBUILD +++ b/packaging/aur/proton-drive-for-linux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Nils Pukropp pkgname=proton-drive-for-linux -pkgver=3.4.1 +pkgver=3.5.0 pkgrel=1 pkgdesc="Unofficial Proton Drive client: files-on-demand FUSE mount, CLI, GTK4 app and tray" arch=('x86_64') @@ -26,7 +26,7 @@ options=('!lto') # Stops the running daemon before removal, so the mount stops mirroring deletes. install=proton-drive.install source=("$pkgname-$pkgver.tar.gz::$_repo/archive/refs/tags/v$pkgver.tar.gz") -sha256sums=('7d36f39fa6a3b44f726f543c3a3f3f30e9c01c64bc6cffe24ca4092aff3e5711') +sha256sums=('5eb0e001ec2301ffb8b2b8429e907fab32810d6eba541bf978a0ea97867d8bc9') _srcname="proton-drive-linux-$pkgver" @@ -78,4 +78,7 @@ package() { install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" + + # pacman brings new releases, so the app never checks for them itself. + install -Dm644 /dev/null "$pkgdir/usr/share/proton-drive-linux/managed-updates" } diff --git a/packaging/copr/README.md b/packaging/copr/README.md new file mode 100644 index 0000000..67292d8 --- /dev/null +++ b/packaging/copr/README.md @@ -0,0 +1,43 @@ +# COPR repository + +Fedora users install from the COPR project +[`narl-rs/proton-drive-linux`](https://copr.fedorainfracloud.org/coprs/narl-rs/proton-drive-linux/), +so `dnf upgrade` brings every release. COPR builds `packaging/proton-drive-linux.spec` from two +release assets: + +| Source | What it is | +| --- | --- | +| `Source0` | GitHub's archive of the `v` tag | +| `Source1` | `proton-drive-linux-vendor-.tar.xz`: every crate from `Cargo.lock`, made by `cargo vendor` in `release.yml` | + +With the vendored crates the build needs no network, so the COPR project keeps network access +off. + +## Releasing a new version + +`.github/workflows/copr.yml` does this automatically: + +- **On a release tag**, `release.yml` calls it after the GitHub release is published. It writes + the version into the spec, downloads both sources with `spectool`, builds a source package + and submits it with `copr-cli build`. The job waits for COPR, so a failed build fails the + release workflow. +- **By hand**, run the workflow from the Actions tab with a version. The release must already + have its vendor tarball. + +It needs the `COPR_CONFIG` repository secret: the whole content of `~/.config/copr` from +. **The token expires after 180 days.** Renew it on that +page and replace the secret, or the next release's COPR job fails. + +To do the same by hand: + +```bash +spectool -g -C ~/rpmbuild/SOURCES packaging/proton-drive-linux.spec +rpmbuild -bs packaging/proton-drive-linux.spec +copr-cli build narl-rs/proton-drive-linux ~/rpmbuild/SRPMS/proton-drive-linux-*.src.rpm +``` + +## Project settings + +- Chroots: `fedora-44-x86_64`, `fedora-45-x86_64` and `fedora-rawhide-x86_64`. The spec is + `ExclusiveArch: x86_64` and needs `rust >= 1.96`. +- Network access in builds: off. diff --git a/packaging/nix/package.nix b/packaging/nix/package.nix index 2f4e6ed..3c052b2 100644 --- a/packaging/nix/package.nix +++ b/packaging/nix/package.nix @@ -106,6 +106,9 @@ rustPlatform.buildRustPackage { patchShebangs po/build.sh po/build.sh $out/share/locale + + # Nix brings new releases, so the app never checks for them itself. + install -Dm644 /dev/null $out/share/proton-drive-linux/managed-updates ''; # Thumbnails shell out to exiftool, and to ffmpeg for video. Suffixed, so a diff --git a/packaging/proton-drive-linux.spec b/packaging/proton-drive-linux.spec index 84e52c5..dbb0d29 100644 --- a/packaging/proton-drive-linux.spec +++ b/packaging/proton-drive-linux.spec @@ -1,13 +1,19 @@ -# Local in-tree RPM (mirrors packaging/PKGBUILD). -# From the repository root: -# rpmbuild -bb packaging/proton-drive-linux.spec --define "git_dir $PWD" ... -# %build needs network so cargo can fetch crates.io (same trade-off as the Arch PKGBUILD). +# Two ways to build this spec: +# +# - From a checkout (local builds and the release workflow). From the repository root: +# rpmbuild -bb packaging/proton-drive-linux.spec --define "git_dir $PWD" ... +# %build needs network so cargo can fetch crates.io (same trade-off as the Arch PKGBUILD). +# - From the release tarballs (COPR, .github/workflows/copr.yml), when git_dir is not +# defined. Source1 holds every crate, so the build runs offline. %global debug_package %{nil} # Match Arch PKGBUILD `!lto` — LTO has broken some GTK/Rust links in practice. %global _lto_cflags %{nil} -%{!?version: %global version 0.4.0} +%{!?version: %global version 3.5.0} +%global forge https://github.com/narrrl/proton-drive-linux +# Files come from the checkout, or from the unpacked tag archive. +%global src %{?git_dir}%{!?git_dir:.} Name: proton-drive-linux Version: %{version} @@ -16,9 +22,13 @@ Summary: Unofficial Proton Drive client for Linux (FUSE, CLI, GTK4 app + License: MIT URL: https://proton-drive.narl.io ExclusiveArch: x86_64 +%if 0%{!?git_dir:1} +Source0: %{forge}/archive/refs/tags/v%{version}.tar.gz#/%{name}-%{version}.tar.gz +Source1: %{forge}/releases/download/v%{version}/%{name}-vendor-%{version}.tar.xz +%endif BuildRequires: cargo -BuildRequires: rust +BuildRequires: rust >= 1.96 BuildRequires: pkgconf-pkg-config BuildRequires: fuse3-devel BuildRequires: gtk4-devel @@ -57,18 +67,31 @@ block-level caching, a scriptable CLI, and a GTK4/Libadwaita desktop app with system tray and search launcher. Unofficial and not affiliated with Proton AG. %prep -# In-tree build: no Source tarball. Pass --define "git_dir /path/to/checkout". -test -n "%{?git_dir}" || (echo 'Pass --define "git_dir $PWD" from the repo root' >&2; exit 1) +%if 0%{?git_dir:1} +# In-tree build: no Source tarball. test -f %{git_dir}/Cargo.toml cp -a %{git_dir}/LICENSE . +%else +# -a 1 unpacks the vendored crates into vendor/ inside the source tree. +%autosetup -n %{name}-%{version} -a 1 +mkdir -p .cargo +cat > .cargo/config.toml <<'EOF' +[source.crates-io] +replace-with = "vendored-sources" + +[source.vendored-sources] +directory = "vendor" +EOF +%endif + %build # The release workflow has already built target/release and passes --define "prebuilt 1". # rpmbuild exports its own compiler flags, which would make cargo rebuild everything. %if 0%{?prebuilt} test -x %{git_dir}/target/release/pdfs %else -cd %{git_dir} -cargo build --release --locked \ +cd %{src} +cargo build --release %{?git_dir:--locked}%{!?git_dir:--frozen} \ --bin pdfs \ --bin pdfs-tray \ --bin pdfs-app \ @@ -76,21 +99,25 @@ cargo build --release --locked \ %endif %install -rel=%{git_dir}/target/release +rel=%{src}/target/release install -D -m0755 "$rel/pdfs" %{buildroot}%{_bindir}/pdfs install -D -m0755 "$rel/pdfs-tray" %{buildroot}%{_bindir}/pdfs-tray install -D -m0755 "$rel/pdfs-app" %{buildroot}%{_bindir}/pdfs-app install -D -m0755 "$rel/pdfs-prompt" %{buildroot}%{_bindir}/pdfs-prompt -install -D -m0644 %{git_dir}/packaging/io.narl.proton-drive-linux.desktop \ +install -D -m0644 %{src}/packaging/io.narl.proton-drive-linux.desktop \ %{buildroot}%{_datadir}/applications/io.narl.proton-drive-linux.desktop -install -D -m0644 %{git_dir}/packaging/io.narl.proton-drive-linux-tray.desktop \ +install -D -m0644 %{src}/packaging/io.narl.proton-drive-linux-tray.desktop \ %{buildroot}%{_sysconfdir}/xdg/autostart/io.narl.proton-drive-linux-tray.desktop -install -D -m0644 %{git_dir}/packaging/io.narl.proton-drive-linux.svg \ +install -D -m0644 %{src}/packaging/io.narl.proton-drive-linux.svg \ %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/io.narl.proton-drive-linux.svg -install -D -m0644 %{git_dir}/packaging/proton-drive.service \ +install -D -m0644 %{src}/packaging/proton-drive.service \ %{buildroot}/usr/lib/systemd/user/proton-drive.service -%{git_dir}/po/build.sh %{buildroot}%{_datadir}/locale +%{src}/po/build.sh %{buildroot}%{_datadir}/locale +%if 0%{!?git_dir:1} +# dnf brings new releases from COPR, so the app does not offer its own check. +install -D -m0644 /dev/null %{buildroot}%{_datadir}/proton-drive-linux/managed-updates +%endif # Removing the package does not stop a running daemon: it keeps serving the # mount from the deleted binary, so files deleted under the mount afterwards, @@ -122,6 +149,9 @@ fi %{_datadir}/icons/hicolor/scalable/apps/io.narl.proton-drive-linux.svg /usr/lib/systemd/user/proton-drive.service %{_datadir}/locale/*/LC_MESSAGES/pdfs.mo +%if 0%{!?git_dir:1} +%{_datadir}/proton-drive-linux/managed-updates +%endif %changelog * Sun Jul 19 2026 Local Packager - 0.4.0-1 diff --git a/po/POTFILES.in b/po/POTFILES.in index 879f4b9..fd55328 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -19,6 +19,7 @@ crates/pdfs-gui/src/app/pages/status.rs crates/pdfs-gui/src/app/pages/takeout.rs crates/pdfs-gui/src/app/pages/trash.rs crates/pdfs-gui/src/app/pages/verify.rs +crates/pdfs-gui/src/app/update.rs crates/pdfs-gui/src/app/widgets/compat.rs crates/pdfs-gui/src/app/widgets/details.rs crates/pdfs-gui/src/app/widgets/file_list.rs diff --git a/po/de.po b/po/de.po index 778e824..5555d25 100644 --- a/po/de.po +++ b/po/de.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: proton-drive-linux 2.1.0\n" "Report-Msgid-Bugs-To: https://github.com/narrrl/proton-drive-linux/issues\n" -"POT-Creation-Date: 2026-10-16 17:09+0200\n" +"POT-Creation-Date: 2026-10-17 01:23+0200\n" "PO-Revision-Date: 2026-09-23 12:00+0200\n" "Last-Translator: Proton Drive for Linux contributors\n" "Language-Team: German\n" @@ -57,7 +57,7 @@ msgstr "%-d. %b %Y" msgid "%A, %B %-d, %Y, %H:%M" msgstr "%A, %-d. %B %Y, %H:%M" -#: crates/pdfs-gui/src/app/main.rs:849 +#: crates/pdfs-gui/src/app/main.rs:856 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:833 @@ -70,74 +70,74 @@ msgstr "%A, %-d. %B %Y, %H:%M" msgid "My files" msgstr "Meine Dateien" -#: crates/pdfs-gui/src/app/main.rs:852 crates/pdfs-gui/src/app/main.rs:1517 +#: crates/pdfs-gui/src/app/main.rs:859 crates/pdfs-gui/src/app/main.rs:1528 #: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/photos.rs:845 #: crates/pdfs-gui/src/app/pages/places.rs:441 msgid "Photos" msgstr "Fotos" -#: crates/pdfs-gui/src/app/main.rs:857 +#: crates/pdfs-gui/src/app/main.rs:864 #: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:484 msgid "Shared with me" msgstr "Mit mir geteilt" -#: crates/pdfs-gui/src/app/main.rs:862 +#: crates/pdfs-gui/src/app/main.rs:869 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 msgid "Shared by me" msgstr "Von mir geteilt" -#: crates/pdfs-gui/src/app/main.rs:865 +#: crates/pdfs-gui/src/app/main.rs:872 #: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:493 msgid "Computers" msgstr "Computer" -#: crates/pdfs-gui/src/app/main.rs:866 +#: crates/pdfs-gui/src/app/main.rs:873 #: crates/pdfs-gui/src/app/pages/trash.rs:101 msgid "Trash" msgstr "Papierkorb" #. Translators: sidebar entry for the page listing this computer's synced folders. -#: crates/pdfs-gui/src/app/main.rs:868 +#: crates/pdfs-gui/src/app/main.rs:875 msgid "Sync" msgstr "Synchronisierung" #. Translators: a failed action's toast. {what} says what failed, such as #. "Couldn't rename"; {detail} is the reason, usually untranslated text #. from the mount service. -#: crates/pdfs-gui/src/app/main.rs:1214 +#: crates/pdfs-gui/src/app/main.rs:1225 #: crates/pdfs-gui/src/app/pages/takeout.rs:714 #, rust-format msgid "{what}: {detail}" msgstr "{what}: {detail}" -#: crates/pdfs-gui/src/app/main.rs:1237 +#: crates/pdfs-gui/src/app/main.rs:1248 msgid "You're offline" msgstr "Keine Internetverbindung" -#: crates/pdfs-gui/src/app/main.rs:1238 +#: crates/pdfs-gui/src/app/main.rs:1249 msgid "That's not there any more" msgstr "Das ist nicht mehr vorhanden" -#: crates/pdfs-gui/src/app/main.rs:1239 +#: crates/pdfs-gui/src/app/main.rs:1250 msgid "You don't have access to that" msgstr "Kein Zugriff darauf" -#: crates/pdfs-gui/src/app/main.rs:1240 +#: crates/pdfs-gui/src/app/main.rs:1251 msgid "Something changed this first" msgstr "Etwas anderes hat dies zuerst geändert" -#: crates/pdfs-gui/src/app/main.rs:1241 +#: crates/pdfs-gui/src/app/main.rs:1252 msgid "Your Proton Drive is full" msgstr "Das Proton Drive ist voll" -#: crates/pdfs-gui/src/app/main.rs:1328 +#: crates/pdfs-gui/src/app/main.rs:1339 msgid "Proton Drive for Linux" msgstr "Proton Drive for Linux" -#: crates/pdfs-gui/src/app/main.rs:1336 +#: crates/pdfs-gui/src/app/main.rs:1347 msgid "" "Files-on-demand Proton Drive for the Linux desktop.\n" "\n" @@ -149,23 +149,23 @@ msgstr "" "Inoffizieller Client – nicht mit Proton AG verbunden, von Proton AG " "unterstützt oder befürwortet." -#: crates/pdfs-gui/src/app/main.rs:1341 +#: crates/pdfs-gui/src/app/main.rs:1352 msgid "Source Code" msgstr "Quellcode" -#: crates/pdfs-gui/src/app/main.rs:1345 +#: crates/pdfs-gui/src/app/main.rs:1356 msgid "Proton Terms of Service" msgstr "Nutzungsbedingungen von Proton" -#: crates/pdfs-gui/src/app/main.rs:1349 +#: crates/pdfs-gui/src/app/main.rs:1360 msgid "Proton Privacy Policy" msgstr "Datenschutzerklärung von Proton" -#: crates/pdfs-gui/src/app/main.rs:1354 +#: crates/pdfs-gui/src/app/main.rs:1365 msgid "Disclaimer" msgstr "Haftungsausschluss" -#: crates/pdfs-gui/src/app/main.rs:1358 +#: crates/pdfs-gui/src/app/main.rs:1369 msgid "" "This is an unofficial, community-built client. It is not affiliated with, " "endorsed by, or supported by Proton AG.\n" @@ -190,7 +190,7 @@ msgstr "" "bereitgestellt. Daten, deren Verlust nicht hinnehmbar ist, sollten " "zusätzlich gesichert werden." -#: crates/pdfs-gui/src/app/main.rs:1366 +#: crates/pdfs-gui/src/app/main.rs:1377 msgid "" "Places are named with data from GeoNames, licensed under GeoNames, lizenziert unter CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1374 +#: crates/pdfs-gui/src/app/main.rs:1385 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -210,7 +210,7 @@ msgstr "" "href=\"https://www.naturalearthdata.com/\">Natural Earth gezeichnet, die " "gemeinfrei sind." -#: crates/pdfs-gui/src/app/main.rs:1382 +#: crates/pdfs-gui/src/app/main.rs:1393 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, licensed under CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1374 +#: crates/pdfs-gui/src/app/main.rs:1385 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -207,7 +207,7 @@ msgstr "" "\">Natural Earth land outlines and country borders, which are in the " "public domain." -#: crates/pdfs-gui/src/app/main.rs:1382 +#: crates/pdfs-gui/src/app/main.rs:1393 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, con licencia CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1374 +#: crates/pdfs-gui/src/app/main.rs:1385 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -208,7 +208,7 @@ msgstr "" "fronteras de países de Natural " "Earth, que son de dominio público." -#: crates/pdfs-gui/src/app/main.rs:1382 +#: crates/pdfs-gui/src/app/main.rs:1393 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, sous licence CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1374 +#: crates/pdfs-gui/src/app/main.rs:1385 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -209,7 +209,7 @@ msgstr "" "frontières des pays de Natural " "Earth, qui sont dans le domaine public." -#: crates/pdfs-gui/src/app/main.rs:1382 +#: crates/pdfs-gui/src/app/main.rs:1393 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, con licenza CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1374 +#: crates/pdfs-gui/src/app/main.rs:1385 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -207,7 +207,7 @@ msgstr "" "confini nazionali di Natural " "Earth, che sono di pubblico dominio." -#: crates/pdfs-gui/src/app/main.rs:1382 +#: crates/pdfs-gui/src/app/main.rs:1393 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, gelicentieerd onder CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1374 +#: crates/pdfs-gui/src/app/main.rs:1385 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -208,7 +208,7 @@ msgstr "" "href=\"https://www.naturalearthdata.com/\">Natural Earth, die in het " "publieke domein vallen." -#: crates/pdfs-gui/src/app/main.rs:1382 +#: crates/pdfs-gui/src/app/main.rs:1393 msgid "" "The street map, when turned on, comes from OpenFreeMap in the \n" "Language-Team: LANGUAGE \n" @@ -60,7 +60,7 @@ msgstr "" msgid "%A, %B %-d, %Y, %H:%M" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:849 +#: crates/pdfs-gui/src/app/main.rs:856 #: crates/pdfs-gui/src/app/pages/activity_text.rs:539 #: crates/pdfs-gui/src/app/pages/browser.rs:454 #: crates/pdfs-gui/src/app/pages/browser.rs:833 @@ -73,74 +73,74 @@ msgstr "" msgid "My files" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:852 crates/pdfs-gui/src/app/main.rs:1517 +#: crates/pdfs-gui/src/app/main.rs:859 crates/pdfs-gui/src/app/main.rs:1528 #: crates/pdfs-gui/src/app/pages/albums.rs:638 #: crates/pdfs-gui/src/app/pages/photos.rs:845 #: crates/pdfs-gui/src/app/pages/places.rs:441 msgid "Photos" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:857 +#: crates/pdfs-gui/src/app/main.rs:864 #: crates/pdfs-gui/src/app/pages/shared.rs:168 #: crates/pdfs-gui/src/app/pages/shared.rs:484 msgid "Shared with me" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:862 +#: crates/pdfs-gui/src/app/main.rs:869 #: crates/pdfs-gui/src/app/pages/shared_by_me.rs:120 msgid "Shared by me" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:865 +#: crates/pdfs-gui/src/app/main.rs:872 #: crates/pdfs-gui/src/app/pages/devices.rs:140 #: crates/pdfs-gui/src/app/pages/devices.rs:493 msgid "Computers" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:866 +#: crates/pdfs-gui/src/app/main.rs:873 #: crates/pdfs-gui/src/app/pages/trash.rs:101 msgid "Trash" msgstr "" #. Translators: sidebar entry for the page listing this computer's synced folders. -#: crates/pdfs-gui/src/app/main.rs:868 +#: crates/pdfs-gui/src/app/main.rs:875 msgid "Sync" msgstr "" #. Translators: a failed action's toast. {what} says what failed, such as #. "Couldn't rename"; {detail} is the reason, usually untranslated text #. from the mount service. -#: crates/pdfs-gui/src/app/main.rs:1214 +#: crates/pdfs-gui/src/app/main.rs:1225 #: crates/pdfs-gui/src/app/pages/takeout.rs:714 #, rust-format msgid "{what}: {detail}" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1237 +#: crates/pdfs-gui/src/app/main.rs:1248 msgid "You're offline" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1238 +#: crates/pdfs-gui/src/app/main.rs:1249 msgid "That's not there any more" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1239 +#: crates/pdfs-gui/src/app/main.rs:1250 msgid "You don't have access to that" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1240 +#: crates/pdfs-gui/src/app/main.rs:1251 msgid "Something changed this first" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1241 +#: crates/pdfs-gui/src/app/main.rs:1252 msgid "Your Proton Drive is full" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1328 +#: crates/pdfs-gui/src/app/main.rs:1339 msgid "Proton Drive for Linux" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1336 +#: crates/pdfs-gui/src/app/main.rs:1347 msgid "" "Files-on-demand Proton Drive for the Linux desktop.\n" "\n" @@ -148,23 +148,23 @@ msgid "" "AG." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1341 +#: crates/pdfs-gui/src/app/main.rs:1352 msgid "Source Code" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1345 +#: crates/pdfs-gui/src/app/main.rs:1356 msgid "Proton Terms of Service" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1349 +#: crates/pdfs-gui/src/app/main.rs:1360 msgid "Proton Privacy Policy" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1354 +#: crates/pdfs-gui/src/app/main.rs:1365 msgid "Disclaimer" msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1358 +#: crates/pdfs-gui/src/app/main.rs:1369 msgid "" "This is an unofficial, community-built client. It is not affiliated with, " "endorsed by, or supported by Proton AG.\n" @@ -177,21 +177,21 @@ msgid "" "of any data you cannot afford to lose." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1366 +#: crates/pdfs-gui/src/app/main.rs:1377 msgid "" "Places are named with data from GeoNames, licensed under CC BY 4.0." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1374 +#: crates/pdfs-gui/src/app/main.rs:1385 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " "public domain." msgstr "" -#: crates/pdfs-gui/src/app/main.rs:1382 +#: crates/pdfs-gui/src/app/main.rs:1393 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames udostępnionych na licencji CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1374 +#: crates/pdfs-gui/src/app/main.rs:1385 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -211,7 +211,7 @@ msgstr "" "www.naturalearthdata.com/\">Natural Earth, które należą do domeny " "publicznej." -#: crates/pdfs-gui/src/app/main.rs:1382 +#: crates/pdfs-gui/src/app/main.rs:1393 msgid "" "The street map, when turned on, comes from OpenFreeMap in the GeoNames, licensed under GeoNames, licenciados sob CC BY 4.0." -#: crates/pdfs-gui/src/app/main.rs:1374 +#: crates/pdfs-gui/src/app/main.rs:1385 msgid "" "The places map is drawn from Natural Earth land outlines and country borders, which are in the " @@ -207,7 +207,7 @@ msgstr "" "fronteiras de países do Natural Earth, que são de domínio público." -#: crates/pdfs-gui/src/app/main.rs:1382 +#: crates/pdfs-gui/src/app/main.rs:1393 msgid "" "The street map, when turned on, comes from OpenFreeMap in the wiki page name, in sidebar order. -PAGES = { - "INSTALL.md": "Installation", - "USER_GUIDE.md": "User-Guide", - "CLI.md": "Command-Line-Reference", - "CONFIGURATION.md": "Configuration", - "TROUBLESHOOTING.md": "Troubleshooting", - "RECOVERY.md": "Recovery", -} +HOME = ("README.md", "Home") -LINK = re.compile(r"(\]\()([^)\s]+)(\))") +# Sidebar sections: docs/ file -> wiki page name, in order. +SECTIONS = [ + ("Start here", [ + ("GETTING_STARTED.md", "Getting-Started"), + ("INSTALL.md", "Installation"), + ("FAQ.md", "FAQ"), + ]), + ("Using it", [ + ("USER_GUIDE.md", "User-Guide"), + ("FILES.md", "Files-and-Offline"), + ("SYNCED_FOLDERS.md", "Synced-Folders"), + ("PHOTOS.md", "Photos"), + ("SHARING.md", "Sharing"), + ("COMPUTERS.md", "Computers"), + ("LAUNCHER_AND_TRAY.md", "Launcher-and-Tray"), + ]), + ("Reference", [ + ("CLI.md", "Command-Line-Reference"), + ("CONFIGURATION.md", "Configuration"), + ("SECURITY_MODEL.md", "Security-Model"), + ]), + ("When something goes wrong", [ + ("TROUBLESHOOTING.md", "Troubleshooting"), + ("RECOVERY.md", "Recovery"), + ]), + ("Contributing", [ + ("DEVELOPMENT.md", "Development"), + ("ARCHITECTURE.md", "Architecture"), + ("TESTING.md", "Testing"), + ("TRANSLATING.md", "Translating"), + ("ROADMAP.md", "Roadmap"), + ]), +] + +# Too long or too internal for the wiki; links to them go to GitHub. +EXCLUDED = {"BUGS.md", "CHANGELOG.md"} + +# Files outside docs/ whose links are checked but not published. +CHECKED = ["README.md", "CONTRIBUTING.md", "SECURITY.md"] + +PAGES = dict([HOME] + [page for _, pages in SECTIONS for page in pages]) +images_url = f"https://raw.githubusercontent.com/wiki/{repo_url.removeprefix('https://github.com/')}" + +LINK = re.compile(r"(\]\(|src=\")([^)\"\s]+)([)\"])") +FENCE = re.compile(r"^\s*(```|~~~)") +INLINE_CODE = re.compile(r"(`[^`\n]*`)") +HEADING = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$") +EXPLICIT_ID = re.compile(r" str: - if re.match(r"^[a-z]+:|^#", target): +def outside_fences(text): + """Yield (is_prose, chunk) so links in code blocks are left alone.""" + chunk, fenced = [], False + for line in text.splitlines(keepends=True): + if FENCE.match(line): + if chunk: + yield not fenced, "".join(chunk) + chunk, fenced = [], not fenced + yield False, line + continue + chunk.append(line) + if chunk: + yield not fenced, "".join(chunk) + + +def slug(heading): + """GitHub's anchor for a heading.""" + text = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", heading) + return re.sub(r"[^\w\- ]", "", text.lower()).replace(" ", "-") + + +anchor_cache = {} + + +def anchors(path): + if path not in anchor_cache: + found, seen = set(), {} + for prose, chunk in outside_fences(path.read_text()): + if not prose: + continue + for line in chunk.splitlines(): + found.update(EXPLICIT_ID.findall(line)) + m = HEADING.match(line) + if not m: + continue + base = slug(m.group(2)) + n = seen.get(base, 0) + seen[base] = n + 1 + found.add(base if n == 0 else f"{base}-{n}") + anchor_cache[path] = found + return anchor_cache[path] + + +def rewrite(source, target): + """The wiki form of one link in source, recording broken links.""" + if re.match(r"^[a-z]+:", target): return target path, _, anchor = target.partition("#") + resolved = (source.parent / path).resolve() if path else source + where = f"{source.relative_to(root)}: {target}" + if not resolved.is_relative_to(root) or not resolved.exists(): + errors.append(f"{where}: no such file") + return target + if anchor and resolved.suffix == ".md" and anchor not in anchors(resolved): + errors.append(f"{where}: no heading #{anchor}") + relative = resolved.relative_to(root) anchor = f"#{anchor}" if anchor else "" - if path in PAGES: - return PAGES[path] + anchor - # Anything else is a file in the repository: link to it on GitHub. - resolved = (root / "docs" / path).resolve().relative_to(root) - kind = "tree" if (root / resolved).is_dir() else "blob" - return f"{repo_url}/{kind}/main/{resolved}{anchor}" + if not path: + return anchor + if relative.parent == pathlib.Path("docs") and relative.name in PAGES: + return PAGES[relative.name] + anchor + if relative.parts[0] == "images": + images.add(relative) + return f"{images_url}/{relative}" + kind = "tree" if resolved.is_dir() else "blob" + return f"{repo_url}/{kind}/main/{relative}{anchor}" -def convert(text: str) -> str: - return LINK.sub(lambda m: m.group(1) + rewrite(m.group(2)) + m.group(3), text) +def convert(source): + out = [] + for prose, chunk in outside_fences(source.read_text()): + if prose: + # Odd pieces are `inline code`, which is left alone too. + pieces = INLINE_CODE.split(chunk) + pieces[::2] = [ + LINK.sub(lambda m: m.group(1) + rewrite(source, m.group(2)) + m.group(3), p) + for p in pieces[::2] + ] + chunk = "".join(pieces) + out.append(chunk) + return "".join(out) +def title(source): + return (docs / source).read_text().splitlines()[0].lstrip("# ").strip() + + +def without_title(text): + lines = text.splitlines(keepends=True) + if lines and lines[0].startswith("# "): + lines = lines[1:] + while lines and not lines[0].strip(): + lines = lines[1:] + return "".join(lines) + + +for name in sorted(p.name for p in docs.glob("*.md")): + if name not in PAGES and name not in EXCLUDED: + errors.append(f"docs/{name}: add it to SECTIONS or EXCLUDED in scripts/publish-wiki.sh") + +converted = {source: convert(docs / source) for source in PAGES} +published_images = set(images) +for name in CHECKED: + convert(root / name) + +if errors: + print("Broken documentation links:", file=sys.stderr) + for error in errors: + print(f" {error}", file=sys.stderr) + sys.exit(1) + +# The wiki is generated: anything not written below is stale. +for old in wiki.glob("*.md"): + old.unlink() +shutil.rmtree(wiki / "images", ignore_errors=True) + note = ( "\n\n" ) - for source, page in PAGES.items(): - text = (root / "docs" / source).read_text() - (wiki / f"{page}.md").write_text(note + convert(text)) + (wiki / f"{page}.md").write_text(note + without_title(converted[source])) +for image in published_images: + (wiki / image).parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(root / image, wiki / image) -def title(source: str) -> str: - return (root / "docs" / source).read_text().splitlines()[0].lstrip("# ").strip() - - -home = [ - note, - "# Proton Drive for Linux\n\n", - "An unofficial Proton Drive client for the Linux desktop: your files as a folder that ", - "downloads on demand, synced local folders, Photos, sharing, a GTK4 app with tray and search ", - "launcher, and a scriptable CLI.\n\n", - f"This wiki mirrors the documentation of version {version}. The source, releases and issue ", - f"tracker are in the [repository]({repo_url}).\n\n", - "## Pages\n\n", -] -home += [f"- [{title(source)}]({page})\n" for source, page in PAGES.items()] -home += [ - "\n## More\n\n", - f"- [Changelog]({repo_url}/blob/main/docs/CHANGELOG.md)\n", - f"- [Architecture]({repo_url}/blob/main/docs/ARCHITECTURE.md)\n", - f"- [Contributing]({repo_url}/blob/main/CONTRIBUTING.md)\n", - f"- [Report a problem]({repo_url}/issues/new/choose)\n", -] -(wiki / "Home.md").write_text("".join(home)) - -sidebar = ["**[Home](Home)**\n\n"] -sidebar += [f"- [{title(source)}]({page})\n" for source, page in PAGES.items()] +sidebar = ["**[Home](Home)**\n"] +for section, pages in SECTIONS: + sidebar.append(f"\n**{section}**\n\n") + sidebar += [f"- [{title(source)}]({page})\n" for source, page in pages] sidebar += [ "\n---\n\n", + f"[Changelog]({repo_url}/blob/main/docs/CHANGELOG.md) · ", f"[Releases]({repo_url}/releases) · [Issues]({repo_url}/issues)\n", ] -(wiki / "_Sidebar.md").write_text("".join(sidebar)) +(wiki / "_Sidebar.md").write_text(note + "".join(sidebar)) + +(wiki / "_Footer.md").write_text( + note + + f"Documentation of Proton Drive for Linux {version}, generated from " + + f"[`docs/`]({repo_url}/tree/{rev}/docs) at `{rev}`. " + + f"To change a page, edit it in the [repository]({repo_url}).\n" +) EOF +if [ "$mode" = check ]; then + echo "Documentation links are fine." + exit 0 +fi + cd "$wiki" git add -A if git diff --cached --quiet; then @@ -126,9 +269,9 @@ fi git commit --quiet -m "Update from docs at $rev" git show --stat --format='%s' HEAD -if [ "$push" = 1 ]; then +if [ "$mode" = push ]; then git push --quiet - echo "Pushed to $wiki_remote." + echo "Pushed to the wiki." else echo "Committed in $wiki. Review it, then push from there or re-run with --push." fi