Skip to content

Commit a056eb5

Browse files
committed
Local write ops: helpers take sink
- `scan_sources`, `dry_run_scan` (+ `_internal`, `_recursive`), `handle_dry_run`, `resolve_conflict`, and `copy_single_item` migrate from `&tauri::AppHandle` to `&dyn OperationEventSink`. All `app.emit(...)` sites become `events.emit_*(...)` and `state.emit_progress_via_app(...)` becomes `state.emit_progress_via_sink(...)`. - `OperationEventSink` trait gains `emit_scan_progress`, `emit_scan_conflict`, `emit_dry_run_complete` so the dry-run path goes through the sink. `TauriEventSink` and `CollectorEventSink` implement them. - Add `helpers::run_cancellable_scoped`, a `std::thread::scope`-backed variant of `run_cancellable` that lets the worker closure borrow non-`'static` data (sink reference). `scan_sources` and `dry_run_scan` use it; `validate_disk_space_cancellable` keeps the detached-thread original. - `copy_files_with_progress_inner` drops its `app: &tauri::AppHandle` parameter; it now flows everything through `events` only. The intra-file chunked-copy progress callback emits via the sink too. - `move_op.rs` and `delete.rs` keep their `&AppHandle` signatures (no inner extraction yet) and construct a `TauriEventSink` ad-hoc to pass to the migrated helpers. `merge_move_directory` itself flips to the sink. Full inner extraction lands in follow-up commits. - Test sinks in `volume_copy_tests.rs` and `volume_move_tests.rs` add the three new trait stubs. - Allow `clippy::doc_lazy_continuation` / `too_many_arguments` on `driver_prototype_scratch.rs` so the throwaway M2 prototype doesn't block CI.
1 parent 643e7cb commit a056eb5

10 files changed

Lines changed: 202 additions & 127 deletions

