Skip to content

Commit 0cc675a

Browse files
committed
MTP: Export files directly, no guess-and-fallback
- `MtpVolume::export_to_local` and `export_to_local_with_progress` now call `download_file` directly instead of routing through `download_recursive` - Remove `download_recursive`, `download_entries_recursive`, and `is_file_by_parent` from `bulk_ops.rs` (dead code) - Eliminates the `ObjectNotFound` ERROR log spam that occurred on every file copy because `download_recursive` tried `list_directory` on file paths before falling back
1 parent a66adf6 commit 0cc675a

File tree

4 files changed

+11
-295
lines changed

4 files changed

+11
-295
lines changed

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -518,19 +518,23 @@ impl Volume for MtpVolume {
518518

519519
let handle = tokio::runtime::Handle::current();
520520
let progress = SendSyncProgress(on_progress);
521+
let operation_id = format!("export-{}", uuid::Uuid::new_v4());
521522

522523
handle
523524
.block_on(async {
524525
connection_manager()
525-
.download_recursive_with_progress(
526+
.download_file_with_progress(
526527
&device_id,
527528
storage_id,
528529
&mtp_path,
529530
&local_dest,
530-
&|bytes_done, bytes_total| progress.call(bytes_done, bytes_total),
531+
None,
532+
&operation_id,
533+
Some(&|bytes_done, bytes_total| progress.call(bytes_done, bytes_total)),
531534
)
532535
.await
533536
})
537+
.map(|result| result.bytes_transferred)
534538
.map_err(map_mtp_error)
535539
}
536540

@@ -549,13 +553,15 @@ impl Volume for MtpVolume {
549553
);
550554

551555
let handle = tokio::runtime::Handle::current();
556+
let operation_id = format!("export-{}", uuid::Uuid::new_v4());
552557

553558
handle
554559
.block_on(async move {
555560
connection_manager()
556-
.download_recursive(&device_id, storage_id, &mtp_path, &local_dest)
561+
.download_file(&device_id, storage_id, &mtp_path, &local_dest, None, &operation_id)
557562
.await
558563
})
564+
.map(|result| result.bytes_transferred)
559565
.map_err(map_mtp_error)
560566
}
561567

