Skip to content

Commit 531bb9b

Browse files
committed
Async Volume trait: eliminate nested-runtime panics
- `Volume` trait: 20 I/O methods now return `Pin<Box<dyn Future + Send>>`, 13 accessors stay sync - `VolumeReadStream::next_chunk` is async — MTP downloads use direct `.await`, no channel bridge - `LocalPosixVolume`: all I/O methods wrap in `spawn_blocking` (blocking syscalls stay off the runtime) - `MtpVolume`: all `Handle::block_on` removed, direct `.await` on `MtpConnectionManager` - `SmbVolume`: all `block_on`/`with_smb` bridges removed, `std::sync::Mutex` → `tokio::sync::Mutex` - `InMemoryVolume`: trivially async (no I/O) - Copy/move/delete pipelines: `spawn_blocking` removed, functions are `async fn` - Conflict resolution: `Condvar` → `tokio::sync::oneshot` channel per conflict - Consolidated `export_to_local` + `import_from_local` — progress callback is always part of the signature - `MtpReadStream::Drop` spawns detached cancel task to prevent `ReceiveStream` panic - Cancelled copy operations now log at INFO, not ERROR - Upgraded `mtp-rs` to v0.12.0 (streaming uploads, `Send` bounds on stream types) - Updated CLAUDE.md files for volume, write_operations, listing, and mtp modules
1 parent cac24a1 commit 531bb9b

40 files changed

Lines changed: 3809 additions & 3251 deletions

Cargo.lock

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ mime_guess = "2"
107107

