Skip to content

Commit 2939bfe

Browse files
committed
Refactor: Split 8 more large files into sub-800-line modules
- `volume_copy.rs` (1574) → extract move ops to `volume_move.rs` (476 lines) - `scan.rs` (949) → extract preview subsystem to `scan_preview.rs` (284 lines) - `smb.rs` (2194) → extract watcher to `smb_watcher.rs` (401 lines) - `stress_tests.rs` (1390) → split into concurrency (618) + lifecycle (527) + helpers (261) - `mcp/tests.rs` (1294) → directory module with 7 focused test files - `search/ai/mappings.rs` (1032) → directory module with 4 domain-specific mapping files - `integration.test.ts` (1210) → 3 test files by component + shared utils - `debug/+page.svelte` (1508) → 4 panel components + orchestrator (574) Pure mechanical splits, no logic changes. All 1282 Rust tests, 158 pane tests, and svelte-check pass.
1 parent 4514a83 commit 2939bfe

43 files changed

Lines changed: 7305 additions & 7065 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ Every file system operation (listing, copy, rename, delete, indexing, watching)
1515
| `manager.rs` | `VolumeManager` — thread-safe `RwLock<HashMap>` registry; supports a default volume |
1616
| `local_posix.rs` | `LocalPosixVolume` — real filesystem; delegates listing to `file_system::listing`, indexing to `indexing::scanner`, watching to `indexing::watcher` (FSEvents), copy scanning via `walkdir`. Uses `libc::statvfs` FFI for space info. |
1717
| `mtp.rs` | `MtpVolume` — MTP device storage; synchronous `Volume` trait bridged to async MTP calls via `tokio::runtime::Handle::block_on`. Gated with `#[cfg(any(target_os = "macos", target_os = "linux"))]`. |
18-
| `smb.rs` | `SmbVolume` — SMB share storage; synchronous `Volume` trait bridged to async smb2 calls via `Handle::block_on`. Uses `Mutex<Option<(SmbClient, Tree)>>` + `AtomicU8` connection state. Gated with `#[cfg(any(target_os = "macos", target_os = "linux"))]`. |
18+
| `smb.rs` | `SmbVolume` — SMB share storage; synchronous `Volume` trait bridged to async smb2 calls via `Handle::block_on`. Uses `Mutex<Option<(SmbClient, Tree)>>` + `AtomicU8` connection state. Also contains `connect_smb_volume()`. Gated with `#[cfg(any(target_os = "macos", target_os = "linux"))]`. |
19+
| `smb_watcher.rs` | Background SMB change watcher (`run_smb_watcher`). Owns a dedicated smb2 connection for `CHANGE_NOTIFY`, debounces events, feeds `notify_directory_changed`. Spawned by `connect_smb_volume()`. |
1920
| `in_memory.rs` | `InMemoryVolume``RwLock<HashMap>` store for tests; also used for stress tests (`with_file_count`) |
2021

2122
## Architecture

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,8 @@ pub(crate) mod manager;
555555
mod mtp;
556556
#[cfg(any(target_os = "macos", target_os = "linux"))]
557557
pub(crate) mod smb;
558+
#[cfg(any(target_os = "macos", target_os = "linux"))]
559+
mod smb_watcher;
558560

559561
pub use in_memory::InMemoryVolume;
560562
pub use local_posix::LocalPosixVolume;

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

Lines changed: 2 additions & 394 deletions
Large diffs are not rendered by default.

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

