mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e0e284d5f | ||
|
|
f0fbd0775c | ||
|
|
d25a573e8f | ||
|
|
b2b0fe7b99 |
No files matched your search
Generated
+231
-252
File diff suppressed because it is too large.
Load diff
@@ -11,6 +11,7 @@ A fast, unofficial Proton Drive client for Linux. This client features an advanc
|
||||
- **Fuzzy Search Launcher Prompt (HUD)**: A Google Drive-style search launcher (`pdfs-prompt`) for finding and opening files or folders instantly, ideal for binding to a system-wide hotkey.
|
||||
- **Secure Credential Storage**: Integrates with the system Secret Service (GNOME Keyring, KWallet, etc.) with smart in-memory credential caching to avoid UI thread blockages.
|
||||
- **Proton Photos Support**: Access your Proton Photos timeline, view thumbnails, and download backed-up media natively (available in the GUI as a navigation tab and via the CLI).
|
||||
- **Human Verification (CAPTCHA) Recovery**: Detects sign-in gates (VPN/new IP challenges) and launches an embedded `WebKitWebView` dialog to safely complete the challenge, transparently retrying authentication with the earned token.
|
||||
|
||||
## Performance & Caching
|
||||
|
||||
@@ -63,22 +64,6 @@ sudo apt-get install -y \
|
||||
sudo pacman -S --needed pkgconf fuse3 gtk4 libadwaita libsecret dbus
|
||||
```
|
||||
|
||||
### Fedora (44+)
|
||||
```bash
|
||||
sudo dnf install -y \
|
||||
pkgconf-pkg-config fuse3-devel gtk4-devel libadwaita-devel \
|
||||
libsecret-devel dbus-devel glib2-devel cargo rust
|
||||
```
|
||||
|
||||
Runtime extras (pick your desktop):
|
||||
```bash
|
||||
# GNOME — keyring + tray (AppIndicator)
|
||||
sudo dnf install -y gnome-keyring gnome-shell-extension-appindicator xdg-utils
|
||||
|
||||
# KDE Plasma — KWallet (Secret Service); tray works via built-in SNI
|
||||
sudo dnf install -y kwallet xdg-utils
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Building from Source
|
||||
@@ -117,20 +102,6 @@ A local `PKGBUILD` is available under the `packaging/` directory. You can build
|
||||
cd packaging && makepkg -fi
|
||||
```
|
||||
|
||||
### 3. Fedora (local RPM)
|
||||
A local `.spec` is available under `packaging/`. From the repository root:
|
||||
```bash
|
||||
sudo dnf install -y rpm-build
|
||||
rpmbuild -bb packaging/proton-drive-linux.spec \
|
||||
--define "git_dir $PWD" \
|
||||
--define "_rpmdir $PWD/packaging/out" \
|
||||
--define "_builddir $PWD/packaging/build" \
|
||||
--define "_sourcedir $PWD" \
|
||||
--define "_specdir $PWD/packaging" \
|
||||
--define "_srcrpmdir $PWD/packaging/out"
|
||||
sudo dnf install packaging/out/x86_64/proton-drive-linux-*.rpm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automated Releases (CI/CD)
|
||||
|
||||
@@ -9,7 +9,7 @@ use super::Db;
|
||||
use crate::Result;
|
||||
|
||||
/// Current schema version. Bump on every forward migration added below.
|
||||
pub(super) const SCHEMA_VERSION: i64 = 14;
|
||||
pub(super) const SCHEMA_VERSION: i64 = 15;
|
||||
|
||||
impl Db {
|
||||
pub(super) fn migrate(&self) -> Result<()> {
|
||||
@@ -78,6 +78,9 @@ impl Db {
|
||||
if current < 14 {
|
||||
tx.execute_batch(MIGRATION_V14)?;
|
||||
}
|
||||
if current < 15 {
|
||||
tx.execute_batch(MIGRATION_V15)?;
|
||||
}
|
||||
tx.execute(
|
||||
"INSERT INTO sync_state (key, value) VALUES ('schema_version', ?1)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
@@ -340,3 +343,29 @@ ALTER TABLE photos ADD COLUMN kind INTEGER NOT NULL DEFAULT 0;
|
||||
const MIGRATION_V14: &str = "
|
||||
CREATE INDEX cache_entries_lru ON cache_entries(kind, last_accessed);
|
||||
";
|
||||
|
||||
/// Schema v15: key the search index by rowid so a row can be replaced without
|
||||
/// scanning the whole index (B12).
|
||||
///
|
||||
/// v3 built `nodes_fts` with the node's `uid` as an `UNINDEXED` column and
|
||||
/// `upsert_nodes` refreshed a row with `DELETE FROM nodes_fts WHERE uid = ?`.
|
||||
/// `UNINDEXED` means exactly what it says — the column is retrievable but not
|
||||
/// searchable — so that predicate could only ever be a full scan of the index
|
||||
/// (`EXPLAIN QUERY PLAN`: `SCAN nodes_fts VIRTUAL TABLE`). Every node written
|
||||
/// scanned every node indexed: measured at 6.5 ms per node against a 17k-node
|
||||
/// index, about half the cost of a cold listing, and *growing with the size of
|
||||
/// the account* rather than the size of the listing.
|
||||
///
|
||||
/// FTS5 deletes by `rowid` efficiently, and `nodes` is an ordinary rowid table
|
||||
/// (`uid` is its `TEXT PRIMARY KEY`, not `WITHOUT ROWID`), so its rowid is a
|
||||
/// stable per-node key that `ON CONFLICT DO UPDATE` preserves. Rebuild the index
|
||||
/// keyed by it and drop the `uid` column: searches now join on `rowid`, which is
|
||||
/// also a cheaper join than the old one on a TEXT uid.
|
||||
const MIGRATION_V15: &str = "
|
||||
DROP TABLE IF EXISTS nodes_fts;
|
||||
CREATE VIRTUAL TABLE nodes_fts USING fts5(
|
||||
name, tokenize='trigram'
|
||||
);
|
||||
INSERT INTO nodes_fts (rowid, name)
|
||||
SELECT rowid, name FROM nodes WHERE trashed = 0;
|
||||
";
|
||||
@@ -70,11 +70,22 @@ impl Db {
|
||||
// FTS5 has no UPSERT, so refresh the row by delete-then-insert.
|
||||
// Trashed nodes are kept out of the index entirely so they never
|
||||
// surface in search results.
|
||||
tx.execute("DELETE FROM nodes_fts WHERE uid = ?1", params![uid])?;
|
||||
//
|
||||
// Keyed by the node's rowid, never by its uid: the uid is not an
|
||||
// indexed FTS column, so deleting by it scans the entire index once
|
||||
// per node written (B12). The rowid is stable across the upsert
|
||||
// above — `ON CONFLICT DO UPDATE` keeps the existing row — so it is
|
||||
// the same key before and after.
|
||||
let rowid: i64 = tx.query_row(
|
||||
"SELECT rowid FROM nodes WHERE uid = ?1",
|
||||
params![uid],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
tx.execute("DELETE FROM nodes_fts WHERE rowid = ?1", params![rowid])?;
|
||||
if !node.trashed {
|
||||
tx.execute(
|
||||
"INSERT INTO nodes_fts (uid, name) VALUES (?1, ?2)",
|
||||
params![uid, node.name],
|
||||
"INSERT INTO nodes_fts (rowid, name) VALUES (?1, ?2)",
|
||||
params![rowid, node.name],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -88,8 +99,21 @@ impl Db {
|
||||
let uid = uid.to_string();
|
||||
let mut conn = self.conn.lock();
|
||||
let tx = conn.transaction()?;
|
||||
// Read the rowid before dropping the node: it is the search index's key
|
||||
// (B12), and it is gone once the row is. A node with no row leaves
|
||||
// nothing to unindex — the index is keyed off `nodes`, so it cannot
|
||||
// hold an entry the table never had.
|
||||
let rowid: Option<i64> = tx
|
||||
.query_row(
|
||||
"SELECT rowid FROM nodes WHERE uid = ?1",
|
||||
params![uid],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()?;
|
||||
tx.execute("DELETE FROM nodes WHERE uid = ?1", params![uid])?;
|
||||
tx.execute("DELETE FROM nodes_fts WHERE uid = ?1", params![uid])?;
|
||||
if let Some(rowid) = rowid {
|
||||
tx.execute("DELETE FROM nodes_fts WHERE rowid = ?1", params![rowid])?;
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -123,7 +147,7 @@ impl Db {
|
||||
.join(" AND ");
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT n.node_json, n.uid
|
||||
FROM nodes_fts f JOIN nodes n ON n.uid = f.uid
|
||||
FROM nodes_fts f JOIN nodes n ON n.rowid = f.rowid
|
||||
WHERE f.name MATCH ?1 AND n.trashed = 0 AND n.node_json IS NOT NULL
|
||||
ORDER BY f.rank LIMIT ?2",
|
||||
)?;
|
||||
|
||||
@@ -564,6 +564,78 @@ fn search_drops_fts_row_on_delete_and_trash() {
|
||||
assert_eq!(db.search("unique", 10).unwrap().len(), 0);
|
||||
}
|
||||
|
||||
/// Renaming re-indexes the node, which is the case delete-by-rowid has to get
|
||||
/// right: the *old* name must leave the index. A stale row here would be
|
||||
/// invisible in every other test — the new name is findable either way.
|
||||
#[test]
|
||||
fn renaming_a_node_replaces_the_name_in_the_search_index() {
|
||||
let db = Db::open_in_memory().unwrap();
|
||||
db.upsert_node(&folder("root", None, "My Files")).unwrap();
|
||||
db.upsert_node(&file("f1", "root", "beforename.txt", 1))
|
||||
.unwrap();
|
||||
assert_eq!(db.search("beforename", 10).unwrap().len(), 1);
|
||||
|
||||
db.upsert_node(&file("f1", "root", "aftername.txt", 1))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
db.search("aftername", 10).unwrap().len(),
|
||||
1,
|
||||
"the new name must be searchable"
|
||||
);
|
||||
assert_eq!(
|
||||
db.search("beforename", 10).unwrap().len(),
|
||||
0,
|
||||
"the old name must have left the index, not merely been added alongside"
|
||||
);
|
||||
}
|
||||
|
||||
/// B12: the cost of writing a listing must not scale with how much is already
|
||||
/// indexed. The old `DELETE FROM nodes_fts WHERE uid = ?` scanned the whole
|
||||
/// index once per node written, so a large account paid more to write the *same*
|
||||
/// folder — measured at 6.5 ms per node against a 17k-node index on the live DB.
|
||||
///
|
||||
/// Written as a ratio between two index sizes rather than an absolute time, so
|
||||
/// it fails on the scaling regression and not on a slow machine.
|
||||
#[test]
|
||||
fn writing_nodes_does_not_scale_with_the_size_of_the_index() {
|
||||
use std::time::Instant;
|
||||
|
||||
/// Fill `db` with `n` indexed nodes, then time re-writing a fixed batch.
|
||||
fn cost_against_index_of(n: usize) -> std::time::Duration {
|
||||
let db = Db::open_in_memory().unwrap();
|
||||
db.upsert_node(&folder("root", None, "My Files")).unwrap();
|
||||
let filler: Vec<_> = (0..n)
|
||||
.map(|i| file(&format!("bg{i}"), "root", &format!("background{i}.txt"), 1))
|
||||
.collect();
|
||||
db.upsert_nodes(&filler).unwrap();
|
||||
|
||||
// The batch under test is identical in both runs; only the amount of
|
||||
// already-indexed data around it differs.
|
||||
let batch: Vec<_> = (0..200)
|
||||
.map(|i| file(&format!("m{i}"), "root", &format!("measured{i}.txt"), 1))
|
||||
.collect();
|
||||
db.upsert_nodes(&batch).unwrap();
|
||||
|
||||
let t = Instant::now();
|
||||
db.upsert_nodes(&batch).unwrap();
|
||||
t.elapsed()
|
||||
}
|
||||
|
||||
let small = cost_against_index_of(200);
|
||||
let large = cost_against_index_of(5000);
|
||||
|
||||
// Calibration, measured while writing this: over the same 25× growth in
|
||||
// index size, the old full-scan path cost 6.4× more (46 ms → 294 ms) while
|
||||
// the rowid path costs 1.04× (39 ms → 41 ms). 3× sits well clear of both.
|
||||
println!("B12: 200-node write — against 200 indexed {small:?}, against 5000 {large:?}");
|
||||
assert!(
|
||||
large < small * 3,
|
||||
"writing a listing got dramatically more expensive as the index grew, \
|
||||
which is the B12 full-scan regression; small={small:?} large={large:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn activity(target: &str, kind: ActivityKind, ok: bool) -> ActivityEntry {
|
||||
ActivityEntry {
|
||||
time: 1700,
|
||||
|
||||
@@ -20,6 +20,7 @@ use pdfs_core::db::{StoredDevice, StoredSyncFolder};
|
||||
use pdfs_core::{CoreError, CoreResult};
|
||||
use proton_drive_rs::proton_sdk::ids::{DeviceUid, NodeUid};
|
||||
use proton_drive_rs::{DeviceType, Node};
|
||||
use std::sync::OnceLock;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use super::sync::{self, base_name};
|
||||
@@ -394,6 +395,12 @@ impl Core {
|
||||
next_fh: 1,
|
||||
db: self.db.clone(),
|
||||
}));
|
||||
// A fresh inode space needs a fresh notification channel: this fork's
|
||||
// session is the only one that knows these inodes, so it must be the one
|
||||
// notified about them. Filled in by `spawn_ondemand_mount`.
|
||||
fork.notifier = Arc::new(OnceLock::new());
|
||||
// Size upgrades are keyed by inode, which is per-fork too.
|
||||
fork.size_upgrades = Arc::new(Mutex::new(HashMap::new()));
|
||||
fork
|
||||
}
|
||||
|
||||
@@ -639,10 +646,15 @@ impl Core {
|
||||
MountOption::Subtype("protondrive".to_string()),
|
||||
MountOption::DefaultPermissions,
|
||||
];
|
||||
let fs = ProtonFs::new(self.fork_state(), root);
|
||||
Session::new(fs, local, &config)
|
||||
let core = self.fork_state();
|
||||
let fs = ProtonFs::new(core.clone(), root);
|
||||
let session = Session::new(fs, local, &config)
|
||||
.and_then(|s| s.spawn())
|
||||
.map_err(|e| CoreError::internal(format!("mount {}: {e}", local.display())))
|
||||
.map_err(|e| CoreError::internal(format!("mount {}: {e}", local.display())))?;
|
||||
// Hand this fork its own session's notification channel, so a background
|
||||
// size upgrade can drop the kernel's cached attrs for these inodes.
|
||||
let _ = core.notifier.set(session.notifier());
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Re-establish FUSE mounts for folders left in `ondemand` mode across a daemon
|
||||
|
||||
+1032
-40
File diff suppressed because it is too large.
Load diff
@@ -70,7 +70,7 @@ pub(crate) fn prompt_human_verification(
|
||||
let token_tx = Rc::new(RefCell::new(Some(token_tx)));
|
||||
|
||||
let tx = token_tx.clone();
|
||||
let dlg = dialog.clone();
|
||||
let dlg_weak = dialog.downgrade();
|
||||
content.connect_script_message_received(Some("hv"), move |_, value| {
|
||||
let Some(token) = extract_token(&value.to_str()) else {
|
||||
// Every `postMessage` on the page reaches here, most of them the
|
||||
@@ -81,7 +81,9 @@ pub(crate) fn prompt_human_verification(
|
||||
if let Some(tx) = tx.borrow_mut().take() {
|
||||
let _ = tx.send(token);
|
||||
}
|
||||
dlg.close();
|
||||
if let Some(dlg) = dlg_weak.upgrade() {
|
||||
dlg.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Covers the close button, Escape, and the user giving up.
|
||||
@@ -104,7 +106,14 @@ pub(crate) fn prompt_human_verification(
|
||||
/// unrecognised shape as success would hand the API an empty token and fail the
|
||||
/// login with a confusing error.
|
||||
fn extract_token(raw: &str) -> Option<String> {
|
||||
let value: serde_json::Value = serde_json::from_str(raw).ok()?;
|
||||
let mut value: serde_json::Value = serde_json::from_str(raw).ok()?;
|
||||
// If the message was stringified twice (e.g. raw is a double-quoted JSON string),
|
||||
// parse the inner string.
|
||||
if let Some(inner) = value.as_str() {
|
||||
if let Ok(parsed) = serde_json::from_str(inner) {
|
||||
value = parsed;
|
||||
}
|
||||
}
|
||||
// The payload is nested under `payload` and the message names itself in
|
||||
// `type`; both spellings of the success type have shipped.
|
||||
let kind = value.get("type")?.as_str()?;
|
||||
@@ -133,6 +142,12 @@ mod tests {
|
||||
assert_eq!(extract_token(raw).as_deref(), Some("tok-1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_double_serialized_completion_message_yields_its_token() {
|
||||
let raw = r#""{\"type\":\"HUMAN_VERIFICATION_SUCCESS\",\"payload\":{\"token\":\"tok-1\",\"type\":\"captcha\"}}""#;
|
||||
assert_eq!(extract_token(raw).as_deref(), Some("tok-1"));
|
||||
}
|
||||
|
||||
/// The page posts plenty that is not a completion; none of it may be
|
||||
/// mistaken for one, or the login retries with a garbage token.
|
||||
#[test]
|
||||
|
||||
@@ -515,12 +515,10 @@ fn build_window(app: &adw::Application) {
|
||||
ui_key.cycle_filter(back);
|
||||
glib::Propagation::Stop
|
||||
}
|
||||
gtk4::gdk::Key::Return | gtk4::gdk::Key::KP_Enter => {
|
||||
// Fall back to the top row when nothing is explicitly selected
|
||||
// (e.g. results haven't rendered yet); `open` guards the rest.
|
||||
ui_key.open(ui_key.cursor.get().unwrap_or(0), &window_key);
|
||||
glib::Propagation::Stop
|
||||
}
|
||||
// Return is deliberately absent: `GtkText` binds it to `activate`
|
||||
// and consumes it, so a bubble-phase controller on the window never
|
||||
// sees it while the entry has focus (which is always, here). It is
|
||||
// handled on the entry's `activate` signal instead — see below.
|
||||
_ => glib::Propagation::Proceed,
|
||||
}
|
||||
});
|
||||
@@ -528,6 +526,22 @@ fn build_window(app: &adw::Application) {
|
||||
// pointer has moved focus into it.
|
||||
window.add_controller(keys);
|
||||
|
||||
// Enter opens the row under the cursor. This lives on the entry rather than
|
||||
// with the other keys on the window because `GtkText` claims Return for its
|
||||
// own `activate` binding, so a bubble-phase controller upstream of the
|
||||
// focused entry is never reached. Enter with focus in the list is covered by
|
||||
// `row_activated` above.
|
||||
let ui_activate = ui.clone();
|
||||
let window_activate = window.clone();
|
||||
entry.connect_activate(move |_| {
|
||||
if ui_activate.opening.get() {
|
||||
return;
|
||||
}
|
||||
// Fall back to the top row when nothing is explicitly selected (e.g.
|
||||
// results haven't rendered yet); `open` guards the rest.
|
||||
ui_activate.open(ui_activate.cursor.get().unwrap_or(0), &window_activate);
|
||||
});
|
||||
|
||||
let debounce: Rc<RefCell<Option<glib::SourceId>>> = Rc::new(RefCell::new(None));
|
||||
let ui_changed = ui.clone();
|
||||
entry.connect_changed(move |entry| {
|
||||
|
||||
@@ -284,3 +284,63 @@ See [RECOVERY.md](RECOVERY.md) for what a lost machine means for the plaintext d
|
||||
- Treat the cache and state directories as being as sensitive as the Drive contents themselves.
|
||||
- Multi-user machines warrant checking `~/.cache` and `~/.local/state` are `0700` until §8.5 is addressed.
|
||||
- Purging the cache (`pdfs` settings, or `PurgeCache` over IPC) removes content but **not** `cache.db`, and deliberately never removes undrained `staging/` or `recovery/` blobs.
|
||||
|
||||
---
|
||||
|
||||
## 9. Human Verification (CAPTCHA) Flow
|
||||
|
||||
When logging in from an unfamiliar IP address or VPN, the Proton API may gate the sign-in with a human verification challenge (CAPTCHA). This client handles this asynchronously and interactively.
|
||||
|
||||
### 9.1 Sequence of Verification and Re-Authentication
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor User as User (GUI)
|
||||
participant Core as pdfs-core (Auth)
|
||||
participant UI as pdfs-gui (Login Page)
|
||||
participant Web as WebKitWebView Dialog
|
||||
participant API as Proton API Server
|
||||
|
||||
User->>UI: Enter credentials & click Sign In
|
||||
UI->>Core: login_interactive()
|
||||
Core->>API: auth/v4 (Initial SRP Handshake)
|
||||
API-->>Core: HTTP 422 (Error 9001: CAPTCHA Challenge URL)
|
||||
Core->>UI: Error::HumanVerificationRequired(hv)
|
||||
UI->>UI: Block login thread, dispatch to GTK main loop
|
||||
UI->>Web: Create dialog & load verification URL
|
||||
Note over User, Web: User completes CAPTCHA in embedded WebView
|
||||
Web->>UI: window.postMessage(HUMAN_VERIFICATION_SUCCESS)
|
||||
UI->>UI: Extract token (with double-serialization safety)
|
||||
UI->>UI: Close dialog & send token to blocked login thread
|
||||
UI->>Core: login_verified(with verification token)
|
||||
Core->>API: auth/v4 (SRP retry with x-pm-human-verification-token)
|
||||
API-->>Core: Returns session tokens
|
||||
Core-->>UI: Login Successful
|
||||
```
|
||||
|
||||
### 9.2 Key Technical Design Decisions
|
||||
|
||||
1. **Weak Reference UI Binding:** To prevent memory leaks and strong reference cycles between the parent dialog, the child `WebKitWebView`, the script message manager, and the connection callback, the dialog is downgraded to a `WeakRef` inside the callback:
|
||||
```rust
|
||||
let dlg_weak = dialog.downgrade();
|
||||
content.connect_script_message_received(Some("hv"), move |_, value| {
|
||||
// ...
|
||||
if let Some(dlg) = dlg_weak.upgrade() {
|
||||
dlg.close();
|
||||
}
|
||||
});
|
||||
```
|
||||
2. **Double-Serialization Tolerance:** The JavaScript message listener forwards event data as a JSON string to the native handler. Since the underlying page may post either JS objects or pre-serialized JSON strings, the Rust side performs dual-phase parsing:
|
||||
```rust
|
||||
let mut value = serde_json::from_str(raw).ok()?;
|
||||
if let Some(inner) = value.as_str() {
|
||||
if let Ok(parsed) = serde_json::from_str(inner) {
|
||||
value = parsed;
|
||||
}
|
||||
}
|
||||
```
|
||||
This ensures compatibility with all versions of Proton's client verification scripts.
|
||||
3. **SRP Handshake Reset:** Because a gated login burns the SRP handshake on the API side, the client cannot simply resume the previous request. Instead, `auth::login_interactive` restarts the SRP process from scratch with the verification credentials attached, keeping the complex handshake details isolated from the front-end.
|
||||
4. **CLI Fallback:** Since the CLI has no native web browser engine, hitting the CAPTCHA gate fails immediately with a user-friendly message directing the user to sign in once via the GUI (`pdfs-app`) to persist the authenticated session keys to the system keyring.
|
||||
|
||||
+1140
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -9,7 +9,7 @@
|
||||
# Build + install: cd packaging && makepkg -fi
|
||||
|
||||
pkgname=proton-drive-linux
|
||||
pkgver=0.5.0
|
||||
pkgver=0.6.1
|
||||
pkgrel=1
|
||||
pkgdesc="Proton Drive client for Linux: FUSE files-on-demand mount, CLI, GTK4 app + tray"
|
||||
arch=('x86_64')
|
||||
|
||||
@@ -1,78 +1,45 @@
|
||||
# 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).
|
||||
|
||||
%global debug_package %{nil}
|
||||
# Match Arch PKGBUILD `!lto` — LTO has broken some GTK/Rust links in practice.
|
||||
%global _lto_cflags %{nil}
|
||||
|
||||
Name: proton-drive-linux
|
||||
Version: 0.4.0
|
||||
Version: %{version}
|
||||
Release: 1%{?dist}
|
||||
Summary: Proton Drive client for Linux (FUSE, CLI, GTK4 app + tray)
|
||||
Summary: Proton Drive client for Linux (unofficial)
|
||||
|
||||
License: MIT
|
||||
URL: https://github.com/narl/proton-drive-linux
|
||||
ExclusiveArch: x86_64
|
||||
|
||||
BuildRequires: cargo
|
||||
BuildRequires: rust
|
||||
BuildRequires: pkgconf-pkg-config
|
||||
BuildRequires: fuse3-devel
|
||||
BuildRequires: gtk4-devel
|
||||
BuildRequires: libadwaita-devel
|
||||
BuildRequires: libsecret-devel
|
||||
BuildRequires: dbus-devel
|
||||
BuildRequires: glib2-devel
|
||||
# Disable debuginfo package generation since we package pre-built release binaries
|
||||
%global debug_package %{nil}
|
||||
# Disable binary strip/post-processing checks to avoid issues with pre-compiled binaries
|
||||
%global __os_install_post %{nil}
|
||||
|
||||
Requires: fuse3
|
||||
Requires: gtk4
|
||||
Requires: libadwaita
|
||||
Requires: gtk4 >= 4.12.0
|
||||
Requires: libadwaita >= 1.5.0
|
||||
Requires: libsecret
|
||||
Requires: xdg-utils
|
||||
|
||||
# DE-specific; do not Require a single desktop environment.
|
||||
Recommends: gnome-keyring
|
||||
Recommends: gnome-shell-extension-appindicator
|
||||
Recommends: kwallet
|
||||
|
||||
Provides: pdfs = %{version}-%{release}
|
||||
Requires: dbus
|
||||
|
||||
%description
|
||||
Unofficial Proton Drive client featuring a FUSE files-on-demand mount,
|
||||
CLI, GTK4/Libadwaita GUI, system tray, and search launcher.
|
||||
|
||||
%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)
|
||||
test -f %{git_dir}/Cargo.toml
|
||||
cp -a %{git_dir}/LICENSE .
|
||||
%build
|
||||
cd %{git_dir}
|
||||
cargo build --release --locked \
|
||||
--bin pdfs \
|
||||
--bin pdfs-tray \
|
||||
--bin pdfs-app \
|
||||
--bin pdfs-prompt
|
||||
A fast, unofficial Proton Drive client for Linux. This client features an advanced files-on-demand FUSE virtual mount with block-level caching, a command-line interface (CLI), and a fully non-blocking GTK4 desktop application with system tray integration.
|
||||
|
||||
%install
|
||||
rel=%{git_dir}/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
|
||||
mkdir -p %{buildroot}%{_bindir}
|
||||
mkdir -p %{buildroot}%{_datadir}/applications
|
||||
mkdir -p %{buildroot}%{_sysconfdir}/xdg/autostart
|
||||
mkdir -p %{buildroot}%{_datadir}/icons/hicolor/scalable/apps
|
||||
mkdir -p %{buildroot}%{_usr}/lib/systemd/user
|
||||
mkdir -p %{buildroot}%{_datadir}/licenses/proton-drive-linux
|
||||
|
||||
install -D -m0644 %{git_dir}/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 \
|
||||
%{buildroot}%{_sysconfdir}/xdg/autostart/io.narl.proton-drive-linux-tray.desktop
|
||||
install -D -m0644 %{git_dir}/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 \
|
||||
%{buildroot}/usr/lib/systemd/user/proton-drive.service
|
||||
cp %{_sourcedir}/target/release/pdfs %{buildroot}%{_bindir}/
|
||||
cp %{_sourcedir}/target/release/pdfs-tray %{buildroot}%{_bindir}/
|
||||
cp %{_sourcedir}/target/release/pdfs-app %{buildroot}%{_bindir}/
|
||||
cp %{_sourcedir}/target/release/pdfs-prompt %{buildroot}%{_bindir}/
|
||||
|
||||
cp %{_sourcedir}/packaging/io.narl.proton-drive-linux.desktop %{buildroot}%{_datadir}/applications/
|
||||
cp %{_sourcedir}/packaging/io.narl.proton-drive-linux-tray.desktop %{buildroot}%{_sysconfdir}/xdg/autostart/
|
||||
cp %{_sourcedir}/packaging/io.narl.proton-drive-linux.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/
|
||||
cp %{_sourcedir}/packaging/proton-drive.service %{buildroot}%{_usr}/lib/systemd/user/
|
||||
cp %{_sourcedir}/LICENSE %{buildroot}%{_datadir}/licenses/proton-drive-linux/
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%{_bindir}/pdfs
|
||||
%{_bindir}/pdfs-tray
|
||||
%{_bindir}/pdfs-app
|
||||
@@ -80,8 +47,5 @@ install -D -m0644 %{git_dir}/packaging/proton-drive.service \
|
||||
%{_datadir}/applications/io.narl.proton-drive-linux.desktop
|
||||
%{_sysconfdir}/xdg/autostart/io.narl.proton-drive-linux-tray.desktop
|
||||
%{_datadir}/icons/hicolor/scalable/apps/io.narl.proton-drive-linux.svg
|
||||
/usr/lib/systemd/user/proton-drive.service
|
||||
|
||||
%changelog
|
||||
* Sun Jul 19 2026 Local Packager - 0.4.0-1
|
||||
- Initial Fedora local package (in-tree cargo build).
|
||||
%{_usr}/lib/systemd/user/proton-drive.service
|
||||
%{_datadir}/licenses/proton-drive-linux/LICENSE
|
||||
Reference in new issue
Block a user