|
3 | 3 | //! Wraps MTP device storage as a Volume, enabling MTP browsing through |
4 | 4 | //! the standard file listing pipeline (same icons, sorting, view modes as local files). |
5 | 5 |
|
6 | | -use super::{ConflictInfo, CopyScanResult, SourceItemInfo, SpaceInfo, Volume, VolumeError}; |
| 6 | +use super::{ConflictInfo, CopyScanResult, SourceItemInfo, SpaceInfo, Volume, VolumeError, VolumeReadStream}; |
7 | 7 | use crate::file_system::FileEntry; |
8 | 8 | use crate::mtp::connection::{MtpConnectionError, connection_manager}; |
9 | 9 | use log::debug; |
| 10 | +use mtp_rs::FileDownload; |
10 | 11 | use std::path::{Path, PathBuf}; |
11 | 12 |
|
12 | 13 | /// A volume backed by an MTP device storage. |
@@ -380,6 +381,109 @@ impl Volume for MtpVolume { |
380 | 381 | }) |
381 | 382 | .map_err(map_mtp_error) |
382 | 383 | } |
| 384 | + |
| 385 | + fn supports_streaming(&self) -> bool { |
| 386 | + true |
| 387 | + } |
| 388 | + |
| 389 | + fn open_read_stream(&self, path: &Path) -> Result<Box<dyn VolumeReadStream>, VolumeError> { |
| 390 | + let mtp_path = self.to_mtp_path(path); |
| 391 | + let device_id = self.device_id.clone(); |
| 392 | + let storage_id = self.storage_id; |
| 393 | + |
| 394 | + let handle = tokio::runtime::Handle::current(); |
| 395 | + |
| 396 | + // Get the file download stream from connection manager |
| 397 | + let (download, total_size) = handle |
| 398 | + .block_on(async { |
| 399 | + connection_manager() |
| 400 | + .open_download_stream(&device_id, storage_id, &mtp_path) |
| 401 | + .await |
| 402 | + }) |
| 403 | + .map_err(map_mtp_error)?; |
| 404 | + |
| 405 | + Ok(Box::new(MtpReadStream { |
| 406 | + handle, |
| 407 | + download: Some(download), |
| 408 | + total_size, |
| 409 | + bytes_read: 0, |
| 410 | + })) |
| 411 | + } |
| 412 | + |
| 413 | + fn write_from_stream( |
| 414 | + &self, |
| 415 | + dest: &Path, |
| 416 | + size: u64, |
| 417 | + mut stream: Box<dyn VolumeReadStream>, |
| 418 | + ) -> Result<u64, VolumeError> { |
| 419 | + let dest_folder = dest.parent().map(|p| self.to_mtp_path(p)).unwrap_or_default(); |
| 420 | + let filename = dest |
| 421 | + .file_name() |
| 422 | + .and_then(|n| n.to_str()) |
| 423 | + .ok_or_else(|| VolumeError::IoError("Invalid filename".into()))? |
| 424 | + .to_string(); |
| 425 | + |
| 426 | + let device_id = self.device_id.clone(); |
| 427 | + let storage_id = self.storage_id; |
| 428 | + |
| 429 | + // IMPORTANT: Collect all chunks BEFORE entering block_on to avoid nested runtime error. |
| 430 | + // MtpReadStream::next_chunk() uses block_on internally, so we can't call it from |
| 431 | + // within another block_on (which upload_from_stream would do). |
| 432 | + let mut chunks: Vec<bytes::Bytes> = Vec::new(); |
| 433 | + while let Some(result) = stream.next_chunk() { |
| 434 | + let data = result?; |
| 435 | + chunks.push(bytes::Bytes::from(data)); |
| 436 | + } |
| 437 | + |
| 438 | + let handle = tokio::runtime::Handle::current(); |
| 439 | + |
| 440 | + handle |
| 441 | + .block_on(async { |
| 442 | + connection_manager() |
| 443 | + .upload_from_chunks(&device_id, storage_id, &dest_folder, &filename, size, chunks) |
| 444 | + .await |
| 445 | + }) |
| 446 | + .map_err(map_mtp_error) |
| 447 | + } |
| 448 | +} |
| 449 | + |
| 450 | +/// Streaming reader for MTP files. |
| 451 | +/// |
| 452 | +/// Wraps the mtp-rs FileDownload to provide sync iteration. |
| 453 | +pub struct MtpReadStream { |
| 454 | + /// Tokio runtime handle for blocking on async operations. |
| 455 | + handle: tokio::runtime::Handle, |
| 456 | + /// The underlying async download (wrapped in Option for take semantics). |
| 457 | + download: Option<FileDownload>, |
| 458 | + /// Total file size. |
| 459 | + total_size: u64, |
| 460 | + /// Bytes read so far. |
| 461 | + bytes_read: u64, |
| 462 | +} |
| 463 | + |
| 464 | +impl VolumeReadStream for MtpReadStream { |
| 465 | + fn next_chunk(&mut self) -> Option<Result<Vec<u8>, VolumeError>> { |
| 466 | + let download = self.download.as_mut()?; |
| 467 | + |
| 468 | + self.handle.block_on(async { |
| 469 | + match download.next_chunk().await { |
| 470 | + Some(Ok(bytes)) => { |
| 471 | + self.bytes_read += bytes.len() as u64; |
| 472 | + Some(Ok(bytes.to_vec())) |
| 473 | + } |
| 474 | + Some(Err(e)) => Some(Err(VolumeError::IoError(e.to_string()))), |
| 475 | + None => None, |
| 476 | + } |
| 477 | + }) |
| 478 | + } |
| 479 | + |
| 480 | + fn total_size(&self) -> u64 { |
| 481 | + self.total_size |
| 482 | + } |
| 483 | + |
| 484 | + fn bytes_read(&self) -> u64 { |
| 485 | + self.bytes_read |
| 486 | + } |
383 | 487 | } |
384 | 488 |
|
385 | 489 | /// Maps MTP connection errors to Volume errors. |
@@ -461,4 +565,11 @@ mod tests { |
461 | 565 | let vol = MtpVolume::new("mtp-20-5", 65537, "Test"); |
462 | 566 | assert!(!vol.supports_watching()); |
463 | 567 | } |
| 568 | + |
| 569 | + #[test] |
| 570 | + fn test_supports_streaming_returns_true() { |
| 571 | + // MTP volumes support streaming for direct MTP-to-MTP transfers. |
| 572 | + let vol = MtpVolume::new("mtp-20-5", 65537, "Test"); |
| 573 | + assert!(vol.supports_streaming()); |
| 574 | + } |
464 | 575 | } |
0 commit comments