Lines changed: 401 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ network mounts, cross-filesystem moves, and name/path length limits.
1616
| `types.rs` | All serializable types: events, config, errors, results. `WriteOperationConfig`, `ConflictResolution`, `WriteOperationError`, `DryRunResult`, scan preview events. |
1717
| `state.rs` | Two `LazyLock<RwLock<HashMap>>` caches (`WRITE_OPERATION_STATE`, `OPERATION_STATUS_CACHE`). `WriteOperationState`, `CopyTransaction`, `ScanResult`, `FileInfo`. |
1818
| `helpers.rs` | Validation (`validate_sources`, `validate_destination_writable` via `libc::access`, `validate_disk_space` via `statvfs`). Conflict resolution (condvar wait for Stop mode). `safe_overwrite_file`/`safe_overwrite_dir` (temp+rename). `find_unique_name`. `run_cancellable`. `is_same_filesystem` (device IDs). Background cleanup helpers: `remove_file_in_background`, `remove_dir_all_in_background`. |
19-
| `scan.rs` | `scan_sources` (recursive walk, emits progress), `dry_run_scan`, scan preview subsystem (`start_scan_preview`, `cancel_scan_preview`). |
19+
| `scan.rs` | `scan_sources` (recursive walk, emits progress), `dry_run_scan`, shared `walk_dir_recursive` walker. |
20+
| `scan_preview.rs` | Scan preview subsystem for Copy dialog live stats: `start_scan_preview`, `cancel_scan_preview`, `is_scan_preview_complete`. Background scans (local and volume-based) with result caching. |
2021
| `copy.rs` | `copy_files_with_progress`: scan → disk space check → per-file copy via `copy_single_item`. `CopyTransaction` for rollback. |
2122
| `move_op.rs` | Same-fs: `fs::rename`. Cross-fs: copy to `.cmdr-staging-<uuid>`, atomic rename, delete sources. |
2223
| `delete.rs` | Scan, delete files first, then directories in reverse/deepest-first order. Not rollbackable. Also contains `delete_volume_files_with_progress` for non-local volumes (MTP): scans via `volume.list_directory()`, deletes via `volume.delete()` per item. |
@@ -25,7 +26,9 @@ network mounts, cross-filesystem moves, and name/path length limits.
2526
| `macos_copy.rs` | FFI to macOS `copyfile(3)`. Preserves xattrs, ACLs, resource forks, Finder metadata. Supports APFS `clonefile`. |
2627
| `linux_copy.rs` | Linux `copy_file_range(2)` with reflink support on btrfs/XFS. 4 MB chunks, cancellation between iterations. |
2728
| `chunked_copy.rs` | 1 MB chunked read/write — the default copy method for all non-APFS-clonefile copies on macOS and network copies on Linux. Checks cancellation between chunks. Copies xattrs, ACLs, timestamps. |
28-
| `volume_copy.rs`, `volume_conflict.rs`, `volume_strategy.rs` | Volume-to-volume copy/move (Local↔MTP abstraction). Handles conflict detection, resolution (Stop/Skip/Overwrite/Rename), progress, rollback (delete all copied files in reverse with progress), and partial-file cleanup on cancel. Wired into Tauri commands `copy_between_volumes` and `move_between_volumes`. |
29+
| `volume_copy.rs` | Volume-to-volume copy (Local↔MTP abstraction): `copy_between_volumes`, `scan_for_volume_copy`. Handles conflict detection, resolution, progress, rollback (delete all copied files in reverse with progress), and partial-file cleanup on cancel. Shared `map_volume_error` helper. |
30+
| `volume_move.rs` | Volume-to-volume move: `move_between_volumes`, `move_within_same_volume`. Same-volume uses `Volume::rename`; cross-volume does copy+delete. |
31+
| `volume_conflict.rs`, `volume_strategy.rs` | Conflict resolution (Stop/Skip/Overwrite/Rename) and copy strategy selection for volume operations. |
2932
| `tests.rs` | Unit tests. |
3033
| `copy_integration_test.rs` | Copy operation integration tests (permissions, symlinks, xattrs, edge cases). |
3134
| `delete_integration_test.rs` | Delete operation integration tests. |

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ mod linux_copy;
2727
pub(crate) mod macos_copy;
2828
mod move_op;
2929
mod scan;
30+
mod scan_preview;
3031
mod state;
3132
pub(crate) mod trash;
3233
mod types;
3334
mod volume_conflict;
3435
mod volume_copy;
36+
mod volume_move;
3537
mod volume_strategy;
3638

