Skip to content

Commit 3042f23

Browse files
committed
Bugfix: Copy → "Cancel" rolled back copied files
- Cancel (keep partial files) triggered a race: `handleCancel(false)` sent `rollback=false`, then `onDestroy` immediately sent `rollback=true`, overwriting the user's choice - Frontend: added `operationSettled` flag set in all five terminal paths (complete, error, cancelled event, cancel button, rollback button) — `onDestroy` safety net now skips if already handled - Backend: `cancel_write_operation` now uses `AtomicBool::swap` — first caller's rollback decision wins, subsequent calls are no-ops - `onDestroy` safety net now passes `rollback=false` — unexpected teardown (hot-reload, window close) stops the operation but never silently deletes files in the background
1 parent 148c057 commit 3042f23

2 files changed

Lines changed: 18 additions & 6 deletions

File tree

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,12 @@ pub fn cancel_write_operation(operation_id: &str, rollback: bool) {
185185
if let Ok(cache) = WRITE_OPERATION_STATE.read()
186186
&& let Some(state) = cache.get(operation_id)
187187
{
188-
state.cancelled.store(true, Ordering::Relaxed);
188+
// If already cancelled, don't overwrite skip_rollback — the first caller's
189+
// decision wins. This prevents a later safety-net cancel from changing the
190+
// rollback policy that the user explicitly chose.
191+
if state.cancelled.swap(true, Ordering::Relaxed) {
192+
return;
193+
}
189194
state.skip_rollback.store(!rollback, Ordering::Relaxed);
190195
// Wake up any waiting conflict resolution
191196
let _guard = state.conflict_mutex.lock();

apps/desktop/src/lib/file-operations/transfer/TransferProgressDialog.svelte

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@
153153
let isCancelling = $state(false)
154154
let isRollingBack = $state(false)
155155
let destroyed = false
156+
/** Set when the operation reaches a terminal state (complete, error, cancel, rollback).
157+
* Prevents onDestroy's safety-net cancel from interfering with an already-handled outcome. */
158+
let operationSettled = false
156159
157160
// Events that arrived before we know our operationId (from the command response).
158161
// Without buffering, a stale event from a previous operation could claim the ID slot first.
@@ -299,6 +302,7 @@
299302
bytesProcessed: event.bytesProcessed,
300303
})
301304
305+
operationSettled = true
302306
cleanup()
303307
304308
const totalFiles = event.filesProcessed
@@ -321,6 +325,7 @@
321325
322326
log.error('{op} error: {errorType}', { op: operationLabel, errorType: event.error.type, error: event.error })
323327
328+
operationSettled = true
324329
cleanup()
325330
onError(event.error)
326331
}
@@ -334,6 +339,7 @@
334339
rolledBack: event.rolledBack,
335340
})
336341
342+
operationSettled = true
337343
cleanup()
338344
onCancelled(event.filesProcessed)
339345
}
@@ -511,6 +517,7 @@
511517
if (rollback) {
512518
// Rollback: keep dialog open, show "Rolling back...", wait for event
513519
log.info('Rolling back operation: {operationId}', { operationId })
520+
operationSettled = true
514521
isRollingBack = true
515522
isCancelling = true
516523
try {
@@ -530,6 +537,7 @@
530537
await cancelWriteOperation(operationId, false)
531538
log.debug('Cancel request sent successfully')
532539
// Close immediately without waiting for backend confirmation
540+
operationSettled = true
533541
cleanup()
534542
onCancelled(filesDone)
535543
} catch (err) {
@@ -665,11 +673,10 @@
665673
void cancelScanPreview(previewId)
666674
}
667675
cleanupScanListeners()
668-
if (operationId) {
669-
// Cancel with rollback on unexpected teardown (hot-reload, navigation, crash).
670-
// Normal user-initiated cancel/complete already ran cleanup() + callbacks,
671-
// and the backend ignores cancel on finished operations (idempotent).
672-
void cancelWriteOperation(operationId, true)
676+
if (operationId && !operationSettled) {
677+
// Unexpected teardown (hot-reload, navigation, window close): stop the operation
678+
// but don't roll back — never do silent background work without visual feedback.
679+
void cancelWriteOperation(operationId, false)
673680
}
674681
cleanup()
675682
})

0 commit comments

Comments
 (0)