Skip to main content

mtp_rs/mtp/
storage.rs

1//! Storage operations.
2
3use crate::mtp::object::NewObjectInfo;
4use crate::mtp::stream::{FileDownload, Progress};
5use crate::ptp::{ObjectHandle, ObjectInfo, StorageId, StorageInfo};
6use crate::Error;
7use bytes::Bytes;
8use futures::Stream;
9use std::ops::ControlFlow;
10use std::sync::Arc;
11
12use super::device::MtpDeviceInner;
13
14/// A storage location on an MTP device.
15///
16/// `Storage` holds an `Arc<MtpDeviceInner>` so it can outlive the original
17/// `MtpDevice` and be used from multiple tasks.
18pub struct Storage {
19    inner: Arc<MtpDeviceInner>,
20    id: StorageId,
21    info: StorageInfo,
22}
23
24impl Storage {
25    /// Create a new Storage (internal).
26    pub(crate) fn new(inner: Arc<MtpDeviceInner>, id: StorageId, info: StorageInfo) -> Self {
27        Self { inner, id, info }
28    }
29
30    #[must_use]
31    pub fn id(&self) -> StorageId {
32        self.id
33    }
34
35    /// Storage information (cached, call refresh() to update).
36    #[must_use]
37    pub fn info(&self) -> &StorageInfo {
38        &self.info
39    }
40
41    /// Refresh storage info from device (updates free space, etc.).
42    pub async fn refresh(&mut self) -> Result<(), Error> {
43        self.info = self.inner.session.get_storage_info(self.id).await?;
44        Ok(())
45    }
46
47    /// List objects in a folder (None = root).
48    ///
49    /// This method handles various device quirks:
50    /// - Android devices: parent=0 returns ALL objects, so we use parent=0xFFFFFFFF instead
51    /// - Samsung devices: return InvalidObjectHandle for parent=0, so we fall back to recursive
52    /// - Fuji devices: return all objects for root, so we filter by parent handle
53    pub async fn list_objects(
54        &self,
55        parent: Option<ObjectHandle>,
56    ) -> Result<Vec<ObjectInfo>, Error> {
57        // Android quirk: When listing root (parent=None/0), Android returns ALL objects
58        // on the device instead of just root-level objects. This makes listing extremely slow.
59        // Counter-intuitively, using parent=0xFFFFFFFF (ObjectHandle::ALL) returns the
60        // actual root-level objects on Android devices.
61        let effective_parent = if parent.is_none() && self.inner.is_android() {
62            Some(ObjectHandle::ALL)
63        } else {
64            parent
65        };
66
67        let result = self
68            .inner
69            .session
70            .get_object_handles(self.id, None, effective_parent)
71            .await;
72
73        let handles = match result {
74            Ok(h) => h,
75            Err(Error::Protocol {
76                code: crate::ptp::ResponseCode::InvalidObjectHandle,
77                ..
78            }) if parent.is_none() => {
79                // Samsung fallback: use recursive listing and filter to root items
80                return self.list_objects_samsung_fallback().await;
81            }
82            Err(e) => return Err(e),
83        };
84
85        let mut objects = Vec::with_capacity(handles.len());
86        let expected_parent = parent.unwrap_or(ObjectHandle::ROOT);
87
88        for handle in handles {
89            let mut info = self.inner.session.get_object_info(handle).await?;
90            info.handle = handle;
91
92            // Filter: only include objects whose parent matches the requested parent.
93            // Some devices (like Fuji X-T4) return all objects when asked for root,
94            // not just root-level objects.
95            // For Android root listing (where we used ALL), accept both 0 and 0xFFFFFFFF as parent.
96            let parent_matches = if parent.is_none() && self.inner.is_android() {
97                info.parent.0 == 0 || info.parent.0 == 0xFFFFFFFF
98            } else {
99                info.parent == expected_parent
100            };
101
102            if parent_matches {
103                objects.push(info);
104            }
105        }
106        Ok(objects)
107    }
108
109    /// Samsung fallback: list all objects recursively and filter to root level.
110    async fn list_objects_samsung_fallback(&self) -> Result<Vec<ObjectInfo>, Error> {
111        let handles = self
112            .inner
113            .session
114            .get_object_handles(self.id, None, Some(ObjectHandle::ALL))
115            .await?;
116
117        let mut objects = Vec::new();
118        for handle in handles {
119            let mut info = self.inner.session.get_object_info(handle).await?;
120            info.handle = handle;
121
122            // Root items have parent 0 or 0xFFFFFFFF (depending on device)
123            if info.parent.0 == 0 || info.parent.0 == 0xFFFFFFFF {
124                objects.push(info);
125            }
126        }
127        Ok(objects)
128    }
129
130    /// List objects recursively.
131    ///
132    /// This method automatically detects Android devices and uses manual traversal
133    /// for them, since Android's MTP implementation doesn't support the native
134    /// `ObjectHandle::ALL` recursive listing.
135    ///
136    /// For non-Android devices, it tries native recursive listing first and falls
137    /// back to manual traversal if the results look incomplete.
138    pub async fn list_objects_recursive(
139        &self,
140        parent: Option<ObjectHandle>,
141    ) -> Result<Vec<ObjectInfo>, Error> {
142        if self.inner.is_android() {
143            return self.list_objects_recursive_manual(parent).await;
144        }
145
146        let native_result = self.list_objects_recursive_native(parent).await?;
147
148        // Heuristic: if we only got folders and no files, native listing
149        // probably didn't work - fall back to manual traversal
150        let has_files = native_result.iter().any(|o| o.is_file());
151        if !native_result.is_empty() && !has_files {
152            return self.list_objects_recursive_manual(parent).await;
153        }
154
155        Ok(native_result)
156    }
157
158    /// List objects recursively using native MTP recursive listing.
159    pub async fn list_objects_recursive_native(
160        &self,
161        parent: Option<ObjectHandle>,
162    ) -> Result<Vec<ObjectInfo>, Error> {
163        let recursive_parent = if parent.is_none() {
164            Some(ObjectHandle::ALL)
165        } else {
166            parent
167        };
168
169        let handles = self
170            .inner
171            .session
172            .get_object_handles(self.id, None, recursive_parent)
173            .await?;
174
175        let mut objects = Vec::with_capacity(handles.len());
176        for handle in handles {
177            let mut info = self.inner.session.get_object_info(handle).await?;
178            info.handle = handle;
179            objects.push(info);
180        }
181        Ok(objects)
182    }
183
184    /// List objects recursively using manual folder traversal.
185    pub async fn list_objects_recursive_manual(
186        &self,
187        parent: Option<ObjectHandle>,
188    ) -> Result<Vec<ObjectInfo>, Error> {
189        let mut result = Vec::new();
190        let mut folders_to_visit = vec![parent];
191
192        while let Some(current_parent) = folders_to_visit.pop() {
193            let objects = self.list_objects(current_parent).await?;
194
195            for obj in objects {
196                if obj.is_folder() {
197                    folders_to_visit.push(Some(obj.handle));
198                }
199                result.push(obj);
200            }
201        }
202
203        Ok(result)
204    }
205
206    /// Get object metadata by handle.
207    pub async fn get_object_info(&self, handle: ObjectHandle) -> Result<ObjectInfo, Error> {
208        let mut info = self.inner.session.get_object_info(handle).await?;
209        info.handle = handle;
210        Ok(info)
211    }
212
213    // =========================================================================
214    // Download operations
215    // =========================================================================
216
217    /// Download a file and return all bytes.
218    ///
219    /// For small to medium files where you want all the data in memory.
220    /// For large files or streaming to disk, use [`download_stream()`](Self::download_stream).
221    pub async fn download(&self, handle: ObjectHandle) -> Result<Vec<u8>, Error> {
222        self.inner.session.get_object(handle).await
223    }
224
225    /// Download a partial file (byte range).
226    pub async fn download_partial(
227        &self,
228        handle: ObjectHandle,
229        offset: u64,
230        size: u32,
231    ) -> Result<Vec<u8>, Error> {
232        self.inner
233            .session
234            .get_partial_object(handle, offset, size)
235            .await
236    }
237
238    pub async fn download_thumbnail(&self, handle: ObjectHandle) -> Result<Vec<u8>, Error> {
239        self.inner.session.get_thumb(handle).await
240    }
241
242    /// Download a file as a stream (true USB streaming).
243    ///
244    /// Unlike [`download()`](Self::download), this method yields data chunks
245    /// directly from USB as they arrive, without buffering the entire file
246    /// in memory. Ideal for large files or when piping data to disk.
247    ///
248    /// # Important
249    ///
250    /// The MTP session is locked while the download is active. You must consume
251    /// the entire download (or drop it) before calling other storage methods.
252    ///
253    /// # Example
254    ///
255    /// ```rust,ignore
256    /// let mut download = storage.download_stream(handle).await?;
257    /// println!("Downloading {} bytes...", download.size());
258    ///
259    /// let mut file = tokio::fs::File::create("output.bin").await?;
260    /// while let Some(chunk) = download.next_chunk().await {
261    ///     let bytes = chunk?;
262    ///     file.write_all(&bytes).await?;
263    ///     println!("Progress: {:.1}%", download.progress() * 100.0);
264    /// }
265    /// ```
266    pub async fn download_stream(&self, handle: ObjectHandle) -> Result<FileDownload, Error> {
267        let info = self.get_object_info(handle).await?;
268        let size = info.size;
269
270        let stream = self
271            .inner
272            .session
273            .execute_with_receive_stream(crate::ptp::OperationCode::GetObject, &[handle.0])
274            .await?;
275
276        Ok(FileDownload::new(size, stream))
277    }
278
279    // =========================================================================
280    // Upload operations
281    // =========================================================================
282
283    /// Upload a file from a stream.
284    ///
285    /// The stream is consumed and all data is buffered before sending
286    /// (MTP protocol requires knowing the total size upfront).
287    ///
288    /// # Arguments
289    ///
290    /// * `parent` - Parent folder handle (None for root)
291    /// * `info` - Object metadata including filename and size
292    /// * `data` - Stream of data chunks to upload
293    pub async fn upload<S>(
294        &self,
295        parent: Option<ObjectHandle>,
296        info: NewObjectInfo,
297        data: S,
298    ) -> Result<ObjectHandle, Error>
299    where
300        S: Stream<Item = Result<Bytes, std::io::Error>> + Unpin,
301    {
302        self.upload_with_progress(parent, info, data, |_| ControlFlow::Continue(()))
303            .await
304    }
305
306    /// Upload a file with progress callback.
307    ///
308    /// Progress is reported as data is read from the stream. Return
309    /// `ControlFlow::Break(())` from the callback to cancel the upload.
310    pub async fn upload_with_progress<S, F>(
311        &self,
312        parent: Option<ObjectHandle>,
313        info: NewObjectInfo,
314        mut data: S,
315        mut on_progress: F,
316    ) -> Result<ObjectHandle, Error>
317    where
318        S: Stream<Item = Result<Bytes, std::io::Error>> + Unpin,
319        F: FnMut(Progress) -> ControlFlow<()>,
320    {
321        use futures::StreamExt;
322
323        let total_size = info.size;
324        let mut buffer = Vec::with_capacity(total_size as usize);
325        let mut bytes_received = 0u64;
326
327        while let Some(chunk) = data.next().await {
328            let chunk = chunk.map_err(Error::Io)?;
329            bytes_received += chunk.len() as u64;
330            buffer.extend_from_slice(&chunk);
331
332            let progress = Progress {
333                bytes_transferred: bytes_received,
334                total_bytes: Some(total_size),
335            };
336
337            if let ControlFlow::Break(()) = on_progress(progress) {
338                return Err(Error::Cancelled);
339            }
340        }
341
342        let object_info = info.to_object_info();
343        let parent_handle = parent.unwrap_or(ObjectHandle::ROOT);
344        let (_, _, handle) = self
345            .inner
346            .session
347            .send_object_info(self.id, parent_handle, &object_info)
348            .await?;
349
350        self.inner.session.send_object(&buffer).await?;
351
352        Ok(handle)
353    }
354
355    // =========================================================================
356    // Folder and object management
357    // =========================================================================
358
359    pub async fn create_folder(
360        &self,
361        parent: Option<ObjectHandle>,
362        name: &str,
363    ) -> Result<ObjectHandle, Error> {
364        let info = NewObjectInfo::folder(name);
365        let object_info = info.to_object_info();
366        let parent_handle = parent.unwrap_or(ObjectHandle::ROOT);
367
368        let (_, _, handle) = self
369            .inner
370            .session
371            .send_object_info(self.id, parent_handle, &object_info)
372            .await?;
373
374        Ok(handle)
375    }
376
377    pub async fn delete(&self, handle: ObjectHandle) -> Result<(), Error> {
378        self.inner.session.delete_object(handle).await
379    }
380
381    /// Move an object to a different folder.
382    pub async fn move_object(
383        &self,
384        handle: ObjectHandle,
385        new_parent: ObjectHandle,
386        new_storage: Option<StorageId>,
387    ) -> Result<(), Error> {
388        let storage = new_storage.unwrap_or(self.id);
389        self.inner
390            .session
391            .move_object(handle, storage, new_parent)
392            .await
393    }
394
395    pub async fn copy_object(
396        &self,
397        handle: ObjectHandle,
398        new_parent: ObjectHandle,
399        new_storage: Option<StorageId>,
400    ) -> Result<ObjectHandle, Error> {
401        let storage = new_storage.unwrap_or(self.id);
402        self.inner
403            .session
404            .copy_object(handle, storage, new_parent)
405            .await
406    }
407
408    /// Rename an object (file or folder).
409    ///
410    /// Not all devices support renaming. Use `MtpDevice::supports_rename()`
411    /// to check if this operation is available.
412    pub async fn rename(&self, handle: ObjectHandle, new_name: &str) -> Result<(), Error> {
413        self.inner.session.rename_object(handle, new_name).await
414    }
415}