Skip to content

Commit 547a413

Browse files
committed
MTP: unified backend move, fix event loop lock
- Add `move_between_volumes` backend command: same-volume uses native `Volume::rename` (MTP MoveObject), both-local delegates to `move_files_start`, cross-volume does copy+delete per file. All paths are async with operation registration, progress events, and cancellation. - Expand `MtpVolume::rename` to handle cross-directory moves via `move_object`, with optional rename when the name also changes. - Remove frontend MTP move orchestration: `isMtpMove`, `mtpMovePhase`, `startMtpMoveDeletePhase`, three-stage progress UI. Frontend now calls one command, backend handles strategy. - Fix event loop lock contention: clone `MtpDevice` for event polling instead of holding Cmdr device mutex during `next_event()`. Enabled by mtp-rs 0.7.0 `Clone` derive + concurrency docs. - Fix scan preview for MTP: `start_scan_preview` is now async, passes Tokio runtime handle to the volume scan thread so `MtpVolume::block_on` works.
1 parent 147fb6d commit 547a413

12 files changed

Lines changed: 513 additions & 170 deletions

File tree

apps/desktop/src-tauri/src/commands/file_system.rs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ use crate::file_system::{
1919
get_max_filename_width as ops_get_max_filename_width, get_operation_status as ops_get_operation_status,
2020
get_total_count as ops_get_total_count, get_volume_manager, list_active_operations as ops_list_active_operations,
2121
list_directory_end as ops_list_directory_end, list_directory_start_streaming as ops_list_directory_start_streaming,
22-
list_directory_start_with_volume as ops_list_directory_start_with_volume, move_files_start as ops_move_files_start,
22+
list_directory_start_with_volume as ops_list_directory_start_with_volume,
23+
move_between_volumes as ops_move_between_volumes, move_files_start as ops_move_files_start,
2324
refresh_listing_index_sizes as ops_refresh_listing_index_sizes, resort_listing as ops_resort_listing,
2425
scan_for_volume_copy as ops_scan_for_volume_copy, trash_files_start as ops_trash_files_start,
2526
};
@@ -506,7 +507,7 @@ pub fn cancel_all_write_operations() {
506507
/// When `source_volume_id` is provided and is not "root", the scan uses the Volume trait
507508
/// (enabling MTP and other non-local volumes). Otherwise, uses `std::fs` for local scanning.
508509
#[tauri::command]
509-
pub fn start_scan_preview(
510+
pub async fn start_scan_preview(
510511
app: tauri::AppHandle,
511512
sources: Vec<String>,
512513
source_volume_id: Option<String>,
@@ -530,8 +531,16 @@ pub fn start_scan_preview(
530531
get_volume_manager().get(&volume_id)
531532
};
532533

534+
// Volume scans need a Tokio runtime handle (MtpVolume uses Handle::block_on).
535+
// Async Tauri commands run on the Tokio runtime, so Handle::current() works here.
536+
let runtime_handle = if source_volume.is_some() {
537+
Some(tokio::runtime::Handle::current())
538+
} else {
539+
None
540+
};
541+
533542
let progress_interval = progress_interval_ms.unwrap_or(500);
534-
ops_start_scan_preview(app, sources, source_volume, sort_column, sort_order, progress_interval)
543+
ops_start_scan_preview(app, sources, source_volume, sort_column, sort_order, progress_interval, runtime_handle)
535544
}
536545

537546
#[tauri::command]
@@ -686,6 +695,38 @@ pub async fn copy_between_volumes(
686695
ops_copy_between_volumes(app, source_volume, source_paths, dest_volume, dest_path, config).await
687696
}
688697

698+
/// Unified move across volume types. Same events as `copy_between_volumes`.
699+
/// Handles same-volume (native rename/move), both-local (native move), and cross-volume (copy+delete).
700+
#[tauri::command]
701+
pub async fn move_between_volumes(
702+
app: tauri::AppHandle,
703+
source_volume_id: String,
704+
source_paths: Vec<String>,
705+
dest_volume_id: String,
706+
dest_path: String,
707+
config: Option<VolumeCopyConfig>,
708+
) -> Result<WriteOperationStartResult, WriteOperationError> {
709+
let source_volume = get_volume_manager()
710+
.get(&source_volume_id)
711+
.ok_or_else(|| WriteOperationError::IoError {
712+
path: source_volume_id.clone(),
713+
message: format!("Source volume '{}' not found", source_volume_id),
714+
})?;
715+
716+
let dest_volume = get_volume_manager()
717+
.get(&dest_volume_id)
718+
.ok_or_else(|| WriteOperationError::IoError {
719+
path: dest_volume_id.clone(),
720+
message: format!("Destination volume '{}' not found", dest_volume_id),
721+
})?;
722+
723+
let source_paths: Vec<PathBuf> = source_paths.iter().map(PathBuf::from).collect();
724+
let dest_path = PathBuf::from(dest_path);
725+
let config = config.unwrap_or_default();
726+
727+
ops_move_between_volumes(app, source_volume, source_paths, dest_volume, dest_path, config).await
728+
}
729+
689730
/// Pre-flight scan: total count/bytes, available space, conflicts. Doesn't copy anything.
690731
#[tauri::command]
691732
pub async fn scan_volume_for_copy(

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ pub use write_operations::{
5656
// Re-export volume copy types and functions
5757
// TODO: Remove this allow once volume_copy is integrated into Tauri commands (Phase 5)
5858
#[allow(unused_imports, reason = "Volume copy not yet integrated into Tauri commands")]
59-
pub use write_operations::{VolumeCopyConfig, VolumeCopyScanResult, copy_between_volumes, scan_for_volume_copy};
59+
pub use write_operations::{
60+
VolumeCopyConfig, VolumeCopyScanResult, copy_between_volumes, move_between_volumes, scan_for_volume_copy,
61+
};
6062

6163
/// Global volume manager instance
6264
static VOLUME_MANAGER: LazyLock<VolumeManager> = LazyLock::new(VolumeManager::new);

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

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -281,27 +281,75 @@ impl Volume for MtpVolume {
281281
return Err(VolumeError::AlreadyExists(to.display().to_string()));
282282
}
283283

284-
let mtp_path = self.to_mtp_path(from);
285-
// Extract the new name from the destination path
286-
let new_name = to
284+
let from_mtp = self.to_mtp_path(from);
285+
let to_mtp = self.to_mtp_path(to);
286+
287+
let from_parent = Path::new(&from_mtp).parent().unwrap_or(Path::new(""));
288+
let to_parent = Path::new(&to_mtp).parent().unwrap_or(Path::new(""));
289+
let same_parent = from_parent == to_parent;
290+
291+
let from_name = Path::new(&from_mtp)
287292
.file_name()
288293
.and_then(|n| n.to_str())
289-
.ok_or_else(|| VolumeError::IoError("Invalid destination path".into()))?
290-
.to_string();
294+
.ok_or_else(|| VolumeError::IoError("Invalid source path".into()))?;
295+
let to_name = Path::new(&to_mtp)
296+
.file_name()
297+
.and_then(|n| n.to_str())
298+
.ok_or_else(|| VolumeError::IoError("Invalid destination path".into()))?;
299+
let same_name = from_name == to_name;
291300

292301
let device_id = self.device_id.clone();
293302
let storage_id = self.storage_id;
294-
295303
let handle = tokio::runtime::Handle::current();
296304

297-
handle
298-
.block_on(async move {
299-
connection_manager()
300-
.rename_object(&device_id, storage_id, &mtp_path, &new_name)
301-
.await
302-
})
303-
.map(|_| ())
304-
.map_err(map_mtp_error)
305+
if same_parent {
306+
// Same directory — just rename
307+
let new_name = to_name.to_string();
308+
handle
309+
.block_on(async {
310+
connection_manager()
311+
.rename_object(&device_id, storage_id, &from_mtp, &new_name)
312+
.await
313+
})
314+
.map(|_| ())
315+
.map_err(map_mtp_error)
316+
} else {
317+
// Different directory — use MTP MoveObject
318+
let to_parent_str = to_parent.to_string_lossy().to_string();
319+
handle
320+
.block_on(async {
321+
connection_manager()
322+
.move_object(&device_id, storage_id, &from_mtp, &to_parent_str)
323+
.await
324+
})
325+
.map(|_| ())
326+
.map_err(map_mtp_error)?;
327+
328+
// If the name also changed, rename after moving
329+
if !same_name {
330+
let moved_path = format!(
331+
"{}{}{}",
332+
to_parent_str,
333+
if to_parent_str.is_empty() || to_parent_str.ends_with('/') {
334+
""
335+
} else {
336+
"/"
337+
},
338+
from_name
339+
);
340+
let new_name = to_name.to_string();
341+
handle
342+
.block_on(async {
343+
connection_manager()
344+
.rename_object(&device_id, storage_id, &moved_path, &new_name)
345+
.await
346+
})
347+
.map(|_| ())
348+
.map_err(map_mtp_error)?;
349+
}
350+
351+
Ok(())
352+
}
305353
}
306354

307355
fn supports_export(&self) -> bool {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ pub(crate) use state::{CopyTransaction, WriteOperationState};
8181

8282
// Re-export volume copy types and functions
8383
pub use types::{VolumeCopyConfig, VolumeCopyScanResult};
84-
pub use volume_copy::{copy_between_volumes, scan_for_volume_copy};
84+
pub use volume_copy::{copy_between_volumes, move_between_volumes, scan_for_volume_copy};
8585

8686
// ============================================================================
8787
// Public API functions

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub fn start_scan_preview(
4040
sort_column: SortColumn,
4141
sort_order: SortOrder,
4242
progress_interval_ms: u64,
43+
runtime_handle: Option<tokio::runtime::Handle>,
4344
) -> ScanPreviewStartResult {
4445
let preview_id = Uuid::new_v4().to_string();
4546
let preview_id_clone = preview_id.clone();
@@ -54,14 +55,21 @@ pub fn start_scan_preview(
5455
cache.insert(preview_id.clone(), Arc::clone(&state));
5556
}
5657

57-
// Spawn background task
58-
std::thread::spawn(move || {
59-
if let Some(volume) = source_volume {
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();
6066
run_volume_scan_preview(app, preview_id_clone, sources, volume, state);
61-
} else {
67+
});
68+
} else {
69+
std::thread::spawn(move || {
6270
run_scan_preview(app, preview_id_clone, sources, sort_column, sort_order, state);
63-
}
64-
});
71+
});
72+
}
6573

6674
ScanPreviewStartResult { preview_id }
6775
}

0 commit comments

Comments
 (0)