You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- Cross-volume move (e.g. MTP→local) was shipping `bytes_total = 0` on
every progress event, so the FE's `{#if bytesTotal > 0}` gate hid the
Size bar — only the Files bar showed during the operation, even though
`bytes_done` was being tracked correctly. Same-volume rename had the
same shape.
- Extract the preflight scan that `volume_copy` was already doing into a
shared `volume_preflight.rs` module. `scan_volume_sources` reuses the
cached `TransferDialog` preview when one's available (the common
dialog-driven path is hit-the-cache for free), otherwise dispatches
`volume.scan_for_copy_batch`. Returns `(total_files, total_bytes,
source_hints)`; also emits the `Scanning`-phase event and emits
`write-cancelled` on pre-scan cancel so the FE dialog closes cleanly.
- Both `move_volumes_with_progress` and
`move_within_same_volume_with_progress` now call it. Real `total_bytes`
flows into the driver and every per-source emit; the file-only bulk-skip
uses `preflight.known_directory_paths()`.
- Side cleanups the shared scan enables:
* dropped `collect_known_directory_paths` (per-source stat to filter
directories — replaced by `is_directory` from the cached scan).
* dropped the cross-volume copy+delete loop's per-source
`is_directory` probe.
* dropped the same-volume rename's per-iter `get_metadata` for size.
* conflict resolution now gets a real `source_size_hint`, so MTP
conflict dialogs no longer re-list the parent dir per conflict.
- `volume_copy` slimmed by ~104 lines; its inline scan/cached-preview
block + inline `known_directory_paths` builder are gone.
- Tests: `cross_volume_move_emits_bytes_total_on_progress` +
`same_volume_move_emits_bytes_total_on_progress` pin the regression.
Copy file name to clipboardExpand all lines: apps/desktop/src-tauri/src/file_system/write_operations/CLAUDE.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -33,6 +33,7 @@ Pre-flight scans reuse cached listings when the source volume reports an active
33
33
|`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. |
34
34
|`volume_copy.rs`| Volume-to-volume copy (Local↔MTP↔SMB): `copy_between_volumes`, `scan_for_volume_copy`. Uses `OperationEventSink` (not `AppHandle` directly) for event emission. 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. |
|`volume_preflight.rs`| Shared preflight scan for both volume copy and move: `scan_volume_sources` returns a `VolumePreflight { total_files, total_bytes, source_hints }`. Reuses a cached preview from `TransferDialog` when one is available; otherwise dispatches `volume.scan_for_copy_batch` (so MTP's group-by-parent and SMB's pipelined-stat optimizations still kick in). Emits one `WriteProgressEvent { phase: Scanning, … }` so the FE sees the scan stage. On pre-scan cancel, emits `write-cancelled` and returns `Err(Cancelled)` so the FE dialog closes cleanly. |
36
37
|`volume_conflict.rs`, `volume_strategy.rs`| Conflict resolution (Stop/Skip/Overwrite/Rename/OverwriteSmaller/OverwriteOlder) and copy strategy selection for volume operations. `volume_conflict.rs` mirrors the local-FS `reduce_conditional_resolution` from `helpers.rs` with its own `reduce_volume_conditional_resolution` (async, uses size hints + `get_metadata` for mtime). |
37
38
|`transfer_driver.rs`| Shared per-source transfer driver for copy/move ops. Owns the bulk-skip prelude, per-iter cancellation check, conflict-resolve dispatch (async path), per-iter skip accounting, and paired progress/status emit. Two sibling entry points: `drive_transfer_serial_sync` (used by `copy.rs::copy_files_with_progress_inner`; closure captures `&mut CopyTransaction` / `&mut created_dirs` / `&mut SourceItemTracker`) and `drive_transfer_serial_async` (used by `volume_copy::copy_volumes_with_progress`'s serial path and both `volume_move` paths; driver owns top-level conflict detection + dispatch). The `FuturesUnordered` concurrent path in `volume_copy.rs` stays inline (1-of-4 abstraction; see plan § "Concurrent driver scope"). |
38
39
|`tests.rs`| Unit tests. |
@@ -276,6 +277,9 @@ volume-delete handler.
276
277
**Decision**: Volume delete reuses the scan preview and is oracle-aware on the no-preview path.
277
278
**Why**: Before this, `delete_volume_files_with_progress_inner` ignored `config.preview_id` entirely and ran `scan_volume_recursive` again. On MTP that meant a second 17 s parent listing for a 135-photo `/DCIM/Camera` delete after the user had just paid that cost in the pre-flight dialog — and the second scan emitted no per-top-level-file progress, so the UI looked frozen. The fix has three parts. (1) `delete_volume_files_with_progress_inner` calls `take_cached_scan_result(preview_id)` at the top; on hit, top-level files are recorded from `CopyScanResult::total_bytes` with no `is_directory` probe and no `list_directory` round-trip, and top-level dirs recurse via the oracle-aware `scan_volume_recursive` (passing `is_dir_hint = Some(true)` so the recursion never re-probes). (2) The walker's internal `volume.list_directory(path, ...)` is now preceded by `try_get_watched_listing(volume_id, path)`; on hit, the cached entries replace the volume call entirely at every recursion level. (3) On the no-preview path (MCP triggers, programmatic deletes), the top-level `volume.is_directory(source)` probe stays only when the parent oracle misses — when a pane has the source's parent open and watcher-fresh, the type comes from the cached `FileEntry` and the probe is skipped. The cache-hit path also emits a throttled scan-progress event per `progress_interval` while building the entry list, so the FE dialog shows movement during the fast path instead of waiting for the delete phase to start. Pinned by `delete_volume_reuse_tests.rs`. Data-safety contract: stale-by-one cached entries can either silently skip a now-gone file (acceptable: the user already moved it elsewhere) or attempt to delete a missing one (the volume's `delete` errors cleanly). Neither direction can delete the wrong file because we feed `volume.delete(&entry.path)` exact paths the cache observed; a cached entry that races with a concurrent rename ends up addressing the old path the next call won't find.
278
279
280
+
**Decision**: Volume move runs the same preflight scan as volume copy (extracted to `volume_preflight.rs`).
281
+
**Why**: Both `move_volumes_with_progress` (cross-volume copy+delete) and `move_within_same_volume_with_progress` (rename) used to skip the scan phase entirely, sending `bytes_total = 0` on every progress event. The FE's `TransferProgressDialog` hides the Size progress bar behind `{#if bytesTotal > 0}`, so during an MTP→local move the user saw only the Files bar with no size feedback — even though `bytes_done` was being tracked correctly. The fix shares one helper (`scan_volume_sources`) between copy and move: reuses a cached `TransferDialog` preview when available (free in the common dialog-driven path), falls through to `volume.scan_for_copy_batch` otherwise. Move now gets the same `(total_files, total_bytes, source_hints)` triple copy has, so progress events carry the real `bytes_total`, the per-source `is_directory` probe inside the move loop is gone (hint comes from the scan), and the same-volume rename's per-iter `get_metadata` for size is gone (hint again). The previous `collect_known_directory_paths` helper (file-only-bulk-skip via per-source `get_metadata`) is replaced by `VolumePreflight::known_directory_paths()`. Behavior change to flag: programmatic moves with no `preview_id` (MCP, etc.) now pay one batch scan up front; for MTP this is ~one parent listing's RTT. Same cost copy has always paid; consistent across both ops. Pinned by `volume_move::tests::*_emits_bytes_total_on_progress`.
282
+
279
283
**Decision**: Keep `exacl` crate for ACL copy in chunked copies (not custom FFI bindings).
280
284
**Why**: `exacl` adds zero new transitive dependencies (all of its deps, `bitflags`, `log`, `scopeguard`, `uuid`, are already in our tree). It provides cross-platform ACL support (macOS, Linux, FreeBSD) and full ACL parsing/manipulation for potential future UI features. The crate appears unmaintained (last release Feb 2024) but ACL APIs are stable and don't change. Our usage is best-effort with graceful fallback: if `exacl` ever breaks, files still copy, they just lose ACLs. MIT licensed (compatible with BSL).
0 commit comments