Skip to content

Commit f530355

Browse files
committed
SMB: Add real-time progress for file transfers
- Add `export_to_local_with_progress` and `import_from_local_with_progress` to Volume trait (default delegates to non-progress version) - SmbVolume overrides both using smb2's `read_file_with_progress` / `write_file_with_progress` with pipelined I/O - `copy_single_path` and `copy_volumes_with_progress` wired to pass progress callback through to Volume methods - Progress callback uses `AtomicU64` + `Cell<Instant>` for throttled Tauri event emission (satisfies `Fn` constraint) - Cancellation flows via `ControlFlow::Break` from callback → smb2 stops transfer - Update smb2 dependency to latest (adds `read_file_with_progress`)
1 parent a7d401a commit f530355

7 files changed

Lines changed: 360 additions & 59 deletions

File tree

Cargo.lock

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

apps/desktop/src-tauri/src/file_system/volume/CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Optional methods default to `Err(VolumeError::NotSupported)` or `false`, so new
4343
- `smb_connection_state()` — returns `Some(SmbConnectionState)` for SMB volumes (green/yellow indicator in volume picker). Default `None`. Only `SmbVolume` implements it.
4444
- `on_unmount()` — lifecycle hook called before unregistration. `SmbVolume` uses it to disconnect its smb2 session. Default is no-op.
4545
- `scanner()` / `watcher()` — drive indexing hooks; `None` by default.
46+
- `export_to_local_with_progress()` / `import_from_local_with_progress()` — per-file progress callbacks during copy. Default delegates to the non-progress version. `SmbVolume` overrides both, using smb2's `read_file_with_progress`/`write_file_with_progress`. The callback takes `(bytes_done, bytes_total)` for the current file and returns `ControlFlow::Break(())` to cancel. `MtpVolume` and `LocalPosixVolume` use the default (no intra-file progress).
4647

4748
## Path handling gotchas
4849

@@ -141,6 +142,9 @@ provides a manual upgrade path.
141142
**Gotcha**: Watcher filenames are NFC (from server) but macOS mount paths are NFD
142143
**Why**: SMB servers return NFC-normalized filenames. macOS filesystem paths use NFD. The watcher NFD-normalizes filenames before constructing display paths used for cache lookups.
143144

145+
**Decision**: Progress callbacks use `&dyn Fn(u64, u64) -> ControlFlow<()>`, not `FnMut`
146+
**Why**: The Volume trait is object-safe (`dyn Volume`), so callbacks must be `Fn` (not `FnMut`). Callers use `AtomicU64` for byte counters and `Cell<Instant>` for timestamps to mutate state inside a `Fn` closure. This avoids needing `RefCell` or `Mutex` in the hot path.
147+
144148
## Testing
145149

146150
- `in_memory_test.rs` — unit tests for `InMemoryVolume` (CRUD, sorting, concurrency, stress 50k entries)

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,21 @@ pub trait Volume: Send + Sync {
397397
Err(VolumeError::NotSupported)
398398
}
399399

400+
/// Exports a file from this volume to a local path, reporting progress.
401+
///
402+
/// `on_progress(bytes_done, bytes_total)` is called periodically during the transfer.
403+
/// Return `ControlFlow::Break(())` from the callback to cancel the transfer.
404+
/// Default implementation delegates to `export_to_local` (no per-file progress).
405+
fn export_to_local_with_progress(
406+
&self,
407+
source: &Path,
408+
local_dest: &Path,
409+
on_progress: &dyn Fn(u64, u64) -> std::ops::ControlFlow<()>,
410+
) -> Result<u64, VolumeError> {
411+
let _ = on_progress;
412+
self.export_to_local(source, local_dest)
413+
}
414+
400415
/// Imports/uploads a file or directory from a local path to this volume.
401416
/// For local volumes, this is a file copy. For MTP, this uploads.
402417
/// Returns bytes transferred.
@@ -405,6 +420,21 @@ pub trait Volume: Send + Sync {
405420
Err(VolumeError::NotSupported)
406421
}
407422

423+
/// Imports a file from a local path to this volume, reporting progress.
424+
///
425+
/// `on_progress(bytes_done, bytes_total)` is called periodically during the transfer.
426+
/// Return `ControlFlow::Break(())` from the callback to cancel the transfer.
427+
/// Default implementation delegates to `import_from_local` (no per-file progress).
428+
fn import_from_local_with_progress(
429+
&self,
430+
local_source: &Path,
431+
dest: &Path,
432+
on_progress: &dyn Fn(u64, u64) -> std::ops::ControlFlow<()>,
433+
) -> Result<u64, VolumeError> {
434+
let _ = on_progress;
435+
self.import_from_local(local_source, dest)
436+
}
437+
408438
/// Checks destination for conflicts with source items.
409439
/// Returns list of files that already exist at destination.
410440
fn scan_for_conflicts(

0 commit comments

Comments
 (0)