mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
fmt
This commit is contained in:
1 parent
86fa8aef03
commit
f31633cb3a
7 files changed
+271
-104
No files matched your search
+27
-11
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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: >k4::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])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user