apps/desktop/src-tauri/src/mtp/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ On Linux, users may need udev rules for USB device permissions (see `resources/9
2020
| `connection/directory_ops.rs` | `list_directory()` (with lock-contention logging), `resolve_path_to_handle()` (cache-only) |
2121
| `connection/file_ops.rs` | `download_file()`, `upload_file()` — emit `mtp-transfer-progress` Tauri events |
2222
| `connection/mutation_ops.rs` | `delete()` (recursive, children-first), `create_folder()`, `rename()`, `move_object()` — no copy+delete fallback |
23-
| `connection/bulk_ops.rs` | `scan_for_copy()`, `download_recursive()`, `upload_recursive()` — use `Box::pin` for async recursion |
23+
| `connection/bulk_ops.rs` | `scan_for_copy()`, `upload_recursive()` — use `Box::pin` for async recursion |
2424
| `virtual_device.rs` | Virtual MTP device for E2E testing; creates backing dirs + registers device via `mtp-rs`. Gated behind `virtual-mtp` feature. Run with: `cd apps/desktop && pnpm tauri dev -c src-tauri/tauri.dev.json --features virtual-mtp` |
2525

2626
## Architecture / data flow

apps/desktop/src-tauri/src/mtp/connection/bulk_ops.rs

Lines changed: 0 additions & 290 deletions
Original file line numberDiff line numberDiff line change
@@ -144,296 +144,6 @@ impl MtpConnectionManager {
144144
})
145145
}
146146

147-
/// Downloads a file or directory recursively from the MTP device to a local path.
148-
///
149-
/// # Arguments
150-
///
151-
/// * `device_id` - The connected device ID
152-
/// * `storage_id` - The storage ID within the device
153-
/// * `object_path` - Virtual path on the device to download
154-
/// * `local_dest` - Local destination path
155-
///
156-
/// # Returns
157-
///
158-
/// Total bytes transferred.
159-
pub async fn download_recursive(
160-
&self,
161-
device_id: &str,
162-
storage_id: u32,
163-
object_path: &str,
164-
local_dest: &Path,
165-
) -> Result<u64, MtpConnectionError> {
166-
debug!(
167-
"MTP download_recursive: device={}, storage={}, path={}, dest={}",
168-
device_id,
169-
storage_id,
170-
object_path,
171-
local_dest.display()
172-
);
173-
174-
// Try to list the path as a directory
175-
match self.list_directory(device_id, storage_id, object_path).await {
176-
Ok(entries) if !entries.is_empty() => {
177-
// Directory with contents — download all entries
178-
self.download_entries_recursive(device_id, storage_id, &entries, local_dest)
179-
.await
180-
}
181-
Ok(_) => {
182-
// Empty result — disambiguate file vs empty directory
183-
if self.is_file_by_parent(device_id, storage_id, object_path).await {
184-
debug!("MTP download_recursive: {} is a file, downloading", object_path);
185-
let operation_id = format!("download-{}", uuid::Uuid::new_v4());
186-
let result = self
187-
.download_file(device_id, storage_id, object_path, local_dest, None, &operation_id)
188-
.await?;
189-
Ok(result.bytes_transferred)
190-
} else {
191-
debug!("MTP download_recursive: {} is an empty directory", object_path);
192-
tokio::fs::create_dir_all(local_dest)
193-
.await
194-
.map_err(|e| MtpConnectionError::Other {
195-
device_id: device_id.to_string(),
196-
message: format!("Failed to create local directory: {}", e),
197-
})?;
198-
Ok(0)
199-
}
200-
}
201-
Err(e) => {
202-
// list_directory failed — check if it's a file
203-
debug!(
204-
"MTP download_recursive: list failed for '{}', checking if it's a file: {:?}",
205-
object_path, e
206-
);
207-
if self.is_file_by_parent(device_id, storage_id, object_path).await {
208-
debug!("MTP download_recursive: {} is a file, downloading", object_path);
209-
let operation_id = format!("download-{}", uuid::Uuid::new_v4());
210-
let result = self
211-
.download_file(device_id, storage_id, object_path, local_dest, None, &operation_id)
212-
.await?;
213-
Ok(result.bytes_transferred)
214-
} else {
215-
Err(e)
216-
}
217-
}
218-
}
219-
}
220-
221-
/// Downloads a list of already-known directory entries to a local path.
222-
///
223-
/// Like `scan_entries_recursive`, this takes entries from a parent listing,
224-
/// so files are handled directly and directories get one `list_directory` call each.
225-
async fn download_entries_recursive(
226-
&self,
227-
device_id: &str,
228-
storage_id: u32,
229-
entries: &[FileEntry],
230-
local_dest: &Path,
231-
) -> Result<u64, MtpConnectionError> {
232-
tokio::fs::create_dir_all(local_dest)
233-
.await
234-
.map_err(|e| MtpConnectionError::Other {
235-
device_id: device_id.to_string(),
236-
message: format!("Failed to create local directory: {}", e),
237-
})?;
238-
239-
let mut total_bytes = 0u64;
240-
241-
for entry in entries {
242-
let child_dest = local_dest.join(&entry.name);
243-
244-
if entry.is_directory {
245-
let children = self.list_directory(device_id, storage_id, &entry.path).await?;
246-
if children.is_empty() {
247-
tokio::fs::create_dir_all(&child_dest)
248-
.await
249-
.map_err(|e| MtpConnectionError::Other {
250-
device_id: device_id.to_string(),
251-
message: format!("Failed to create local directory: {}", e),
252-
})?;
253-
} else {
254-
let bytes =
255-
Box::pin(self.download_entries_recursive(device_id, storage_id, &children, &child_dest))
256-
.await?;
257-
total_bytes += bytes;
258-
}
259-
} else {
260-
let operation_id = format!("download-{}", uuid::Uuid::new_v4());
261-
let result = self
262-
.download_file(device_id, storage_id, &entry.path, &child_dest, None, &operation_id)
263-
.await?;
264-
total_bytes += result.bytes_transferred;
265-
}
266-
}
267-
268-
debug!(
269-
"MTP download_entries_recursive: directory {} complete, {} bytes",
270-
local_dest.display(),
271-
total_bytes
272-
);
273-
Ok(total_bytes)
274-
}
275-
276-
/// Downloads a file or directory recursively with a progress/cancellation callback.
277-
///
278-
/// Same as `download_recursive` but calls `on_progress(bytes_done, bytes_total)` per chunk.
279-
/// Returns `ControlFlow::Break(())` from the callback to cancel.
280-
pub async fn download_recursive_with_progress(
281-
&self,
282-
device_id: &str,
283-
storage_id: u32,
284-
object_path: &str,
285-
local_dest: &Path,
286-
on_progress: &(dyn Fn(u64, u64) -> std::ops::ControlFlow<()> + Send + Sync),
287-
) -> Result<u64, MtpConnectionError> {
288-
debug!(
289-
"MTP download_recursive_with_progress: device={}, storage={}, path={}, dest={}",
290-
device_id,
291-
storage_id,
292-
object_path,
293-
local_dest.display()
294-
);
295-
296-
match self.list_directory(device_id, storage_id, object_path).await {
297-
Ok(entries) if !entries.is_empty() => {
298-
self.download_entries_recursive_with_progress(device_id, storage_id, &entries, local_dest, on_progress)
299-
.await
300-
}
301-
Ok(_) => {
302-
if self.is_file_by_parent(device_id, storage_id, object_path).await {
303-
let operation_id = format!("download-{}", uuid::Uuid::new_v4());
304-
let result = self
305-
.download_file_with_progress(
306-
device_id,
307-
storage_id,
308-
object_path,
309-
local_dest,
310-
None,
311-
&operation_id,
312-
Some(on_progress),
313-
)
314-
.await?;
315-
Ok(result.bytes_transferred)
316-
} else {
317-
tokio::fs::create_dir_all(local_dest)
318-
.await
319-
.map_err(|e| MtpConnectionError::Other {
320-
device_id: device_id.to_string(),
321-
message: format!("Failed to create local directory: {}", e),
322-
})?;
323-
Ok(0)
324-
}
325-
}
326-
Err(e) => {
327-
if self.is_file_by_parent(device_id, storage_id, object_path).await {
328-
let operation_id = format!("download-{}", uuid::Uuid::new_v4());
329-
let result = self
330-
.download_file_with_progress(
331-
device_id,
332-
storage_id,
333-
object_path,
334-
local_dest,
335-
None,
336-
&operation_id,
337-
Some(on_progress),
338-
)
339-
.await?;
340-
Ok(result.bytes_transferred)
341-
} else {
342-
Err(e)
343-
}
344-
}
345-
}
346-
}
347-
348-
/// Downloads directory entries recursively with a progress/cancellation callback.
349-
async fn download_entries_recursive_with_progress(
350-
&self,
351-
device_id: &str,
352-
storage_id: u32,
353-
entries: &[FileEntry],
354-
local_dest: &Path,
355-
on_progress: &(dyn Fn(u64, u64) -> std::ops::ControlFlow<()> + Send + Sync),
356-
) -> Result<u64, MtpConnectionError> {
357-
tokio::fs::create_dir_all(local_dest)
358-
.await
359-
.map_err(|e| MtpConnectionError::Other {
360-
device_id: device_id.to_string(),
361-
message: format!("Failed to create local directory: {}", e),
362-
})?;
363-
364-
let mut total_bytes = 0u64;
365-
366-
for entry in entries {
367-
let child_dest = local_dest.join(&entry.name);
368-
369-
if entry.is_directory {
370-
let children = self.list_directory(device_id, storage_id, &entry.path).await?;
371-
if children.is_empty() {
372-
tokio::fs::create_dir_all(&child_dest)
373-
.await
374-
.map_err(|e| MtpConnectionError::Other {
375-
device_id: device_id.to_string(),
376-
message: format!("Failed to create local directory: {}", e),
377-
})?;
378-
} else {
379-
let bytes = Box::pin(self.download_entries_recursive_with_progress(
380-
device_id,
381-
storage_id,
382-
&children,
383-
&child_dest,
384-
on_progress,
385-
))
386-
.await?;
387-
total_bytes += bytes;
388-
}
389-
} else {
390-
let operation_id = format!("download-{}", uuid::Uuid::new_v4());
391-
let result = self
392-
.download_file_with_progress(
393-
device_id,
394-
storage_id,
395-
&entry.path,
396-
&child_dest,
397-
None,
398-
&operation_id,
399-
Some(on_progress),
400-
)
401-
.await?;
402-
total_bytes += result.bytes_transferred;
403-
}
404-
}
405-
406-
debug!(
407-
"MTP download_entries_recursive_with_progress: directory {} complete, {} bytes",
408-
local_dest.display(),
409-
total_bytes
410-
);
411-
Ok(total_bytes)
412-
}
413-
414-
/// Checks if a path is a file by looking it up in its parent directory listing.
415-
async fn is_file_by_parent(&self, device_id: &str, storage_id: u32, path: &str) -> bool {
416-
let path_buf = normalize_mtp_path(path);
417-
let Some(parent) = path_buf.parent() else {
418-
return false;
419-
};
420-
let Some(name) = path_buf.file_name().and_then(|n| n.to_str()) else {
421-
return false;
422-
};
423-
424-
if let Ok(parent_entries) = self
425-
.list_directory(device_id, storage_id, &parent.to_string_lossy())
426-
.await
427-
{
428-
parent_entries
429-
.iter()
430-
.find(|e| e.name == name)
431-
.is_some_and(|e| !e.is_directory)
432-
} else {
433-
false
434-
}
435-
}
436-
437147
/// Uploads a file or directory from local filesystem to MTP device recursively.
438148
///
439149
/// If the source is a directory, creates the directory on the device and

apps/desktop/src-tauri/src/mtp/connection/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ impl MtpConnectionManager {
494494
// - event_loop.rs: start_event_loop, stop_event_loop, event handling
495495
// - file_ops.rs: download_file, upload_file, open_download_stream, upload_from_chunks
496496
// - mutation_ops.rs: delete_object, create_folder, rename_object, move_object
497-
// - bulk_ops.rs: scan_for_copy, download_recursive, upload_recursive
497+
// - bulk_ops.rs: scan_for_copy, upload_recursive
498498

499499
/// Global connection manager instance.
500500
static CONNECTION_MANAGER: LazyLock<MtpConnectionManager> = LazyLock::new(MtpConnectionManager::new);

0 commit comments

Comments
 (0)