3739
use std::path::PathBuf;
@@ -54,7 +56,7 @@ use state::{WRITE_OPERATION_STATE, register_operation_status, unregister_operati
5456
use trash::trash_files_with_progress;
5557

5658
// Re-export public types
57-
pub use scan::{cancel_scan_preview, is_scan_preview_complete, start_scan_preview};
59+
pub use scan_preview::{cancel_scan_preview, is_scan_preview_complete, start_scan_preview};
5860
pub use state::{
5961
cancel_all_write_operations, cancel_write_operation, get_operation_status, list_active_operations,
6062
resolve_write_conflict,
@@ -81,7 +83,8 @@ pub(crate) use state::{CopyTransaction, OperationIntent, WriteOperationState, is
8183

8284
// Re-export volume copy types and functions
8385
pub use types::{VolumeCopyConfig, VolumeCopyScanResult};
84-
pub use volume_copy::{copy_between_volumes, move_between_volumes, scan_for_volume_copy};
86+
pub use volume_copy::{copy_between_volumes, scan_for_volume_copy};
87+
pub use volume_move::move_between_volumes;
8588

8689
// ============================================================================
8790
// Public API functions

apps/desktop/src-tauri/src/file_system/write_operations/scan.rs

Lines changed: 12 additions & 287 deletions
Original file line numberDiff line numberDiff line change
@@ -1,304 +1,29 @@
11
//! Scanning functionality for write operations.
22
//!
3-
//! Contains file scanning, preview scanning, and dry-run operations.
3+
//! Contains file scanning, dry-run operations, and the shared directory walker.
44
55
use std::collections::HashSet;
66
use std::fs;
77
use std::path::{Path, PathBuf};
88
use std::sync::Arc;
9-
use std::sync::atomic::{AtomicBool, Ordering};
109
use std::time::{Duration, Instant};
1110

12-
use uuid::Uuid;
13-
1411
use super::helpers::{calculate_dest_path, create_conflict_info, is_symlink_loop, run_cancellable, sample_conflicts};
15-
use super::state::{
16-
CachedScanResult, FileInfo, SCAN_PREVIEW_RESULTS, SCAN_PREVIEW_STATE, ScanPreviewState, ScanResult,
17-
WriteOperationState, update_operation_status,
18-
};
12+
use super::state::{FileInfo, SCAN_PREVIEW_RESULTS, ScanResult, WriteOperationState, update_operation_status};
1913
use super::types::{
20-
ConflictInfo, IoResultExt, ScanPreviewCancelledEvent, ScanPreviewCompleteEvent, ScanPreviewErrorEvent,
21-
ScanPreviewProgressEvent, ScanPreviewStartResult, ScanProgressEvent, WriteOperationError, WriteOperationPhase,
22-
WriteOperationType, WriteProgressEvent,
14+
ConflictInfo, IoResultExt, ScanProgressEvent, WriteOperationError, WriteOperationPhase, WriteOperationType,
15+
WriteProgressEvent,
2316
};
2417
use crate::file_system::listing::{SortColumn, SortOrder};
25-
use crate::file_system::volume::Volume;
26-
27-
// ============================================================================
28-
// Scan preview (for Copy dialog live stats)
29-
// ============================================================================
30-
31-
/// Starts a scan preview for the Copy dialog.
32-
/// Returns a preview_id that can be used to cancel or to pass to copy_files.
33-
///
34-
/// When `source_volume` is provided, uses `Volume::scan_for_copy()` instead of `std::fs`,
35-
/// enabling MTP and other non-local volumes to produce scan previews.
36-
pub fn start_scan_preview(
37-
app: tauri::AppHandle,
38-
sources: Vec<PathBuf>,
39-
source_volume: Option<Arc<dyn Volume>>,
40-
sort_column: SortColumn,
41-
sort_order: SortOrder,
42-
progress_interval_ms: u64,
43-
runtime_handle: Option<tokio::runtime::Handle>,
44-
) -> ScanPreviewStartResult {
45-
let preview_id = Uuid::new_v4().to_string();
46-
let preview_id_clone = preview_id.clone();
47-
48-
let state = Arc::new(ScanPreviewState {
49-
cancelled: AtomicBool::new(false),
50-
progress_interval: Duration::from_millis(progress_interval_ms),
51-
});
52-
53-
// Register state
54-
if let Ok(mut cache) = SCAN_PREVIEW_STATE.write() {
55-
cache.insert(preview_id.clone(), Arc::clone(&state));
56-
}
57-
58-
// Spawn background task.
59-
// Volume scans need a Tokio runtime context (MtpVolume uses Handle::block_on),
60-
// so we capture the runtime handle and enter it on the spawned thread.
61-
// Local scans use std::thread directly (no runtime needed).
62-
if let Some(volume) = source_volume {
63-
let handle = runtime_handle.expect("runtime_handle required for volume scan preview");
64-
std::thread::spawn(move || {
65-
let _guard = handle.enter();
66-
run_volume_scan_preview(app, preview_id_clone, sources, volume, state);
67-
});
68-
} else {
69-
std::thread::spawn(move || {
70-
run_scan_preview(app, preview_id_clone, sources, sort_column, sort_order, state);
71-
});
72-
}
73-
74-
ScanPreviewStartResult { preview_id }
75-
}
76-
77-
/// Returns true if scan preview results are cached (scan completed successfully).
78-
pub fn is_scan_preview_complete(preview_id: &str) -> bool {
79-
SCAN_PREVIEW_RESULTS
80-
.read()
81-
.is_ok_and(|cache| cache.contains_key(preview_id))
82-
}
83-
84-
/// Cancels a running scan preview.
85-
pub fn cancel_scan_preview(preview_id: &str) {
86-
if let Ok(cache) = SCAN_PREVIEW_STATE.read()
87-
&& let Some(state) = cache.get(preview_id)
88-
{
89-
state.cancelled.store(true, Ordering::Relaxed);
90-
}
91-
}
92-
93-
/// Internal function that runs the scan preview in a background thread.
94-
fn run_scan_preview(
95-
app: tauri::AppHandle,
96-
preview_id: String,
97-
sources: Vec<PathBuf>,
98-
sort_column: SortColumn,
99-
sort_order: SortOrder,
100-
state: Arc<ScanPreviewState>,
101-
) {
102-
use tauri::Emitter;
103-
104-
let mut files: Vec<FileInfo> = Vec::new();
105-
let mut dirs: Vec<PathBuf> = Vec::new();
106-
let mut total_bytes = 0u64;
107-
let mut last_progress_time = Instant::now();
108-
let mut visited = HashSet::new();
109-
110-
let result: Result<(), String> = (|| {
111-
let ctx = WalkContext {
112-
progress_interval: state.progress_interval,
113-
is_cancelled: &|| state.cancelled.load(Ordering::Relaxed),
114-
on_io_error: &|_, e| e.to_string(),
115-
on_cancelled: &|| "Cancelled".to_string(),
116-
on_symlink_loop: &|path| format!("Symlink loop detected: {}", path.display()),
117-
on_progress: &|files_found, dirs_found, bytes_found, current_path| {
118-
let _ = app.emit(
119-
"scan-preview-progress",
120-
ScanPreviewProgressEvent {
121-
preview_id: preview_id.to_string(),
122-
files_found,
123-
dirs_found,
124-
bytes_found,
125-
current_path,
126-
},
127-
);
128-
},
129-
};
130-
for source in &sources {
131-
let source_root = source.parent().unwrap_or(source);
132-
walk_dir_recursive(
133-
source,
134-
source_root,
135-
&mut files,
136-
&mut dirs,
137-
&mut total_bytes,
138-
&mut last_progress_time,
139-
&mut visited,
140-
&ctx,
141-
)?;
142-
}
143-
Ok(())
144-
})();
145-
146-
// Clean up state
147-
if let Ok(mut cache) = SCAN_PREVIEW_STATE.write() {
148-
cache.remove(&preview_id);
149-
}
150-
151-
match result {
152-
Ok(()) => {
153-
if state.cancelled.load(Ordering::Relaxed) {
154-
// Cancelled
155-
let _ = app.emit(
156-
"scan-preview-cancelled",
157-
ScanPreviewCancelledEvent {
158-
preview_id: preview_id.clone(),
159-
},
160-
);
161-
} else {
162-
// Sort files
163-
sort_files(&mut files, sort_column, sort_order);
164-
165-
// Cache the results
166-
let file_count = files.len();
167-
let dirs_count = dirs.len();
168-
if let Ok(mut cache) = SCAN_PREVIEW_RESULTS.write() {
169-
cache.insert(
170-
preview_id.clone(),
171-
CachedScanResult {
172-
files,
173-
dirs,
174-
file_count,
175-
total_bytes,
176-
},
177-
);
178-
}
179-
180-
// Emit completion
181-
let _ = app.emit(
182-
"scan-preview-complete",
183-
ScanPreviewCompleteEvent {
184-
preview_id,
185-
files_total: file_count,
186-
dirs_total: dirs_count,
187-
bytes_total: total_bytes,
188-
},
189-
);
190-
}
191-
}
192-
Err(message) => {
193-
let _ = app.emit("scan-preview-error", ScanPreviewErrorEvent { preview_id, message });
194-
}
195-
}
196-
}
197-
198-
/// Runs a volume-based scan preview (for MTP and other non-local volumes).
199-
///
200-
/// Uses `Volume::scan_for_copy()` per source path instead of `walk_dir_recursive`.
201-
/// Emits the same events as `run_scan_preview` so the frontend can't tell the difference.
202-
fn run_volume_scan_preview(
203-
app: tauri::AppHandle,
204-
preview_id: String,
205-
sources: Vec<PathBuf>,
206-
volume: Arc<dyn Volume>,
207-
state: Arc<ScanPreviewState>,
208-
) {
209-
use tauri::Emitter;
210-
211-
let mut total_files = 0usize;
212-
let mut total_dirs = 0usize;
213-
let mut total_bytes = 0u64;
214-
let mut last_progress_time = Instant::now();
215-
216-
let result: Result<(), String> = (|| {
217-
for source in &sources {
218-
if state.cancelled.load(Ordering::Relaxed) {
219-
return Err("Cancelled".to_string());
220-
}
221-
222-
let scan = volume
223-
.scan_for_copy(source)
224-
.map_err(|e| format!("Scan failed for {}: {}", source.display(), e))?;
225-
226-
total_files += scan.file_count;
227-
total_dirs += scan.dir_count;
228-
total_bytes += scan.total_bytes;
229-
230-
// Emit progress between source items
231-
if last_progress_time.elapsed() >= state.progress_interval {
232-
let _ = app.emit(
233-
"scan-preview-progress",
234-
ScanPreviewProgressEvent {
235-
preview_id: preview_id.clone(),
236-
files_found: total_files,
237-
dirs_found: total_dirs,
238-
bytes_found: total_bytes,
239-
current_path: source.file_name().map(|n| n.to_string_lossy().to_string()),
240-
},
241-
);
242-
last_progress_time = Instant::now();
243-
}
244-
}
245-
Ok(())
246-
})();
247-
248-
// Clean up state
249-
if let Ok(mut cache) = SCAN_PREVIEW_STATE.write() {
250-
cache.remove(&preview_id);
251-
}
252-
253-
match result {
254-
Ok(()) => {
255-
if state.cancelled.load(Ordering::Relaxed) {
256-
let _ = app.emit(
257-
"scan-preview-cancelled",
258-
ScanPreviewCancelledEvent {
259-
preview_id: preview_id.clone(),
260-
},
261-
);
262-
} else {
263-
// Cache results — volume scans don't produce per-file FileInfo,
264-
// but the cache stores aggregate stats that copy_between_volumes can reuse.
265-
if let Ok(mut cache) = SCAN_PREVIEW_RESULTS.write() {
266-
cache.insert(
267-
preview_id.clone(),
268-
CachedScanResult {
269-
files: Vec::new(),
270-
dirs: Vec::new(),
271-
file_count: total_files,
272-
total_bytes,
273-
},
274-
);
275-
}
276-
277-
let _ = app.emit(
278-
"scan-preview-complete",
279-
ScanPreviewCompleteEvent {
280-
preview_id,
281-
files_total: total_files,
282-
dirs_total: total_dirs,
283-
bytes_total: total_bytes,
284-
},
285-
);
286-
}
287-
}
288-
Err(message) => {
289-
let _ = app.emit("scan-preview-error", ScanPreviewErrorEvent { preview_id, message });
290-
}
291-
}
292-
}
29318

29419
/// Callbacks for customizing `walk_dir_recursive` behavior per caller.
295-
struct WalkContext<'a, E> {
296-
progress_interval: Duration,
297-
is_cancelled: &'a dyn Fn() -> bool,
298-
on_io_error: &'a dyn Fn(&Path, std::io::Error) -> E,
299-
on_cancelled: &'a dyn Fn() -> E,
300-
on_symlink_loop: &'a dyn Fn(&Path) -> E,
301-
on_progress: &'a dyn Fn(usize, usize, u64, Option<String>),
20+
pub(super) struct WalkContext<'a, E> {
21+
pub(super) progress_interval: Duration,
22+
pub(super) is_cancelled: &'a dyn Fn() -> bool,
23+
pub(super) on_io_error: &'a dyn Fn(&Path, std::io::Error) -> E,
24+
pub(super) on_cancelled: &'a dyn Fn() -> E,
25+
pub(super) on_symlink_loop: &'a dyn Fn(&Path) -> E,
26+
pub(super) on_progress: &'a dyn Fn(usize, usize, u64, Option<String>),
30227
}
30328

30429
/// Recursively walks a directory tree, collecting files and directories.
@@ -309,7 +34,7 @@ struct WalkContext<'a, E> {
30934
clippy::too_many_arguments,
31035
reason = "Recursive fn requires passing state through multiple levels"
31136
)]
312-
fn walk_dir_recursive<E>(
37+
pub(super) fn walk_dir_recursive<E>(
31338
path: &Path,
31439
source_root: &Path,
31540
files: &mut Vec<FileInfo>,

0 commit comments

Comments
 (0)