Skip to content

Commit ba5409e

Browse files
committed
Add chunked copy feature
- Cancellable on network drives - Even pausable! - Keeps metadata
1 parent b119c83 commit ba5409e

15 files changed

Lines changed: 1227 additions & 30 deletions

File tree

apps/desktop/src-tauri/Cargo.lock

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ tauri-plugin-updater = "2"
6565
tauri-plugin-process = "2"
6666
memchr = "2"
6767
walkdir = "2"
68+
# For chunked copy metadata preservation (network filesystems)
69+
xattr = "1"
70+
filetime = "0.2"
71+
exacl = "0.12"
6872

6973
[target.'cfg(unix)'.dependencies]
7074
libc = "0.2"

apps/desktop/src-tauri/src/file_system/macos_copy.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ extern "C" fn copy_progress_callback(
116116
_dst: *const i8,
117117
ctx: *mut c_void,
118118
) -> c_int {
119+
// Log callback invocation (helps diagnose if callback is being called at all)
120+
log::trace!(
121+
"copyfile callback: what={}, stage={}, ctx_null={}",
122+
what,
123+
stage,
124+
ctx.is_null()
125+
);
126+
119127
if ctx.is_null() {
120128
return COPYFILE_CONTINUE;
121129
}
@@ -125,6 +133,7 @@ extern "C" fn copy_progress_callback(
125133

126134
// Check cancellation
127135
if context.cancelled.load(Ordering::Relaxed) {
136+
log::info!("copyfile callback: cancellation detected, returning COPYFILE_QUIT");
128137
return COPYFILE_QUIT;
129138
}
130139

@@ -296,7 +305,14 @@ pub fn copy_file_native(
296305
}
297306

298307
// Perform the copy
308+
log::info!(
309+
"copyfile: starting copy from {} to {} (flags={:#x})",
310+
source.display(),
311+
destination.display(),
312+
flags
313+
);
299314
let result = unsafe { copyfile(src_cstring.as_ptr(), dst_cstring.as_ptr(), state, flags) };
315+
log::info!("copyfile: completed with result={}", result);
300316

301317
// Free state
302318
unsafe {

apps/desktop/src-tauri/src/file_system/operations.rs

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use std::fs;
88
use std::os::unix::fs::{MetadataExt, PermissionsExt};
99
use std::path::{Path, PathBuf};
1010
use std::sync::atomic::{AtomicBool, Ordering};
11+
use std::sync::mpsc;
1112
use std::sync::{Arc, LazyLock, RwLock};
13+
use std::time::Duration;
1214
use uuid::Uuid;
1315
use uzers::{get_group_by_gid, get_user_by_uid};
1416

@@ -165,6 +167,10 @@ pub struct StreamingListingState {
165167
pub cancelled: AtomicBool,
166168
}
167169

170+
/// Interval for checking cancellation while waiting for directory listing results.
171+
/// This ensures we can respond to ESC within ~100ms even if I/O is blocked.
172+
const CANCELLATION_POLL_INTERVAL: Duration = Duration::from_millis(100);
173+
168174
/// Cache for streaming state (separate from completed listings cache)
169175
pub(crate) static STREAMING_STATE: LazyLock<RwLock<HashMap<String, Arc<StreamingListingState>>>> =
170176
LazyLock::new(|| RwLock::new(HashMap::new()));
@@ -361,9 +367,9 @@ pub fn list_directory(path: &Path) -> Result<Vec<FileEntry>, std::io::Error> {
361367
let overall_start = std::time::Instant::now();
362368
let mut entries = Vec::new();
363369

364-
let mut metadata_time = std::time::Duration::ZERO;
365-
let mut owner_lookup_time = std::time::Duration::ZERO;
366-
let mut entry_creation_time = std::time::Duration::ZERO;
370+
let mut metadata_time = Duration::ZERO;
371+
let mut owner_lookup_time = Duration::ZERO;
372+
let mut entry_creation_time = Duration::ZERO;
367373

368374
let read_start = std::time::Instant::now();
369375
let dir_entries: Vec<_> = fs::read_dir(path)?.collect();
@@ -1022,8 +1028,8 @@ pub fn list_directory_core(path: &Path) -> Result<Vec<FileEntry>, std::io::Error
10221028
benchmark::log_event_value("readdir END, count", dir_entries.len());
10231029

10241030
benchmark::log_event("stat_loop START");
1025-
let mut metadata_time = std::time::Duration::ZERO;
1026-
let mut owner_lookup_time = std::time::Duration::ZERO;
1031+
let mut metadata_time = Duration::ZERO;
1032+
let mut owner_lookup_time = Duration::ZERO;
10271033

10281034
for entry in dir_entries {
10291035
let entry = entry?;
@@ -1426,10 +1432,40 @@ fn read_directory_with_progress(
14261432
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, format!("Volume not found: {}", volume_id)))?;
14271433

14281434
// Read directory entries via Volume abstraction
1435+
// Use polling-based cancellation to remain responsive even when filesystem I/O blocks
1436+
// (e.g., on slow/stuck network drives like SMB mounts)
14291437
let read_start = std::time::Instant::now();
1430-
let mut entries = volume
1431-
.list_directory(path)
1432-
.map_err(|e| std::io::Error::other(e.to_string()))?;
1438+
let path_for_thread = path.to_path_buf();
1439+
let (tx, rx) = mpsc::channel();
1440+
1441+
std::thread::spawn(move || {
1442+
let result = volume.list_directory(&path_for_thread);
1443+
let _ = tx.send(result);
1444+
});
1445+
1446+
// Poll for results, checking cancellation between polls
1447+
let entries_result = loop {
1448+
if state.cancelled.load(Ordering::Relaxed) {
1449+
benchmark::log_event("read_directory_with_progress CANCELLED (during read_dir polling)");
1450+
let _ = app.emit(
1451+
"listing-cancelled",
1452+
ListingCancelledEvent {
1453+
listing_id: listing_id.to_string(),
1454+
},
1455+
);
1456+
return Ok(());
1457+
}
1458+
1459+
match rx.recv_timeout(CANCELLATION_POLL_INTERVAL) {
1460+
Ok(result) => break result,
1461+
Err(mpsc::RecvTimeoutError::Timeout) => continue,
1462+
Err(mpsc::RecvTimeoutError::Disconnected) => {
1463+
return Err(std::io::Error::other("Directory listing thread terminated unexpectedly"));
1464+
}
1465+
}
1466+
};
1467+
1468+
let mut entries = entries_result.map_err(|e| std::io::Error::other(e.to_string()))?;
14331469
let read_dir_time = read_start.elapsed();
14341470
benchmark::log_event_value("read_dir COMPLETE, entries", entries.len());
14351471

apps/desktop/src-tauri/src/file_system/volume/local_posix.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ impl Volume for LocalPosixVolume {
103103
true
104104
}
105105

106+
fn local_path(&self) -> Option<PathBuf> {
107+
Some(self.root.clone())
108+
}
109+
106110
fn create_file(&self, path: &Path, content: &[u8]) -> Result<(), VolumeError> {
107111
let abs_path = self.resolve(path);
108112
std::fs::write(&abs_path, content)?;

apps/desktop/src-tauri/src/file_system/volume/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,16 @@ pub trait Volume: Send + Sync {
239239
}
240240

241241
// ========================================
242-
// Streaming: Optional, default not supported
242+
// Capability hints for copy optimization
243243
// ========================================
244244

245+
/// Returns the local filesystem path if this volume is backed by one.
246+
/// Used to optimize local-to-local copies using native OS APIs (e.g., copyfile on macOS).
247+
/// Returns None for non-local volumes (MTP, S3, FTP, etc.).
248+
fn local_path(&self) -> Option<std::path::PathBuf> {
249+
None
250+
}
251+
245252
/// Returns true if this volume supports streaming read/write operations.
246253
fn supports_streaming(&self) -> bool {
247254
false

0 commit comments

Comments
 (0)