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
- New `ConflictResolution::OverwriteSmaller` / `OverwriteOlder` variants. Strict comparison: equal sizes / equal mtimes / unknown metadata always reduce to `Skip` so a borderline file is never silently overwritten. Implemented in `helpers::reduce_conditional_resolution` (local FS) and `volume_conflict::reduce_volume_conditional_resolution` (volume, async, with size hints + `get_metadata` for mtime).
- Per-file conflict dialog reorganized into a 2-col × 4-row grid: single-file action left, apply-to-all right; the two new conditional bulk buttons live in the 4th row (always apply-to-all by design).
- `TransferDialog` upfront radios extended with the two new policies, wrap naturally via flex-wrap.
- Volume `WriteConflictEvent` now populates `source_modified` / `destination_modified` / `destination_is_newer` via `get_metadata`, so MTP/SMB copies render `(newer)` annotations like local-FS already did. Stale "we can't easily get modification times from Volume trait" comment removed.
- Each Skip under a conditional policy logs the reason (`target: "conflict_resolution"`), so an SMB copy with `OverwriteOlder` against a backend that doesn't surface `modified_at` is debuggable instead of silent.
- The "apply-to-all" persistence stores the *original* conditional variant, so each subsequent conflict re-evaluates per-file against its own source/dest.
- 29 new Rust unit tests pin the strict-comparison contract across all axes (local + volume, with and without size hints, missing metadata, equal/larger/older/newer, axis independence).
- 3 new Playwright specs (`conflict-overwrite-conditional.spec.ts`) cover both the upfront radios and the per-file button.
- `write_operations/CLAUDE.md` and `file-operations/CLAUDE.md` updated.
|`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. |
30
30
|`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_conflict.rs`, `volume_strategy.rs`| Conflict resolution (Stop/Skip/Overwrite/Rename) and copy strategy selection for volume operations. |
32
+
|`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). |
33
33
|`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"). |
rename temp → dest, delete backup. The original is intact until step 3 completes.
110
110
111
+
**Conditional conflict policies (`OverwriteSmaller` / `OverwriteOlder`)** reduce per-file. The user picks "Overwrite all smaller" / "Overwrite all older" either upfront (TransferDialog radios) or via the per-file conflict dialog's apply-to-all buttons. Each conflict re-evaluates against its own source/dest metadata: `OverwriteSmaller` overwrites only when `dst.len() < src.len()`, `OverwriteOlder` overwrites only when `dst.modified() < src.modified()`. Equal sizes / equal mtimes / unknown metadata all reduce to `Skip` — strict comparison so a borderline file is never silently overwritten. Implemented by `helpers::reduce_conditional_resolution` (sync, local FS) and `volume_conflict::reduce_volume_conditional_resolution` (async, volume backends). Both log a `target: "conflict_resolution"` info line on every Skip with the reason (not-strictly-smaller, not-strictly-older, missing metadata), so users running an MTP/SMB copy who picked one of these can see in the operation log why their conflicts got skipped instead of being puzzled by silence. **The apply-to-all storage saves the *original* conditional variant**, not the reduced one — subsequent conflicts re-run the comparison against their own files. Tested exhaustively across the comparison axes in `helpers::conditional_resolution_tests` and `volume_conflict::tests::volume_*`.
112
+
111
113
**Stop-mode conflict resolution.** Emits `write-conflict` event, then blocks on a `tokio::sync::oneshot` channel
112
114
(`blocking_recv()` inside `spawn_blocking`). A new oneshot channel is created per conflict. Frontend calls
113
115
`resolve_write_conflict(operation_id, resolution, apply_to_all)` which takes the stored `Sender` and sends the
@@ -183,7 +185,7 @@ frontend's `handleError` removes all listeners on first receipt.
183
185
threads (used for temp/backup file cleanup, not for user-visible rollback). If the network mount disconnects or the app
184
186
exits, partial files or staging directories may remain on disk. These use the `.cmdr-` prefix, so they're recognizable.
185
187
186
-
**`volume_copy` path is fully wired up.** The three `volume_*` files are re-exported from `mod.rs` and called by the `copy_between_volumes` and `move_between_volumes` Tauri commands. Both copy and move operations support conflict detection and resolution (Stop/Skip/Overwrite/Rename) for all volume combinations (Local↔MTP, MTP↔MTP). Volume copy supports rollback (delete all copied files in reverse order with progress events, matching the local copy's `rollback_with_progress` pattern) and cancel cleanup (delete only the last partial file). Rollback uses `delete_volume_path_recursive` which lists directory contents via `Volume::list_directory` and deletes children before parents.
188
+
**`volume_copy` path is fully wired up.** The three `volume_*` files are re-exported from `mod.rs` and called by the `copy_between_volumes` and `move_between_volumes` Tauri commands. Both copy and move operations support conflict detection and resolution (Stop/Skip/Overwrite/Rename/OverwriteSmaller/OverwriteOlder) for all volume combinations (Local↔MTP, MTP↔MTP). Volume copy supports rollback (delete all copied files in reverse order with progress events, matching the local copy's `rollback_with_progress` pattern) and cancel cleanup (delete only the last partial file). Rollback uses `delete_volume_path_recursive` which lists directory contents via `Volume::list_directory` and deletes children before parents.
187
189
188
190
**`write-error` carries a provider-enriched `FriendlyError` for both move and copy.** Both `move_between_volumes` and `copy_volumes_with_progress` keep the originating `VolumeError + path` alongside each `?`-propagated `WriteOperationError` via the shared `WriteFailure` struct (in `volume_copy.rs`). `WriteFailure::from_volume(path, e)` and `WriteFailure::synthetic(write_err)` are the two constructors: one captures volume context, the other doesn't (cancellation, validation, synthetic IoError). The shared `write_error_event_from(...)` helper builds a `WriteErrorEvent` from any `WriteFailure`: when the volume context is present it calls `WriteErrorEvent::with_friendly` (full `friendly_error_from_volume_error` + `enrich_with_provider` pipeline, picks up provider-specific suggestions like "This folder is managed by **MacDroid**…"); otherwise it falls back to the variant-derived `WriteErrorEvent::new` via `friendly_from_write_error`. Both move and copy paths land at the same FE quality.
0 commit comments