File tree

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ fn validate_disk_space_cancellable(
5252

5353
pub(super) fn copy_files_with_progress_inner(
5454
events: &dyn OperationEventSink,
55-
app: &tauri::AppHandle,
5655
operation_id: &str,
5756
state: &Arc<WriteOperationState>,
5857
sources: &[PathBuf],
@@ -71,7 +70,7 @@ pub(super) fn copy_files_with_progress_inner(
7170
sources,
7271
destination,
7372
state,
74-
app,
73+
events,
7574
operation_id,
7675
WriteOperationType::Copy,
7776
state.progress_interval,
@@ -105,7 +104,7 @@ pub(super) fn copy_files_with_progress_inner(
105104
scan_sources(
106105
sources,
107106
state,
108-
app,
107+
events,
109108
operation_id,
110109
WriteOperationType::Copy,
111110
config.sort_column,
@@ -121,7 +120,7 @@ pub(super) fn copy_files_with_progress_inner(
121120
scan_sources(
122121
sources,
123122
state,
124-
app,
123+
events,
125124
operation_id,
126125
WriteOperationType::Copy,
127126
config.sort_column,
@@ -271,7 +270,7 @@ pub(super) fn copy_files_with_progress_inner(
271270
scan_result.file_count,
272271
scan_result.total_bytes,
273272
state,
274-
app,
273+
events,
275274
operation_id,
276275
WriteOperationType::Copy,
277276
&state.progress_interval,
@@ -458,7 +457,7 @@ pub(super) fn copy_single_item(
458457
files_total: usize,
459458
bytes_total: u64,
460459
state: &Arc<WriteOperationState>,
461-
app: &tauri::AppHandle,
460+
events: &dyn OperationEventSink,
462461
operation_id: &str,
463462
operation_type: WriteOperationType,
464463
progress_interval: &Duration,
@@ -517,7 +516,7 @@ pub(super) fn copy_single_item(
517516
&blocking,
518517
&blocking,
519518
config,
520-
app,
519+
events,
521520
operation_id,
522521
state,
523522
apply_to_all_resolution,
@@ -612,7 +611,7 @@ pub(super) fn copy_single_item(
612611
source,
613612
&dest_path,
614613
config,
615-
app,
614+
events,
616615
operation_id,
617616
state,
618617
apply_to_all_resolution,
@@ -666,7 +665,7 @@ pub(super) fn copy_single_item(
666665
source,
667666
&dest_path,
668667
config,
669-
app,
668+
events,
670669
operation_id,
671670
state,
672671
apply_to_all_resolution,
@@ -721,8 +720,8 @@ pub(super) fn copy_single_item(
721720
effective_bytes_done,
722721
bytes_total
723722
);
724-
state.emit_progress_via_app(
725-
app,
723+
state.emit_progress_via_sink(
724+
events,
726725
WriteProgressEvent::new(
727726
operation_id.to_string(),
728727
operation_type,
@@ -770,8 +769,8 @@ pub(super) fn copy_single_item(
770769
*bytes_done,
771770
bytes_total
772771
);
773-
state.emit_progress_via_app(
774-
app,
772+
state.emit_progress_via_sink(
773+
events,
775774
WriteProgressEvent::new(
776775
operation_id.to_string(),
777776
operation_type,

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use super::helpers::spawn_async_sync;
99
use super::scan::{SourceItemTracker, scan_sources};
1010
use super::state::{WriteOperationState, update_operation_status};
1111
use super::types::{
12-
DryRunResult, IoResultExt, WriteCancelledEvent, WriteCompleteEvent, WriteOperationConfig, WriteOperationError,
13-
WriteOperationPhase, WriteOperationType, WriteProgressEvent, WriteSourceItemDoneEvent,
12+
DryRunResult, IoResultExt, TauriEventSink, WriteCancelledEvent, WriteCompleteEvent, WriteOperationConfig,
13+
WriteOperationError, WriteOperationPhase, WriteOperationType, WriteProgressEvent, WriteSourceItemDoneEvent,
1414
};
1515
use super::volume_copy::map_volume_error;
1616
use crate::file_system::volume::Volume;
@@ -28,11 +28,13 @@ pub(super) fn delete_files_with_progress(
2828
) -> Result<(), WriteOperationError> {
2929
use tauri::Emitter;
3030

31+
let events = TauriEventSink::new(app.clone());
32+
3133
// Phase 1: Scan to get file count (delete uses default sorting)
3234
let scan_result = scan_sources(
3335
sources,
3436
state,
35-
app,
37+
&events,
3638
operation_id,
3739
WriteOperationType::Delete,
3840
config.sort_column,

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,20 @@
7777
7878
#![cfg(test)]
7979
#![allow(dead_code, reason = "Throwaway prototype, deleted at end of M2")]
80+
#![allow(
81+
clippy::doc_lazy_continuation,
82+
clippy::too_many_arguments,
83+
reason = "Throwaway prototype, not held to the project's lints"
84+
)]
8085

81-
use std::collections::HashSet;
82-
use std::path::{Path, PathBuf};
83-
use std::sync::Arc;
8486
use super::scan::SourceItemTracker;
8587
use super::state::{CopyTransaction, FileInfo, WriteOperationState};
8688
use super::types::{
87-
ConflictResolution, OperationEventSink, WriteOperationError, WriteOperationType,
88-
WriteSourceItemDoneEvent,
89+
ConflictResolution, OperationEventSink, WriteOperationError, WriteOperationType, WriteSourceItemDoneEvent,
8990
};
91+
use std::collections::HashSet;
92+
use std::path::{Path, PathBuf};
93+
use std::sync::Arc;
9094

9195
// ---------------------------------------------------------------------------
9296
// Proposed driver API surface (sketch, not the final shape)

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

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ use super::macos_copy::{CopyProgressContext, copy_single_file_native};
1515
use super::state::WriteOperationState;
1616
#[cfg(not(target_os = "macos"))]
1717
use super::types::IoResultExt;
18-
use super::types::{ConflictInfo, ConflictResolution, WriteConflictEvent, WriteOperationConfig, WriteOperationError};
18+
use super::types::{
19+
ConflictInfo, ConflictResolution, OperationEventSink, WriteConflictEvent, WriteOperationConfig, WriteOperationError,
20+
};
1921

2022
// ============================================================================
2123
// Validation helpers
@@ -337,13 +339,11 @@ pub(super) fn resolve_conflict(
337339
source: &Path,
338340
dest_path: &Path,
339341
config: &WriteOperationConfig,
340-
app: &tauri::AppHandle,
342+
events: &dyn OperationEventSink,
341343
operation_id: &str,
342344
state: &Arc<WriteOperationState>,
343345
apply_to_all_resolution: &mut Option<ConflictResolution>,
344346
) -> Result<Option<ResolvedDestination>, WriteOperationError> {
345-
use tauri::Emitter;
346-
347347
// Determine effective conflict resolution
348348
let resolution = if let Some(saved_resolution) = apply_to_all_resolution {
349349
// Use saved "apply to all" resolution
@@ -385,20 +385,17 @@ pub(super) fn resolve_conflict(
385385
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
386386
.map(|d| d.as_secs() as i64);
387387

388-
let _ = app.emit(
389-
"write-conflict",
390-
WriteConflictEvent {
391-
operation_id: operation_id.to_string(),
392-
source_path: source.display().to_string(),
393-
destination_path: dest_path.display().to_string(),
394-
source_size,
395-
destination_size,
396-
source_modified,
397-
destination_modified,
398-
destination_is_newer,
399-
size_difference,
400-
},
401-
);
388+
events.emit_conflict(WriteConflictEvent {
389+
operation_id: operation_id.to_string(),
390+
source_path: source.display().to_string(),
391+
destination_path: dest_path.display().to_string(),
392+
source_size,
393+
destination_size,
394+
source_modified,
395+
destination_modified,
396+
destination_is_newer,
397+
size_difference,
398+
});
402399

403400
// Create a oneshot channel for this conflict resolution
404401
let (tx, rx) = tokio::sync::oneshot::channel();
@@ -703,3 +700,51 @@ where
703700
}
704701
}
705702
}
703+
704+
/// Scoped variant of [`run_cancellable`] that allows the work closure to borrow
705+
/// non-`'static` data (for example, a `&dyn OperationEventSink` reference).
706+
///
707+
/// Uses `std::thread::scope`, so the call blocks until the worker thread
708+
/// finishes or cancellation is observed. Behavior is otherwise identical to
709+
/// `run_cancellable`: the cancellation flag is polled every 100ms while the
710+
/// worker runs, and the function returns early on cancellation.
711+
pub(super) fn run_cancellable_scoped<'env, T, F>(
712+
work: F,
713+
state: &Arc<WriteOperationState>,
714+
context: &str,
715+
operation_id: &str,
716+
) -> Result<T, WriteOperationError>
717+
where
718+
F: FnOnce() -> Result<T, WriteOperationError> + Send + 'env,
719+
T: Send + 'env,
720+
{
721+
use std::sync::mpsc;
722+
723+
let (tx, rx) = mpsc::channel();
724+
725+
std::thread::scope(|scope| {
726+
scope.spawn(move || {
727+
let _ = tx.send(work());
728+
});
729+
730+
loop {
731+
if super::state::is_cancelled(&state.intent) {
732+
log::debug!("{context}: cancellation detected during polling op={operation_id}");
733+
return Err(WriteOperationError::Cancelled {
734+
message: "Operation cancelled by user".to_string(),
735+
});
736+
}
737+
738+
match rx.recv_timeout(CANCELLATION_POLL_INTERVAL) {
739+
Ok(result) => return result,
740+
Err(mpsc::RecvTimeoutError::Timeout) => {}
741+
Err(mpsc::RecvTimeoutError::Disconnected) => {
742+
return Err(WriteOperationError::IoError {
743+
path: context.to_string(),
744+
message: format!("{context} thread terminated unexpectedly"),
745+
});
746+
}
747+
}
748+
}
749+
})
750+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ pub async fn copy_files_start(
196196
validate_not_same_location(&sources, &destination)?;
197197
validate_destination_not_inside_source(&sources, &destination)?;
198198
let events = types::TauriEventSink::new(app.clone());
199-
copy_files_with_progress_inner(&events, &app, &op_id, &state, &sources, &destination, &config)
199+
copy_files_with_progress_inner(&events, &op_id, &state, &sources, &destination, &config)
200200
},
201201
)
202202
.await
@@ -363,10 +363,10 @@ pub async fn trash_files_start(
363363
#[cfg(test)]
364364
mod copy_integration_test;
365365
#[cfg(test)]
366-
mod driver_prototype_scratch;
367-
#[cfg(test)]
368366
mod delete_integration_test;
369367
#[cfg(test)]
368+
mod driver_prototype_scratch;
369+
#[cfg(test)]
370370
mod move_integration_test;
371371
#[cfg(test)]
372372
mod tests;

0 commit comments

Comments
 (0)