This commit is contained in:
nvrl committed 2026-06-30 04:58:40 +02:00
1 parent 86fa8aef03
commit f31633cb3a
7 files changed
+271 -104

No files matched your search

+27 -11
View File
@@ -11,7 +11,11 @@ use pdfs_core::config::AppDirs;
use pdfs_core::control::{Request as CtlRequest, Response as CtlResponse};
#[derive(Parser)]
#[command(name = "pdfs", version, about = "Proton Drive for Linux (Files On-Demand)")]
#[command(
name = "pdfs",
version,
about = "Proton Drive for Linux (Files On-Demand)"
)]
struct Cli {
#[command(subcommand)]
command: Command,
@@ -111,8 +115,7 @@ fn cmd_login(username: Option<String>) -> Result<()> {
// The 2FA prompt fires lazily — only if the account actually requires it.
let get_totp = || -> pdfs_core::Result<String> {
prompt_line("2FA code")
.map_err(|e| pdfs_core::Error::Other(format!("read 2FA code: {e}")))
prompt_line("2FA code").map_err(|e| pdfs_core::Error::Other(format!("read 2FA code: {e}")))
};
let rt = tokio::runtime::Runtime::new()?;
@@ -143,7 +146,9 @@ fn cmd_status() -> Result<()> {
}
// If a mount daemon is running, report live status from its control socket.
match control_request(CtlRequest::Status) {
Ok(CtlResponse::Status { mountpoint, pinned, .. }) => {
Ok(CtlResponse::Status {
mountpoint, pinned, ..
}) => {
println!("Mounted at {mountpoint} ({pinned} pinned)");
}
Ok(other) => println!("Mount: unexpected response {other:?}"),
@@ -209,8 +214,14 @@ fn cmd_mount(mountpoint: Option<PathBuf>) -> Result<()> {
});
let handle = rt.handle().clone();
let result =
pdfs_fuse::mount(client, handle, &mountpoint, cache, &control_socket, username);
let result = pdfs_fuse::mount(
client,
handle,
&mountpoint,
cache,
&control_socket,
username,
);
// Clean unmount: flush whatever tokens the store holds now so the keyring
// always ends a session with the live refresh token, not a stale one. Runs
@@ -224,7 +235,9 @@ fn cmd_mount(mountpoint: Option<PathBuf>) -> Result<()> {
}
fn cmd_pin(path: PathBuf) -> Result<()> {
match control_request(CtlRequest::Pin { path: path_arg(&path)? })? {
match control_request(CtlRequest::Pin {
path: path_arg(&path)?,
})? {
CtlResponse::Ok { message } => println!("{message}"),
CtlResponse::Error { message } => bail!("{message}"),
other => bail!("unexpected response: {other:?}"),
@@ -233,7 +246,9 @@ fn cmd_pin(path: PathBuf) -> Result<()> {
}
fn cmd_unpin(path: PathBuf) -> Result<()> {
match control_request(CtlRequest::Unpin { path: path_arg(&path)? })? {
match control_request(CtlRequest::Unpin {
path: path_arg(&path)?,
})? {
CtlResponse::Ok { message } => println!("{message}"),
CtlResponse::Error { message } => bail!("{message}"),
other => bail!("unexpected response: {other:?}"),
@@ -277,7 +292,9 @@ fn cmd_ls(path: Option<PathBuf>) -> Result<()> {
fn cmd_photos(limit: usize, offset: usize) -> Result<()> {
match control_request(CtlRequest::PhotosTimeline { offset, limit })? {
CtlResponse::Photos { available: false, .. } => {
CtlResponse::Photos {
available: false, ..
} => {
println!("This account has no photos volume.")
}
CtlResponse::Photos { items, .. } if items.is_empty() => println!("No photos."),
@@ -307,8 +324,7 @@ fn cmd_open_photo(uid: String) -> Result<()> {
/// relative paths are passed through and resolved against the mount root.
fn path_arg(path: &Path) -> Result<String> {
let p = if path.is_absolute() {
std::fs::canonicalize(path)
.with_context(|| format!("resolve {}", path.display()))?
std::fs::canonicalize(path).with_context(|| format!("resolve {}", path.display()))?
} else {
path.to_path_buf()
};
+1 -1
View File
@@ -12,7 +12,7 @@ use proton_sdk::config::ProtonClientConfiguration;
use proton_sdk::session::{PasswordMode, ProtonApiSession, ResumeParameters};
use serde::{Deserialize, Serialize};
use crate::config::{AppDirs, APP_VERSION, KEYRING_SERVICE, USER_AGENT};
use crate::config::{APP_VERSION, AppDirs, KEYRING_SERVICE, USER_AGENT};
use crate::error::{Error, Result};
/// Fixed keyring account name for the single stored session blob.
+18 -5
View File
@@ -81,7 +81,12 @@ impl ContentCache {
if let Some(parent) = pins_path.parent() {
std::fs::create_dir_all(parent)?;
}
Ok(Self { content_dir, thumb_dir, pins_path, max_bytes })
Ok(Self {
content_dir,
thumb_dir,
pins_path,
max_bytes,
})
}
/// Filesystem-safe, fixed-length cache key for a uid display string: the
@@ -192,7 +197,8 @@ impl ContentCache {
}
fn thumb_meta(&self, uid: &NodeUid, ttype: i32) -> PathBuf {
self.thumb_dir.join(format!("{}.t{ttype}.meta", Self::key(uid)))
self.thumb_dir
.join(format!("{}.t{ttype}.meta", Self::key(uid)))
}
/// Serve a cached thumbnail of `ttype` for `uid`, or `None` on miss/stale.
@@ -267,8 +273,12 @@ impl ContentCache {
return;
}
// Pinned blobs (by cache key) are exempt from eviction.
let pinned: HashSet<String> =
self.load_pins().pins.keys().map(|uid| Self::key_str(uid)).collect();
let pinned: HashSet<String> = self
.load_pins()
.pins
.keys()
.map(|uid| Self::key_str(uid))
.collect();
// Scan blobs (the `<key>` files; skip `.meta`/`.tmp` siblings), record
// size + last-access (mtime) for each.
@@ -336,7 +346,10 @@ impl ContentCache {
let key = uid.to_string();
file.pins.insert(
key.clone(),
Pin { uid: key, path: path.display().to_string() },
Pin {
uid: key,
path: path.display().to_string(),
},
);
self.save_pins(&file)
}
+4 -1
View File
@@ -99,7 +99,10 @@ pub enum Response {
Entries { entries: Vec<DirEntry> },
/// A page of the photos timeline. `available` is false when the account
/// has no photos volume.
Photos { available: bool, items: Vec<PhotoItem> },
Photos {
available: bool,
items: Vec<PhotoItem>,
},
/// An on-disk path the front-end can open (e.g. a downloaded photo).
FilePath { path: String },
/// The request failed.
+176 -62
View File
@@ -32,13 +32,13 @@ use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use fuser::{
BsdFileFlags, Config, Errno, FileAttr, FileHandle, FileType, Filesystem, FopenFlags, Generation,
INodeNo, LockOwner, MountOption, Notifier, OpenAccMode, OpenFlags, RenameFlags, ReplyAttr,
ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyWrite, Request,
Session, TimeOrNow, WriteFlags,
};
use fuser::ReplyXattr;
use fuser::{
BsdFileFlags, Config, Errno, FileAttr, FileHandle, FileType, Filesystem, FopenFlags,
Generation, INodeNo, LockOwner, MountOption, Notifier, OpenAccMode, OpenFlags, RenameFlags,
ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen,
ReplyWrite, Request, Session, TimeOrNow, WriteFlags,
};
use pdfs_core::cache::ContentCache;
use pdfs_core::control::{DirEntry, PhotoItem, Request as CtlRequest, Response as CtlResponse};
use proton_drive_rs::proton_sdk::ids::{DriveEventId, LinkId, NodeUid, VolumeId};
@@ -120,8 +120,14 @@ impl State {
let ino = self.next_ino;
self.next_ino += 1;
self.by_uid.insert(node.uid.clone(), ino);
self.entries
.insert(ino, Entry { uid: node.uid.clone(), parent, node });
self.entries.insert(
ino,
Entry {
uid: node.uid.clone(),
parent,
node,
},
);
ino
}
@@ -252,14 +258,16 @@ impl Core {
let mut ino = ROOT_INO;
let mut uid = {
let st = self.state.lock().unwrap();
st.entries.get(&ROOT_INO).map(|e| e.uid.clone()).ok_or(Errno::ENOENT)?
st.entries
.get(&ROOT_INO)
.map(|e| e.uid.clone())
.ok_or(Errno::ENOENT)?
};
for comp in rel.components() {
match comp {
Component::RootDir | Component::CurDir => continue,
Component::Normal(name) => {
let (child_ino, child_uid) =
self.lookup_child(ino, &name.to_string_lossy())?;
let (child_ino, child_uid) = self.lookup_child(ino, &name.to_string_lossy())?;
ino = child_ino;
uid = child_uid;
}
@@ -320,14 +328,20 @@ impl Core {
/// into the content cache and record it in the pin registry. Returns the
/// resolved node name on success.
fn pin(&self, rel: &Path) -> Result<String, String> {
let (ino, uid) = self.resolve_path(rel).map_err(|e| format!("resolve path: {e:?}"))?;
let (ino, uid) = self
.resolve_path(rel)
.map_err(|e| format!("resolve path: {e:?}"))?;
let (name, mtime, size) = {
let st = self.state.lock().unwrap();
let e = st.entries.get(&ino).ok_or("node vanished")?;
if !e.node.is_file() {
return Err("not a regular file".into());
}
(e.node.name.clone(), e.node.modification_time, node_size(&e.node))
(
e.node.name.clone(),
e.node.modification_time,
node_size(&e.node),
)
};
let bytes = self
.rt
@@ -336,7 +350,9 @@ impl Core {
self.cache
.store(&uid, mtime, size, &bytes)
.map_err(|e| format!("cache store: {e}"))?;
self.cache.add_pin(&uid, rel).map_err(|e| format!("pin: {e}"))?;
self.cache
.add_pin(&uid, rel)
.map_err(|e| format!("pin: {e}"))?;
Ok(name)
}
@@ -363,19 +379,28 @@ impl Core {
Errno::EIO
})?;
if let Some(bytes) = &bytes {
let _ = self.cache.store_thumbnail(&uid, ttype.as_i32(), mtime, bytes);
let _ = self
.cache
.store_thumbnail(&uid, ttype.as_i32(), mtime, bytes);
}
Ok(bytes)
}
/// Unpin the file at `rel`, evicting its cached content.
fn unpin(&self, rel: &Path) -> Result<String, String> {
let (ino, uid) = self.resolve_path(rel).map_err(|e| format!("resolve path: {e:?}"))?;
let (ino, uid) = self
.resolve_path(rel)
.map_err(|e| format!("resolve path: {e:?}"))?;
let name = {
let st = self.state.lock().unwrap();
st.entries.get(&ino).map(|e| e.node.name.clone()).unwrap_or_default()
st.entries
.get(&ino)
.map(|e| e.node.name.clone())
.unwrap_or_default()
};
self.cache.remove_pin(&uid).map_err(|e| format!("unpin: {e}"))?;
self.cache
.remove_pin(&uid)
.map_err(|e| format!("unpin: {e}"))?;
Ok(name)
}
@@ -391,8 +416,11 @@ impl Core {
/// the same lazy remote enumeration `readdir` uses, projected into
/// serializable [`DirEntry`]s (with per-file pin state).
fn list_dir(&self, rel: &Path) -> Result<Vec<DirEntry>, String> {
let (ino, _uid) = self.resolve_path(rel).map_err(|e| format!("resolve path: {e:?}"))?;
self.ensure_children(ino).map_err(|e| format!("enumerate: {e:?}"))?;
let (ino, _uid) = self
.resolve_path(rel)
.map_err(|e| format!("resolve path: {e:?}"))?;
self.ensure_children(ino)
.map_err(|e| format!("enumerate: {e:?}"))?;
// Snapshot the listing, then drop the lock before touching the on-disk
// pin registry so a slow disk read doesn't block FUSE metadata ops.
let rows: Vec<(String, bool, u64, i64, NodeUid)> = {
@@ -480,16 +508,22 @@ impl Core {
// Batch-fetch only the thumbnails not already cached fresh.
let want: Vec<NodeUid> = page
.iter()
.filter(|it| self.cache.cached_thumbnail_path(&it.uid, ttype, it.capture_time).is_none())
.filter(|it| {
self.cache
.cached_thumbnail_path(&it.uid, ttype, it.capture_time)
.is_none()
})
.map(|it| it.uid.clone())
.collect();
if !want.is_empty() {
let cap: HashMap<NodeUid, i64> =
page.iter().map(|it| (it.uid.clone(), it.capture_time)).collect();
match self
.rt
.block_on(self.photos().enumerate_thumbnails(&want, ThumbnailType::Thumbnail))
{
let cap: HashMap<NodeUid, i64> = page
.iter()
.map(|it| (it.uid.clone(), it.capture_time))
.collect();
match self.rt.block_on(
self.photos()
.enumerate_thumbnails(&want, ThumbnailType::Thumbnail),
) {
Ok(thumbs) => {
for ft in thumbs {
if let (Ok(bytes), Some(&tag)) = (ft.result, cap.get(&ft.file_uid)) {
@@ -546,7 +580,9 @@ impl Core {
/// when a fresh blob already exists). Lets a front-end open the file with
/// the user's default application without pinning it.
fn open_file(&self, rel: &Path) -> Result<PathBuf, String> {
let (ino, uid) = self.resolve_path(rel).map_err(|e| format!("resolve path: {e:?}"))?;
let (ino, uid) = self
.resolve_path(rel)
.map_err(|e| format!("resolve path: {e:?}"))?;
let (mtime, size) = {
let st = self.state.lock().unwrap();
let e = st.entries.get(&ino).ok_or("node vanished")?;
@@ -593,7 +629,11 @@ impl ProtonFs {
st.by_uid.insert(root.uid.clone(), ROOT_INO);
st.entries.insert(
ROOT_INO,
Entry { uid: root.uid.clone(), parent: ROOT_INO, node: root },
Entry {
uid: root.uid.clone(),
parent: ROOT_INO,
node: root,
},
);
}
// SAFETY: geteuid/getegid are infallible and have no preconditions.
@@ -682,9 +722,11 @@ fn media_type_for(name: &str) -> &'static str {
fn node_size(node: &Node) -> u64 {
match &node.kind {
NodeKind::Folder => 0,
NodeKind::File { claimed_size, total_size_on_storage, .. } => {
claimed_size.unwrap_or(*total_size_on_storage).max(0) as u64
}
NodeKind::File {
claimed_size,
total_size_on_storage,
..
} => claimed_size.unwrap_or(*total_size_on_storage).max(0) as u64,
}
}
@@ -804,7 +846,13 @@ impl Filesystem for ProtonFs {
st.next_fh += 1;
st.handles.insert(
fh,
WriteHandle { ino: ino.0, uid, buf: Vec::new(), seeded: false, dirty: false },
WriteHandle {
ino: ino.0,
uid,
buf: Vec::new(),
seeded: false,
dirty: false,
},
);
reply.opened(FileHandle(fh), FopenFlags::empty());
}
@@ -925,10 +973,22 @@ impl Filesystem for ProtonFs {
st.next_fh += 1;
st.handles.insert(
fh,
WriteHandle { ino, uid: new_uid, buf: Vec::new(), seeded: true, dirty: false },
WriteHandle {
ino,
uid: new_uid,
buf: Vec::new(),
seeded: true,
dirty: false,
},
);
let attr = self.attr(ino, &st.entries.get(&ino).unwrap().node);
reply.created(&TTL, &attr, Generation(0), FileHandle(fh), FopenFlags::empty());
reply.created(
&TTL,
&attr,
Generation(0),
FileHandle(fh),
FopenFlags::empty(),
);
}
fn write(
@@ -1001,7 +1061,14 @@ impl Filesystem for ProtonFs {
st.set_size(ino.0, len);
len
};
debug!(ino = ino.0, fh, offset, len = data.len(), new_len, "buffered write");
debug!(
ino = ino.0,
fh,
offset,
len = data.len(),
new_len,
"buffered write"
);
reply.written(data.len() as u32);
}
@@ -1101,14 +1168,28 @@ impl Filesystem for ProtonFs {
}
}
fn flush(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, _lock_owner: LockOwner, reply: ReplyEmpty) {
fn flush(
&self,
_req: &Request,
_ino: INodeNo,
fh: FileHandle,
_lock_owner: LockOwner,
reply: ReplyEmpty,
) {
match self.core.commit(fh.0) {
Ok(()) => reply.ok(),
Err(e) => reply.error(e),
}
}
fn fsync(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, _datasync: bool, reply: ReplyEmpty) {
fn fsync(
&self,
_req: &Request,
_ino: INodeNo,
fh: FileHandle,
_datasync: bool,
reply: ReplyEmpty,
) {
match self.core.commit(fh.0) {
Ok(()) => reply.ok(),
Err(e) => reply.error(e),
@@ -1170,18 +1251,19 @@ impl Filesystem for ProtonFs {
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.ok();
let new_uid = match self
.core
.rt
.block_on(self.core.client.create_folder(&parent_uid, &name, now))
{
Ok(u) => u,
Err(e) => {
error!(%parent_uid, name, error = %e, "create folder failed");
reply.error(Errno::EIO);
return;
}
};
let new_uid =
match self
.core
.rt
.block_on(self.core.client.create_folder(&parent_uid, &name, now))
{
Ok(u) => u,
Err(e) => {
error!(%parent_uid, name, error = %e, "create folder failed");
reply.error(Errno::EIO);
return;
}
};
let node = match self.core.fetch_node(&new_uid) {
Ok(n) => n,
Err(e) => {
@@ -1340,9 +1422,19 @@ impl Filesystem for ProtonFs {
/// The cache is authoritative-by-absence: dropping a directory's `children`
/// entry forces the next `lookup`/`readdir` to re-enumerate from the remote, so
/// most events only need to invalidate listings rather than re-fetch eagerly.
fn apply_event(state: &Mutex<State>, content: &ContentCache, notifier: &Notifier, event: &DriveEvent) {
fn apply_event(
state: &Mutex<State>,
content: &ContentCache,
notifier: &Notifier,
event: &DriveEvent,
) {
match event {
DriveEvent::NodeUpdated { node_uid, parent_node_uid, is_trashed, .. } => {
DriveEvent::NodeUpdated {
node_uid,
parent_node_uid,
is_trashed,
..
} => {
let mut st = state.lock().unwrap();
if *is_trashed {
// Trashing makes a node vanish from its parent listing.
@@ -1486,19 +1578,25 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
},
Ok(CtlRequest::Pin { path }) => match rel_to_mount(mountpoint, &path) {
Ok(rel) => match core.pin(&rel) {
Ok(name) => CtlResponse::Ok { message: format!("pinned {name}") },
Ok(name) => CtlResponse::Ok {
message: format!("pinned {name}"),
},
Err(e) => CtlResponse::Error { message: e },
},
Err(e) => CtlResponse::Error { message: e },
},
Ok(CtlRequest::Unpin { path }) => match rel_to_mount(mountpoint, &path) {
Ok(rel) => match core.unpin(&rel) {
Ok(name) => CtlResponse::Ok { message: format!("unpinned {name}") },
Ok(name) => CtlResponse::Ok {
message: format!("unpinned {name}"),
},
Err(e) => CtlResponse::Error { message: e },
},
Err(e) => CtlResponse::Error { message: e },
},
Ok(CtlRequest::ListPins) => CtlResponse::Pins { pins: core.cache.list_pins() },
Ok(CtlRequest::ListPins) => CtlResponse::Pins {
pins: core.cache.list_pins(),
},
Ok(CtlRequest::ListDir { path }) => match rel_to_mount(mountpoint, &path) {
Ok(rel) => match core.list_dir(&rel) {
Ok(entries) => CtlResponse::Entries { entries },
@@ -1508,26 +1606,40 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
},
Ok(CtlRequest::PhotosTimeline { offset, limit }) => {
match core.photos_timeline(offset, limit) {
Ok(Some(items)) => CtlResponse::Photos { available: true, items },
Ok(None) => CtlResponse::Photos { available: false, items: Vec::new() },
Ok(Some(items)) => CtlResponse::Photos {
available: true,
items,
},
Ok(None) => CtlResponse::Photos {
available: false,
items: Vec::new(),
},
Err(e) => CtlResponse::Error { message: e },
}
}
Ok(CtlRequest::OpenPhoto { uid }) => match parse_uid(&uid) {
Some(u) => match core.open_photo(&u) {
Ok(p) => CtlResponse::FilePath { path: p.display().to_string() },
Ok(p) => CtlResponse::FilePath {
path: p.display().to_string(),
},
Err(e) => CtlResponse::Error { message: e },
},
None => CtlResponse::Error { message: format!("bad uid: {uid}") },
None => CtlResponse::Error {
message: format!("bad uid: {uid}"),
},
},
Ok(CtlRequest::OpenFile { path }) => match rel_to_mount(mountpoint, &path) {
Ok(rel) => match core.open_file(&rel) {
Ok(p) => CtlResponse::FilePath { path: p.display().to_string() },
Ok(p) => CtlResponse::FilePath {
path: p.display().to_string(),
},
Err(e) => CtlResponse::Error { message: e },
},
Err(e) => CtlResponse::Error { message: e },
},
Err(e) => CtlResponse::Error { message: format!("bad request: {e}") },
Err(e) => CtlResponse::Error {
message: format!("bad request: {e}"),
},
};
let mut out = match serde_json::to_vec(&response) {
Ok(v) => v,
@@ -1618,7 +1730,9 @@ pub fn mount(
// `mount`'s blocking contract.
let session = Session::new(fs, mountpoint, &config)?.spawn()?;
let notifier = session.notifier();
rt.spawn(run_event_sync(client, scope, core.state, core.cache, notifier));
rt.spawn(run_event_sync(
client, scope, core.state, core.cache, notifier,
));
let result = session.join();
let _ = std::fs::remove_file(control_socket);
+41 -21
View File
@@ -87,8 +87,7 @@ struct Ui {
fn main() -> glib::ExitCode {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
)
.init();
@@ -152,7 +151,12 @@ fn build_window(app: &adw::Application) {
// Login has no title, so it never appears in the switcher; the three signed-in
// pages are titled + iconed so the bottom switcher lists them.
stack.add_named(&login_page, Some("login"));
stack.add_titled_with_icon(&main_page, Some("main"), "Account", "avatar-default-symbolic");
stack.add_titled_with_icon(
&main_page,
Some("main"),
"Account",
"avatar-default-symbolic",
);
stack.add_titled_with_icon(&browser_page, Some("browser"), "Files", "folder-symbolic");
stack.add_titled_with_icon(
&gallery_page,
@@ -364,9 +368,7 @@ fn build_main_page() -> (
storage_box.set_margin_top(6);
storage_box.set_margin_bottom(6);
let cache_bar = gtk4::ProgressBar::new();
let cache_label = gtk4::Label::builder()
.halign(gtk4::Align::Start)
.build();
let cache_label = gtk4::Label::builder().halign(gtk4::Align::Start).build();
cache_label.add_css_class("dim-label");
storage_box.append(&cache_bar);
storage_box.append(&cache_label);
@@ -388,7 +390,10 @@ fn build_main_page() -> (
inner.append(&storage_group);
inner.append(&pins_group);
let clamp = adw::Clamp::builder().maximum_size(560).child(&inner).build();
let clamp = adw::Clamp::builder()
.maximum_size(560)
.child(&inner)
.build();
let scroll = gtk4::ScrolledWindow::builder().child(&clamp).build();
(
@@ -424,7 +429,10 @@ fn wire_login(ui: &Rc<Ui>) {
let ui = ui.clone();
glib::spawn_future_local(async move {
let result = rx.recv().await.unwrap_or_else(|_| Err("login cancelled".into()));
let result = rx
.recv()
.await
.unwrap_or_else(|_| Err("login cancelled".into()));
ui.login_button.set_sensitive(true);
match result {
Ok(()) => {
@@ -635,7 +643,10 @@ fn repaint_pins(ui: &Rc<Ui>, pins: &[pdfs_core::cache::Pin]) {
/// channel that yields the [`Response`] once. Browser/gallery requests reach the
/// network through the daemon, so they must not block the GTK main loop the way
/// the cheap [`Request::Status`] poll in [`refresh`] can.
fn spawn_request(socket: PathBuf, req: Request) -> async_channel::Receiver<Result<Response, String>> {
fn spawn_request(
socket: PathBuf,
req: Request,
) -> async_channel::Receiver<Result<Response, String>> {
let (tx, rx) = async_channel::bounded(1);
std::thread::spawn(move || {
let result = send(&socket, &req).map_err(|e| e.to_string());
@@ -714,7 +725,10 @@ fn build_browser_page() -> (
.max_columns(10)
.build();
grid.add_css_class("file-grid");
let grid_scroll = gtk4::ScrolledWindow::builder().vexpand(true).child(&grid).build();
let grid_scroll = gtk4::ScrolledWindow::builder()
.vexpand(true)
.child(&grid)
.build();
// Column list.
let column_view = gtk4::ColumnView::builder()
@@ -753,7 +767,10 @@ fn build_browser_page() -> (
inner.append(&status);
inner.append(&view_stack);
(inner.upcast(), (model, back, crumb, status, grid, column_view))
(
inner.upcast(),
(model, back, crumb, status, grid, column_view),
)
}
/// Install the entry factories, columns, activation handlers and the back
@@ -986,7 +1003,9 @@ fn activate_entry(ui: &Rc<Ui>, entry: &DirEntry) {
glib::spawn_future_local(async move {
match rx.recv().await {
Ok(Ok(Response::FilePath { path })) => open_path(&path),
Ok(Ok(Response::Error { message })) => tracing::error!("open file failed: {message}"),
Ok(Ok(Response::Error { message })) => {
tracing::error!("open file failed: {message}")
}
_ => tracing::error!("open file request failed"),
}
});
@@ -1063,7 +1082,10 @@ fn icon_base_for(entry: &DirEntry) -> &'static str {
/// Format an epoch-seconds modification time as a short local date.
fn format_modified(secs: i64) -> String {
match glib::DateTime::from_unix_local(secs) {
Ok(dt) => dt.format("%-d %b %Y").map(|s| s.to_string()).unwrap_or_default(),
Ok(dt) => dt
.format("%-d %b %Y")
.map(|s| s.to_string())
.unwrap_or_default(),
Err(_) => String::new(),
}
}
@@ -1086,9 +1108,7 @@ fn load_browser(ui: &Rc<Ui>) {
Ok(Ok(Response::Entries { entries })) => repaint_browser(&ui, &entries),
Ok(Ok(Response::Error { message })) => browser_message(&ui, &message),
Ok(Ok(_)) => browser_message(&ui, "Unexpected reply from daemon."),
Ok(Err(_)) | Err(_) => {
browser_message(&ui, "Mount Proton Drive to browse your files.")
}
Ok(Err(_)) | Err(_) => browser_message(&ui, "Mount Proton Drive to browse your files."),
}
});
}
@@ -1207,7 +1227,9 @@ fn wire_gallery(ui: &Rc<Ui>, grid: &gtk4::GridView) {
glib::spawn_future_local(async move {
match rx.recv().await {
Ok(Ok(Response::FilePath { path })) => open_path(&path),
Ok(Ok(Response::Error { message })) => tracing::error!("open photo failed: {message}"),
Ok(Ok(Response::Error { message })) => {
tracing::error!("open photo failed: {message}")
}
_ => tracing::error!("open photo request failed"),
}
});
@@ -1258,9 +1280,7 @@ fn load_gallery(ui: &Rc<Ui>, append: bool) {
}
Ok(Ok(Response::Error { message })) => gallery_message(&ui, &message),
Ok(Ok(_)) => gallery_message(&ui, "Unexpected reply from daemon."),
Ok(Err(_)) | Err(_) => {
gallery_message(&ui, "Mount Proton Drive to see your photos.")
}
Ok(Err(_)) | Err(_) => gallery_message(&ui, "Mount Proton Drive to see your photos."),
}
});
}
@@ -1315,4 +1335,4 @@ fn human_bytes(bytes: u64) -> String {
} else {
format!("{value:.1} {}", UNITS[unit])
}
}
}
+4 -3
View File
@@ -42,7 +42,9 @@ struct DriveTray {
/// menu can still say "Logged in …" / "Not logged in" when no mount is running.
fn poll_state(socket: &Path, default_mountpoint: &Path) -> DriveState {
match send(socket, &Request::Status) {
Ok(Response::Status { mountpoint, pinned, .. }) => DriveState {
Ok(Response::Status {
mountpoint, pinned, ..
}) => DriveState {
line: format!("Mounted at {mountpoint} ({pinned} pinned)"),
mounted: true,
mountpoint: PathBuf::from(mountpoint),
@@ -174,8 +176,7 @@ impl Tray for DriveTray {
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
)
.init();