108108
[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies]
109109
# MTP (Android device) support via pure Rust implementation
110-
mtp-rs = "0.11.0"
110+
mtp-rs = "0.12.0"
111111
# USB hotplug detection for MTP device watcher
112112
nusb = "0.2.3"
113113
bytes = "1"

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,9 @@ pub async fn path_exists(volume_id: Option<String>, path: String) -> bool {
4949
// Try to use Volume abstraction
5050
if let Some(volume) = get_volume_manager().get(&volume_id) {
5151
let path_for_check = expanded_path.clone();
52-
return blocking_with_timeout(PATH_EXISTS_TIMEOUT, false, move || {
53-
volume.exists(Path::new(&path_for_check))
54-
})
55-
.await;
52+
return tokio::time::timeout(PATH_EXISTS_TIMEOUT, volume.exists(Path::new(&path_for_check)))
53+
.await
54+
.unwrap_or_default();
5655
}
5756

5857
// Fallback for unknown volumes (shouldn't happen in practice)

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

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,14 @@ pub async fn scan_volume_for_copy(
9494
let dest_path = PathBuf::from(dest_path);
9595
let max_conflicts = max_conflicts.unwrap_or(100);
9696

97-
// Run scan in blocking context for MTP volume support
97+
// Run scan (now async)
9898
tokio::time::timeout(
9999
Duration::from_secs(30),
100-
tokio::task::spawn_blocking(move || {
101-
ops_scan_for_volume_copy(&*source_volume, &source_paths, &*dest_volume, &dest_path, max_conflicts)
102-
.map_err(|e| e.to_string())
103-
}),
100+
ops_scan_for_volume_copy(&*source_volume, &source_paths, &*dest_volume, &dest_path, max_conflicts),
104101
)
105102
.await
106103
.map_err(|_| IpcError::timeout())?
107-
.map_err(|e| IpcError::from_err(format!("Scan task failed: {}", e)))?
108-
.map_err(IpcError::from_err)
104+
.map_err(|e| IpcError::from_err(e.to_string()))
109105
}
110106

111107
/// Checks which source items already exist at the destination. Returns conflict details for UI.
@@ -129,19 +125,14 @@ pub async fn scan_volume_for_conflicts(
129125
.collect();
130126
let dest_path = PathBuf::from(dest_path);
131127

132-
// Run in blocking context for MTP volume support
128+
// Run conflict scan (now async)
133129
tokio::time::timeout(
134130
Duration::from_secs(30),
135-
tokio::task::spawn_blocking(move || {
136-
volume
137-
.scan_for_conflicts(&source_items, &dest_path)
138-
.map_err(|e| e.to_string())
139-
}),
131+
volume.scan_for_conflicts(&source_items, &dest_path),
140132
)
141133
.await
142134
.map_err(|_| IpcError::timeout())?
143-
.map_err(|e| IpcError::from_err(format!("Conflict scan task failed: {}", e)))?
144-
.map_err(IpcError::from_err)
135+
.map_err(|e| IpcError::from_err(e.to_string()))
145136
}
146137

147138
/// Input type for source item information (used by scan_volume_for_conflicts).

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

Lines changed: 27 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -83,27 +83,19 @@ pub(super) async fn create_directory_core(
8383

8484
// Use spawn_blocking to run the Volume operation in a context where
8585
// tokio::runtime::Handle::current() is available (needed for MtpVolume)
86-
tokio::time::timeout(
87-
Duration::from_secs(5),
88-
tokio::task::spawn_blocking(move || {
89-
volume.create_directory(&new_path_clone).map_err(|e| match e {
90-
crate::file_system::VolumeError::AlreadyExists(_) => {
91-
format!("'{}' already exists", name_owned)
92-
}
93-
crate::file_system::VolumeError::PermissionDenied(_) => {
94-
format!(
95-
"Permission denied: cannot create '{}' in '{}'",
96-
name_owned, parent_path_owned
97-
)
98-
}
99-
_ => format!("Couldn't create folder: {}", e),
100-
})
101-
}),
102-
)
103-
.await
104-
.map_err(|_| IpcError::timeout())?
105-
.map_err(|e| IpcError::from_err(format!("Task failed: {}", e)))?
106-
.map_err(IpcError::from_err)?;
86+
tokio::time::timeout(Duration::from_secs(5), volume.create_directory(&new_path_clone))
87+
.await
88+
.map_err(|_| IpcError::timeout())?
89+
.map_err(|e| match e {
90+
crate::file_system::VolumeError::AlreadyExists(_) => {
91+
IpcError::from_err(format!("'{}' already exists", name_owned))
92+
}
93+
crate::file_system::VolumeError::PermissionDenied(_) => IpcError::from_err(format!(
94+
"Permission denied: cannot create '{}' in '{}'",
95+
name_owned, parent_path_owned
96+
)),
97+
_ => IpcError::from_err(format!("Couldn't create folder: {}", e)),
98+
})?;
10799

108100
return Ok((new_path, expanded_path));
109101
}
@@ -153,27 +145,19 @@ pub(super) async fn create_file_core(
153145
let parent_path_owned = parent_path.to_string();
154146
let name_owned = name.to_string();
155147

156-
tokio::time::timeout(
157-
Duration::from_secs(5),
158-
tokio::task::spawn_blocking(move || {
159-
volume.create_file(&new_path_clone, b"").map_err(|e| match e {
160-
crate::file_system::VolumeError::AlreadyExists(_) => {
161-
format!("'{}' already exists", name_owned)
162-
}
163-
crate::file_system::VolumeError::PermissionDenied(_) => {
164-
format!(
165-
"Permission denied: cannot create '{}' in '{}'",
166-
name_owned, parent_path_owned
167-
)
168-
}
169-
_ => format!("Couldn't create file: {}", e),
170-
})
171-
}),
172-
)
173-
.await
174-
.map_err(|_| IpcError::timeout())?
175-
.map_err(|e| IpcError::from_err(format!("Task failed: {}", e)))?
176-
.map_err(IpcError::from_err)?;
148+
tokio::time::timeout(Duration::from_secs(5), volume.create_file(&new_path_clone, b""))
149+
.await
150+
.map_err(|_| IpcError::timeout())?
151+
.map_err(|e| match e {
152+
crate::file_system::VolumeError::AlreadyExists(_) => {
153+
IpcError::from_err(format!("'{}' already exists", name_owned))
154+
}
155+
crate::file_system::VolumeError::PermissionDenied(_) => IpcError::from_err(format!(
156+
"Permission denied: cannot create '{}' in '{}'",
157+
name_owned, parent_path_owned
158+
)),
159+
_ => IpcError::from_err(format!("Couldn't create file: {}", e)),
160+
})?;
177161

178162
return Ok((new_path, expanded_path));
179163
}
@@ -307,24 +291,8 @@ pub async fn start_scan_preview(
307291
get_volume_manager().get(&volume_id)
308292
};
309293

310-
// Volume scans need a Tokio runtime handle (MtpVolume uses Handle::block_on).
311-
// Async Tauri commands run on the Tokio runtime, so Handle::current() works here.
312-
let runtime_handle = if source_volume.is_some() {
313-
Some(tokio::runtime::Handle::current())
314-
} else {
315-
None
316-
};
317-
318294
let progress_interval = progress_interval_ms.unwrap_or(500);
319-
ops_start_scan_preview(
320-
app,
321-
sources,
322-
source_volume,
323-
sort_column,
324-
sort_order,
325-
progress_interval,
326-
runtime_handle,
327-
)
295+
ops_start_scan_preview(app, sources, source_volume, sort_column, sort_order, progress_interval)
328296
}
329297

330298
#[tauri::command]

0 commit comments

Comments
 (0)