mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
transfer progress api
This commit is contained in:
1 parent
f9000f8540
commit
35bc957cd5
6 files changed
+340
-25
No files matched your search
Generated
+4
-4
@@ -3085,9 +3085,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proton-drive-rs"
|
name = "proton-drive-rs"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c9380c88a032e2ff079b2d5a6221ccc40ebeee3ebbb4ea60d4d65ef770e4ca8d"
|
checksum = "76e4399b4d9b076c5886c7db87ae7f70322dd7127e6c5a4e7eff8719434d2434"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -3110,9 +3110,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proton-sdk"
|
name = "proton-sdk"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1da22604d2568af2d15be6a45f23f8c5b95fa775bdac1540e833c2eafb587888"
|
checksum = "099f6f20709452f2cdb0cee4db0a82e61a29bec261295812908bf956ffcdaa00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
+3
-2
@@ -11,8 +11,8 @@ rust-version = "1.96"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# Local Proton SDK crates (path deps until published; switch to versioned later).
|
# Local Proton SDK crates (path deps until published; switch to versioned later).
|
||||||
proton-sdk = "0.1.2"
|
proton-sdk = "0.1.3"
|
||||||
proton-drive-rs = "0.1.2"
|
proton-drive-rs = "0.1.3"
|
||||||
|
|
||||||
# Internal crates.
|
# Internal crates.
|
||||||
pdfs-core = { path = "crates/pdfs-core" }
|
pdfs-core = { path = "crates/pdfs-core" }
|
||||||
@@ -33,3 +33,4 @@ clap = { version = "4", features = ["derive"] }
|
|||||||
rpassword = "7"
|
rpassword = "7"
|
||||||
fuser = "0.17"
|
fuser = "0.17"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|
||||||
@@ -119,6 +119,8 @@ enum Command {
|
|||||||
/// Destination folder path, inside the mountpoint or relative to it.
|
/// Destination folder path, inside the mountpoint or relative to it.
|
||||||
parent: PathBuf,
|
parent: PathBuf,
|
||||||
},
|
},
|
||||||
|
/// Show the daemon's in-flight transfers (active uploads/downloads).
|
||||||
|
Transfers,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
@@ -151,6 +153,7 @@ fn main() -> Result<()> {
|
|||||||
Command::Rm { path } => cmd_rm(path),
|
Command::Rm { path } => cmd_rm(path),
|
||||||
Command::Mkdir { parent, name } => cmd_mkdir(parent, name),
|
Command::Mkdir { parent, name } => cmd_mkdir(parent, name),
|
||||||
Command::Upload { file, parent } => cmd_upload(file, parent),
|
Command::Upload { file, parent } => cmd_upload(file, parent),
|
||||||
|
Command::Transfers => cmd_transfers(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +369,34 @@ fn cmd_unpin(path: PathBuf) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cmd_transfers() -> Result<()> {
|
||||||
|
use pdfs_core::control::TransferDirection;
|
||||||
|
match control_request(CtlRequest::GetQueueStatus)? {
|
||||||
|
CtlResponse::Transfers { items } if items.is_empty() => println!("No active transfers."),
|
||||||
|
CtlResponse::Transfers { items } => {
|
||||||
|
for t in items {
|
||||||
|
let arrow = match t.direction {
|
||||||
|
TransferDirection::Download => "↓",
|
||||||
|
TransferDirection::Upload => "↑",
|
||||||
|
};
|
||||||
|
let pct = if t.bytes_total > 0 {
|
||||||
|
format!("{:.0}%", 100.0 * t.bytes_completed as f64 / t.bytes_total as f64)
|
||||||
|
} else {
|
||||||
|
"—".to_string()
|
||||||
|
};
|
||||||
|
let speed = t.speed_bytes_sec as f64 / 1_048_576.0;
|
||||||
|
println!(
|
||||||
|
"{arrow} {:>4} {:>8} / {:>8} {speed:>6.2} MiB/s {}",
|
||||||
|
pct, t.bytes_completed, t.bytes_total, t.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CtlResponse::Error { message } => bail!("{message}"),
|
||||||
|
other => bail!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn cmd_pins() -> Result<()> {
|
fn cmd_pins() -> Result<()> {
|
||||||
match control_request(CtlRequest::ListPins)? {
|
match control_request(CtlRequest::ListPins)? {
|
||||||
CtlResponse::Pins { pins } if pins.is_empty() => println!("No pinned files."),
|
CtlResponse::Pins { pins } if pins.is_empty() => println!("No pinned files."),
|
||||||
|
|||||||
@@ -89,6 +89,34 @@ pub enum Request {
|
|||||||
/// persist it to config so the next mount keeps it. Replies with
|
/// persist it to config so the next mount keeps it. Replies with
|
||||||
/// [`Response::Ok`].
|
/// [`Response::Ok`].
|
||||||
SetCacheBudget { bytes: u64 },
|
SetCacheBudget { bytes: u64 },
|
||||||
|
/// Snapshot the daemon's in-flight transfers (active uploads/downloads).
|
||||||
|
/// Replies with [`Response::Transfers`]. Cheap to poll: the daemon keeps the
|
||||||
|
/// registry in memory, so a front-end can render a live progress widget.
|
||||||
|
GetQueueStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which way an active transfer is moving, in a [`TransferItem`].
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TransferDirection {
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One in-flight transfer in a [`Response::Transfers`] snapshot.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct TransferItem {
|
||||||
|
/// Node uid in `volume~link` form (empty for an upload whose uid isn't known
|
||||||
|
/// until the draft is sealed).
|
||||||
|
pub uid: String,
|
||||||
|
/// File name being transferred.
|
||||||
|
pub name: String,
|
||||||
|
pub direction: TransferDirection,
|
||||||
|
/// Bytes moved so far.
|
||||||
|
pub bytes_completed: u64,
|
||||||
|
/// Total bytes expected, or `0` when unknown (indeterminate progress).
|
||||||
|
pub bytes_total: u64,
|
||||||
|
/// Average throughput since the transfer began, bytes per second.
|
||||||
|
pub speed_bytes_sec: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One entry in a [`Request::ListDir`] listing.
|
/// One entry in a [`Request::ListDir`] listing.
|
||||||
@@ -178,6 +206,8 @@ pub enum Response {
|
|||||||
FilePath { path: String },
|
FilePath { path: String },
|
||||||
/// Full-text search results (reply to [`Request::Search`]).
|
/// Full-text search results (reply to [`Request::Search`]).
|
||||||
SearchResults { hits: Vec<SearchHit> },
|
SearchResults { hits: Vec<SearchHit> },
|
||||||
|
/// A snapshot of in-flight transfers (reply to [`Request::GetQueueStatus`]).
|
||||||
|
Transfers { items: Vec<TransferItem> },
|
||||||
/// The request failed.
|
/// The request failed.
|
||||||
Error { message: String },
|
Error { message: String },
|
||||||
}
|
}
|
||||||
|
|||||||
+103
-19
@@ -52,13 +52,18 @@ use pdfs_core::cache::{BLOCK_SIZE, ContentCache};
|
|||||||
use pdfs_core::config::AppDirs;
|
use pdfs_core::config::AppDirs;
|
||||||
use pdfs_core::control::{
|
use pdfs_core::control::{
|
||||||
DirEntry, PhotoItem, Request as CtlRequest, Response as CtlResponse, SearchHit,
|
DirEntry, PhotoItem, Request as CtlRequest, Response as CtlResponse, SearchHit,
|
||||||
|
TransferDirection,
|
||||||
};
|
};
|
||||||
use pdfs_core::db::{Db, StoredNode};
|
use pdfs_core::db::{Db, StoredNode};
|
||||||
|
use proton_drive_rs::proton_sdk::error::ProtonError;
|
||||||
use proton_drive_rs::proton_sdk::ids::{DriveEventId, LinkId, NodeUid, VolumeId};
|
use proton_drive_rs::proton_sdk::ids::{DriveEventId, LinkId, NodeUid, VolumeId};
|
||||||
use proton_drive_rs::{
|
use proton_drive_rs::{
|
||||||
DriveEvent, DriveEventScopeId, Node, NodeKind, PhotosTimelineItem, ProtonDriveClient,
|
DriveEvent, DriveEventScopeId, Node, NodeKind, PhotosTimelineItem, ProtonDriveClient,
|
||||||
ProtonPhotosClient, ThumbnailType,
|
ProtonPhotosClient, ThumbnailType,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod transfers;
|
||||||
|
use transfers::{CountingReader, CountingWriter, TransferRegistry};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
/// Attribute/entry cache lifetime handed back to the kernel. Long because the
|
/// Attribute/entry cache lifetime handed back to the kernel. Long because the
|
||||||
@@ -293,6 +298,9 @@ struct Core {
|
|||||||
/// requests slice memory instead of re-fetching the whole timeline per page.
|
/// requests slice memory instead of re-fetching the whole timeline per page.
|
||||||
/// `None` until first fetched; refreshed once older than [`TIMELINE_TTL`].
|
/// `None` until first fetched; refreshed once older than [`TIMELINE_TTL`].
|
||||||
timeline: Arc<Mutex<Option<TimelineCache>>>,
|
timeline: Arc<Mutex<Option<TimelineCache>>>,
|
||||||
|
/// In-flight upload/download progress, served to `GetQueueStatus`. Shared
|
||||||
|
/// across the FUSE session and the control-socket task.
|
||||||
|
transfers: Arc<TransferRegistry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The whole photos timeline plus when it was fetched, for TTL freshness.
|
/// The whole photos timeline plus when it was fetched, for TTL freshness.
|
||||||
@@ -681,6 +689,17 @@ impl Core {
|
|||||||
error!(%uid, error = %e, "reopen scratch file failed");
|
error!(%uid, error = %e, "reopen scratch file failed");
|
||||||
Errno::EIO
|
Errno::EIO
|
||||||
})?;
|
})?;
|
||||||
|
let name = {
|
||||||
|
let st = self.state.lock().unwrap();
|
||||||
|
st.entries
|
||||||
|
.get(&ino)
|
||||||
|
.map(|e| e.node.name.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
let guard = self
|
||||||
|
.transfers
|
||||||
|
.begin(name, uid.to_string(), TransferDirection::Upload, len);
|
||||||
|
let reader = CountingReader::new(reader, &guard);
|
||||||
self.rt
|
self.rt
|
||||||
.block_on(self.client.upload_new_revision_from(
|
.block_on(self.client.upload_new_revision_from(
|
||||||
&uid,
|
&uid,
|
||||||
@@ -693,6 +712,7 @@ impl Core {
|
|||||||
error!(%uid, error = %e, "upload new revision failed");
|
error!(%uid, error = %e, "upload new revision failed");
|
||||||
Errno::EIO
|
Errno::EIO
|
||||||
})?;
|
})?;
|
||||||
|
drop(guard);
|
||||||
let now = now_secs();
|
let now = now_secs();
|
||||||
{
|
{
|
||||||
let mut st = self.state.lock().unwrap();
|
let mut st = self.state.lock().unwrap();
|
||||||
@@ -719,6 +739,48 @@ impl Core {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Download a whole file's plaintext, registering the transfer so
|
||||||
|
/// `GetQueueStatus` can report its progress. `total` is the expected size
|
||||||
|
/// (`0` if unknown). Streams through [`download_file_to`] so each block ticks
|
||||||
|
/// the progress counter.
|
||||||
|
///
|
||||||
|
/// [`download_file_to`]: ProtonDriveClient::download_file_to
|
||||||
|
fn download_file_tracked(
|
||||||
|
&self,
|
||||||
|
uid: &NodeUid,
|
||||||
|
name: &str,
|
||||||
|
total: u64,
|
||||||
|
) -> std::result::Result<Vec<u8>, ProtonError> {
|
||||||
|
let guard = self
|
||||||
|
.transfers
|
||||||
|
.begin(name, uid.to_string(), TransferDirection::Download, total);
|
||||||
|
let mut out = CountingWriter::new(Vec::with_capacity(total as usize), &guard);
|
||||||
|
self.rt
|
||||||
|
.block_on(self.client.download_file_to(uid, &mut out))?;
|
||||||
|
Ok(out.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`download_file_tracked`] for a photo, streaming through the photos
|
||||||
|
/// client's [`download_photo_to`].
|
||||||
|
///
|
||||||
|
/// [`download_file_tracked`]: Core::download_file_tracked
|
||||||
|
/// [`download_photo_to`]: ProtonPhotosClient::download_photo_to
|
||||||
|
fn download_photo_tracked(
|
||||||
|
&self,
|
||||||
|
photos: &ProtonPhotosClient,
|
||||||
|
uid: &NodeUid,
|
||||||
|
name: &str,
|
||||||
|
total: u64,
|
||||||
|
) -> std::result::Result<Vec<u8>, ProtonError> {
|
||||||
|
let guard = self
|
||||||
|
.transfers
|
||||||
|
.begin(name, uid.to_string(), TransferDirection::Download, total);
|
||||||
|
let mut out = CountingWriter::new(Vec::with_capacity(total as usize), &guard);
|
||||||
|
self.rt
|
||||||
|
.block_on(photos.download_photo_to(uid, &mut out))?;
|
||||||
|
Ok(out.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
/// Pin the node at mountpoint-relative `rel`. A file downloads its full
|
/// Pin the node at mountpoint-relative `rel`. A file downloads its full
|
||||||
/// plaintext into the content cache; a folder records a recursive pin and
|
/// plaintext into the content cache; a folder records a recursive pin and
|
||||||
/// downloads every descendant file (selective sync). Returns a human message.
|
/// downloads every descendant file (selective sync). Returns a human message.
|
||||||
@@ -746,8 +808,7 @@ impl Core {
|
|||||||
return Ok(format!("{name} ({n} files)"));
|
return Ok(format!("{name} ({n} files)"));
|
||||||
}
|
}
|
||||||
let bytes = self
|
let bytes = self
|
||||||
.rt
|
.download_file_tracked(&uid, &name, size)
|
||||||
.block_on(self.client.download_file(&uid))
|
|
||||||
.map_err(|e| format!("download: {e}"))?;
|
.map_err(|e| format!("download: {e}"))?;
|
||||||
self.cache
|
self.cache
|
||||||
.store(&uid, mtime, size, &bytes)
|
.store(&uid, mtime, size, &bytes)
|
||||||
@@ -763,7 +824,7 @@ impl Core {
|
|||||||
/// Walks the tree depth-first, enumerating each folder so a cold subtree is
|
/// Walks the tree depth-first, enumerating each folder so a cold subtree is
|
||||||
/// fully discovered; the lock is dropped before each network download.
|
/// fully discovered; the lock is dropped before each network download.
|
||||||
fn pin_subtree(&self, ino: u64) -> Result<usize, String> {
|
fn pin_subtree(&self, ino: u64) -> Result<usize, String> {
|
||||||
let mut files: Vec<(NodeUid, i64, u64)> = Vec::new();
|
let mut files: Vec<(NodeUid, String, i64, u64)> = Vec::new();
|
||||||
let mut stack = vec![ino];
|
let mut stack = vec![ino];
|
||||||
while let Some(dir) = stack.pop() {
|
while let Some(dir) = stack.pop() {
|
||||||
self.ensure_children(dir)
|
self.ensure_children(dir)
|
||||||
@@ -777,6 +838,7 @@ impl Core {
|
|||||||
} else {
|
} else {
|
||||||
files.push((
|
files.push((
|
||||||
e.uid.clone(),
|
e.uid.clone(),
|
||||||
|
e.node.name.clone(),
|
||||||
e.node.modification_time,
|
e.node.modification_time,
|
||||||
node_size(&e.node),
|
node_size(&e.node),
|
||||||
));
|
));
|
||||||
@@ -786,12 +848,12 @@ impl Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for (uid, mtime, size) in files {
|
for (uid, name, mtime, size) in files {
|
||||||
if self.cache.is_cached(&uid, mtime, size) {
|
if self.cache.is_cached(&uid, mtime, size) {
|
||||||
count += 1;
|
count += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match self.rt.block_on(self.client.download_file(&uid)) {
|
match self.download_file_tracked(&uid, &name, size) {
|
||||||
Ok(bytes) => {
|
Ok(bytes) => {
|
||||||
if self.cache.store(&uid, mtime, size, &bytes).is_ok() {
|
if self.cache.store(&uid, mtime, size, &bytes).is_ok() {
|
||||||
count += 1;
|
count += 1;
|
||||||
@@ -1050,8 +1112,7 @@ impl Core {
|
|||||||
return Ok(p);
|
return Ok(p);
|
||||||
}
|
}
|
||||||
let bytes = self
|
let bytes = self
|
||||||
.rt
|
.download_photo_tracked(&photos, uid, &node.name, size)
|
||||||
.block_on(photos.download_photo(uid))
|
|
||||||
.map_err(|e| format!("download photo: {e}"))?;
|
.map_err(|e| format!("download photo: {e}"))?;
|
||||||
self.cache
|
self.cache
|
||||||
.store(uid, mtime, size, &bytes)
|
.store(uid, mtime, size, &bytes)
|
||||||
@@ -1067,20 +1128,19 @@ impl Core {
|
|||||||
let (ino, uid) = self
|
let (ino, uid) = self
|
||||||
.resolve_path(rel)
|
.resolve_path(rel)
|
||||||
.map_err(|e| format!("resolve path: {e:?}"))?;
|
.map_err(|e| format!("resolve path: {e:?}"))?;
|
||||||
let (mtime, size) = {
|
let (name, mtime, size) = {
|
||||||
let st = self.state.lock().unwrap();
|
let st = self.state.lock().unwrap();
|
||||||
let e = st.entries.get(&ino).ok_or("node vanished")?;
|
let e = st.entries.get(&ino).ok_or("node vanished")?;
|
||||||
if !e.node.is_file() {
|
if !e.node.is_file() {
|
||||||
return Err("not a regular file".into());
|
return Err("not a regular file".into());
|
||||||
}
|
}
|
||||||
(e.node.modification_time, node_size(&e.node))
|
(e.node.name.clone(), e.node.modification_time, node_size(&e.node))
|
||||||
};
|
};
|
||||||
if let Some(p) = self.cache.cached_content_path(&uid, mtime, size) {
|
if let Some(p) = self.cache.cached_content_path(&uid, mtime, size) {
|
||||||
return Ok(p);
|
return Ok(p);
|
||||||
}
|
}
|
||||||
let bytes = self
|
let bytes = self
|
||||||
.rt
|
.download_file_tracked(&uid, &name, size)
|
||||||
.block_on(self.client.download_file(&uid))
|
|
||||||
.map_err(|e| format!("download: {e}"))?;
|
.map_err(|e| format!("download: {e}"))?;
|
||||||
self.cache
|
self.cache
|
||||||
.store(&uid, mtime, size, &bytes)
|
.store(&uid, mtime, size, &bytes)
|
||||||
@@ -1206,13 +1266,24 @@ impl Core {
|
|||||||
.map_err(|e| format!("resolve path: {e:?}"))?;
|
.map_err(|e| format!("resolve path: {e:?}"))?;
|
||||||
self.ensure_children(pino)
|
self.ensure_children(pino)
|
||||||
.map_err(|e| format!("enumerate: {e:?}"))?;
|
.map_err(|e| format!("enumerate: {e:?}"))?;
|
||||||
|
let guard = self
|
||||||
|
.transfers
|
||||||
|
.begin(name, "", TransferDirection::Upload, bytes.len() as u64);
|
||||||
|
let reader = CountingReader::new(std::io::Cursor::new(bytes), &guard);
|
||||||
let new_uid = self
|
let new_uid = self
|
||||||
.rt
|
.rt
|
||||||
.block_on(
|
.block_on(self.client.upload_file_from(
|
||||||
self.client
|
&parent_uid,
|
||||||
.upload_file(&parent_uid, name, media_type_for(name), bytes),
|
name,
|
||||||
)
|
media_type_for(name),
|
||||||
|
reader,
|
||||||
|
bytes.len() as i64,
|
||||||
|
Vec::new(),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
))
|
||||||
.map_err(|e| format!("upload: {e}"))?;
|
.map_err(|e| format!("upload: {e}"))?;
|
||||||
|
drop(guard);
|
||||||
let node = self
|
let node = self
|
||||||
.fetch_node(&new_uid)
|
.fetch_node(&new_uid)
|
||||||
.map_err(|e| format!("fetch node: {e:?}"))?;
|
.map_err(|e| format!("fetch node: {e:?}"))?;
|
||||||
@@ -2322,10 +2393,19 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
|
|||||||
capture_time,
|
capture_time,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
match core
|
let guard =
|
||||||
.rt
|
core.transfers
|
||||||
.block_on(photos.upload_photo(&name, &media_type, &bytes, metadata))
|
.begin(name.clone(), "", TransferDirection::Upload, bytes.len() as u64);
|
||||||
{
|
let reader = CountingReader::new(std::io::Cursor::new(&bytes), &guard);
|
||||||
|
match core.rt.block_on(photos.upload_photo_from(
|
||||||
|
&name,
|
||||||
|
&media_type,
|
||||||
|
reader,
|
||||||
|
bytes.len() as i64,
|
||||||
|
Vec::new(),
|
||||||
|
metadata,
|
||||||
|
false,
|
||||||
|
)) {
|
||||||
Ok(uid) => {
|
Ok(uid) => {
|
||||||
*core.timeline.lock().unwrap() = None;
|
*core.timeline.lock().unwrap() = None;
|
||||||
CtlResponse::Ok {
|
CtlResponse::Ok {
|
||||||
@@ -2413,6 +2493,9 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(CtlRequest::GetQueueStatus) => CtlResponse::Transfers {
|
||||||
|
items: core.transfers.snapshot(),
|
||||||
|
},
|
||||||
Ok(CtlRequest::SetCacheBudget { bytes }) => {
|
Ok(CtlRequest::SetCacheBudget { bytes }) => {
|
||||||
core.cache.set_budget(bytes);
|
core.cache.set_budget(bytes);
|
||||||
// Persist so the next mount keeps the new cap. Best-effort: a config
|
// Persist so the next mount keeps the new cap. Best-effort: a config
|
||||||
@@ -2539,6 +2622,7 @@ pub fn mount(
|
|||||||
cache: Arc::new(cache),
|
cache: Arc::new(cache),
|
||||||
db,
|
db,
|
||||||
timeline: Arc::new(Mutex::new(None)),
|
timeline: Arc::new(Mutex::new(None)),
|
||||||
|
transfers: TransferRegistry::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
//! In-memory registry of in-flight transfers, behind [`Request::GetQueueStatus`].
|
||||||
|
//!
|
||||||
|
//! The daemon downloads and uploads whole files through the SDK's streaming
|
||||||
|
//! [`Read`]/[`Write`] variants. Wrapping that reader/writer in a
|
||||||
|
//! [`CountingReader`]/[`CountingWriter`] lets each block of bytes tick a
|
||||||
|
//! per-transfer counter without the SDK knowing anything about progress. A
|
||||||
|
//! [`TransferGuard`] registers a transfer on creation and deregisters it on
|
||||||
|
//! drop, so the registry always reflects exactly what is in flight — even if a
|
||||||
|
//! transfer fails partway and unwinds.
|
||||||
|
//!
|
||||||
|
//! [`Request::GetQueueStatus`]: pdfs_core::control::Request::GetQueueStatus
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use pdfs_core::control::{TransferDirection, TransferItem};
|
||||||
|
|
||||||
|
/// One registered transfer. `done` is bumped from the I/O wrapper without the
|
||||||
|
/// registry lock; the rest is immutable for the transfer's lifetime.
|
||||||
|
struct Entry {
|
||||||
|
name: String,
|
||||||
|
uid: String,
|
||||||
|
direction: TransferDirection,
|
||||||
|
total: u64,
|
||||||
|
done: AtomicU64,
|
||||||
|
started: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The set of transfers currently in flight. Cloned `Arc`-style across the FUSE
|
||||||
|
/// session and the control-socket task (both share one registry via [`Core`]).
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TransferRegistry {
|
||||||
|
inner: Mutex<HashMap<u64, Arc<Entry>>>,
|
||||||
|
next: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransferRegistry {
|
||||||
|
pub fn new() -> Arc<Self> {
|
||||||
|
Arc::new(Self::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a new transfer of `total` bytes (`0` = unknown), returning a
|
||||||
|
/// guard that tracks progress and deregisters the transfer when dropped.
|
||||||
|
pub fn begin(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
name: impl Into<String>,
|
||||||
|
uid: impl Into<String>,
|
||||||
|
direction: TransferDirection,
|
||||||
|
total: u64,
|
||||||
|
) -> TransferGuard {
|
||||||
|
let id = self.next.fetch_add(1, Ordering::Relaxed);
|
||||||
|
let entry = Arc::new(Entry {
|
||||||
|
name: name.into(),
|
||||||
|
uid: uid.into(),
|
||||||
|
direction,
|
||||||
|
total,
|
||||||
|
done: AtomicU64::new(0),
|
||||||
|
started: Instant::now(),
|
||||||
|
});
|
||||||
|
self.inner.lock().unwrap().insert(id, entry.clone());
|
||||||
|
TransferGuard {
|
||||||
|
reg: self.clone(),
|
||||||
|
id,
|
||||||
|
entry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot every in-flight transfer for [`Response::Transfers`]. Speed is the
|
||||||
|
/// running average since the transfer began — simple, and stable enough for a
|
||||||
|
/// progress widget without per-tick sampling state.
|
||||||
|
///
|
||||||
|
/// [`Response::Transfers`]: pdfs_core::control::Response::Transfers
|
||||||
|
pub fn snapshot(&self) -> Vec<TransferItem> {
|
||||||
|
let map = self.inner.lock().unwrap();
|
||||||
|
map.values()
|
||||||
|
.map(|e| {
|
||||||
|
let done = e.done.load(Ordering::Relaxed);
|
||||||
|
let secs = e.started.elapsed().as_secs_f64();
|
||||||
|
let speed = if secs > 0.0 {
|
||||||
|
(done as f64 / secs) as u64
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
TransferItem {
|
||||||
|
uid: e.uid.clone(),
|
||||||
|
name: e.name.clone(),
|
||||||
|
direction: e.direction,
|
||||||
|
bytes_completed: done,
|
||||||
|
bytes_total: e.total,
|
||||||
|
speed_bytes_sec: speed,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lifetime handle for a registered transfer: tick progress with [`add`], and
|
||||||
|
/// the transfer leaves the registry when this drops.
|
||||||
|
///
|
||||||
|
/// [`add`]: TransferGuard::add
|
||||||
|
pub struct TransferGuard {
|
||||||
|
reg: Arc<TransferRegistry>,
|
||||||
|
id: u64,
|
||||||
|
entry: Arc<Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransferGuard {
|
||||||
|
/// Record `n` more bytes moved.
|
||||||
|
pub fn add(&self, n: u64) {
|
||||||
|
self.entry.done.fetch_add(n, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TransferGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.reg.inner.lock().unwrap().remove(&self.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Write`] that tallies bytes written to a [`TransferGuard`] (download path).
|
||||||
|
pub struct CountingWriter<'a, W> {
|
||||||
|
inner: W,
|
||||||
|
guard: &'a TransferGuard,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, W: Write> CountingWriter<'a, W> {
|
||||||
|
pub fn new(inner: W, guard: &'a TransferGuard) -> Self {
|
||||||
|
Self { inner, guard }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_inner(self) -> W {
|
||||||
|
self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> Write for CountingWriter<'_, W> {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
let n = self.inner.write(buf)?;
|
||||||
|
self.guard.add(n as u64);
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
self.inner.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Read`] that tallies bytes read through a [`TransferGuard`] (upload path).
|
||||||
|
pub struct CountingReader<'a, R> {
|
||||||
|
inner: R,
|
||||||
|
guard: &'a TransferGuard,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, R: Read> CountingReader<'a, R> {
|
||||||
|
pub fn new(inner: R, guard: &'a TransferGuard) -> Self {
|
||||||
|
Self { inner, guard }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Read> Read for CountingReader<'_, R> {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
|
let n = self.inner.read(buf)?;
|
||||||
|
self.guard.add(n as u64);
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user