Skip to main content

mtp_rs/mtp/backend/
mod.rs

1//! The backend seam for the high-level [`crate::mtp`] API.
2//!
3//! [`MtpBackend`] is the one abstraction every concrete portable-device backend implements in
4//! backend-neutral vocabulary (neutral [`crate::mtp`] types and [`crate::mtp::Error`]). The
5//! PTP-over-USB backend ([`UsbBackend`]) is the sole implementation today; a Windows WPD-over-COM
6//! backend is planned (see `docs/windows-wpd-backend-plan.md`). [`crate::mtp::MtpDevice`] and
7//! [`crate::mtp::Storage`] are thin concrete façades over a `Box<dyn MtpBackend>`, so consumers
8//! never see the trait or generics.
9//!
10//! A trait (not enum dispatch) keeps each backend self-contained in its own module and makes a
11//! future backend a new file rather than edits to every method; the per-call dynamic dispatch is
12//! noise against USB/COM latency.
13
14pub(crate) mod usb;
15
16#[cfg(windows)]
17pub(crate) mod wpd;
18
19use crate::cancel::CancelToken;
20use crate::mtp::object::NewObjectInfo;
21use crate::mtp::stream::Progress;
22use crate::mtp::{
23    Capabilities, DeviceEvent, DeviceInfo, Error, ObjectHandle, ObjectInfo, StorageId, StorageInfo,
24    UploadError,
25};
26use async_trait::async_trait;
27use bytes::Bytes;
28use futures::Stream;
29use std::ops::ControlFlow;
30use std::pin::Pin;
31use std::time::Duration;
32
33/// Selects which backend [`MtpDeviceBuilder`](crate::mtp::MtpDeviceBuilder) opens.
34///
35/// `Auto` (the default) picks per platform: on Windows it prefers the WPD backend (phones are bound
36/// to the WPD driver, not WinUSB), falling back to PTP-over-USB only if no WPD device is present;
37/// on other platforms it uses USB. `Usb` forces PTP-over-USB (e.g. a Zadig/WinUSB-bound camera on
38/// Windows); `Wpd` forces Windows WPD-over-COM (and errors as unsupported off Windows).
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum Backend {
41    /// Platform default: Windows → WPD then USB; elsewhere → USB.
42    #[default]
43    Auto,
44    /// Force PTP-over-USB.
45    Usb,
46    /// Force Windows WPD-over-COM.
47    Wpd,
48}
49
50/// Which bytes of an object a download should cover.
51///
52/// Used by the backend's download / read-range primitives to express whole-file, resume, and
53/// bounded-window reads with one type. The façade's download conveniences all desugar to this.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum ByteRange {
56    /// The whole object, `[0, size)`.
57    Full,
58    /// From `offset` to end-of-file, `[offset, size)`.
59    From(u64),
60    /// A bounded slice `[offset, offset + len)` (clamped to the object size by the backend).
61    Range {
62        /// Start byte offset.
63        offset: u64,
64        /// Number of bytes.
65        len: u64,
66    },
67}
68
69impl ByteRange {
70    /// The starting byte offset of this range.
71    #[must_use]
72    pub(crate) fn offset(self) -> u64 {
73        match self {
74            ByteRange::Full => 0,
75            ByteRange::From(offset) | ByteRange::Range { offset, .. } => offset,
76        }
77    }
78}
79
80/// A progress callback for uploads. Returning [`ControlFlow::Break`] cancels the transfer.
81///
82/// Lifetime-parameterized (not `'static`) so a consumer can pass a callback that borrows local
83/// state — the upload is awaited to completion within the call, so the borrow need only outlive
84/// that call.
85pub(crate) type ProgressFn<'a> = Box<dyn FnMut(Progress) -> ControlFlow<()> + Send + 'a>;
86
87/// A boxed, backend-neutral stream of upload data chunks.
88pub(crate) type UploadStream<'a> =
89    Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send + 'a>>;
90
91/// One in-progress streaming download, holding whatever resource the backend needs for the whole
92/// transfer (the USB backend holds the PTP session open). The façade's [`crate::mtp::FileDownload`]
93/// wraps this; consumers don't see it.
94#[async_trait]
95pub(crate) trait DownloadBody: Send {
96    /// The next chunk of data, or `None` at end-of-file.
97    async fn next_chunk(&mut self) -> Option<Result<Bytes, Error>>;
98
99    /// Cancel the in-flight download, releasing the backend resource and leaving the device usable
100    /// for the next operation. A no-op if already complete.
101    async fn cancel(&mut self, idle_timeout: Duration) -> Result<(), Error>;
102}
103
104/// One streaming download returned by [`MtpBackend::download`]: the full object size plus the body.
105pub(crate) struct BackendDownload {
106    /// Full object size in bytes (always the whole file, even for an offset/range read).
107    pub(crate) size: u64,
108    /// The download body.
109    pub(crate) body: Box<dyn DownloadBody>,
110}
111
112/// Whether a per-handle metadata error invalidates the whole collection or can
113/// be reported while sibling enumeration continues.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub(crate) enum ListingErrorDisposition {
116    Fatal,
117    SkipObject,
118}
119
120/// One error produced after a backend has already enumerated an object's
121/// handle. Keeping the handle and disposition internal lets the public MTP API
122/// remain backend-neutral while detailed collection callers still receive a
123/// useful diagnostic.
124#[derive(Debug)]
125pub(crate) struct BackendListingError {
126    pub(crate) handle: ObjectHandle,
127    pub(crate) source: Error,
128    pub(crate) disposition: ListingErrorDisposition,
129}
130
131impl BackendListingError {
132    pub(crate) fn fatal(handle: ObjectHandle, source: Error) -> Self {
133        Self {
134            handle,
135            source,
136            disposition: ListingErrorDisposition::Fatal,
137        }
138    }
139
140    pub(crate) fn skippable(handle: ObjectHandle, source: Error) -> Self {
141        Self {
142            handle,
143            source,
144            disposition: ListingErrorDisposition::SkipObject,
145        }
146    }
147}
148
149/// A boxed stream of object metadata, yielded by [`MtpBackend::list`].
150pub(crate) type ObjectStream =
151    Pin<Box<dyn Stream<Item = Result<ObjectInfo, BackendListingError>> + Send>>;
152
153/// One in-progress listing returned by [`MtpBackend::list`]: a known total plus the item stream.
154pub(crate) struct BackendListing {
155    /// Total number of handles the device reported (before any parent filtering).
156    pub(crate) total: usize,
157    /// The metadata stream.
158    pub(crate) items: ObjectStream,
159}
160
161/// The backend-neutral portable-device API. See the module docs.
162#[async_trait]
163pub(crate) trait MtpBackend: Send + Sync {
164    /// Cached device identity.
165    fn device_info(&self) -> &DeviceInfo;
166
167    /// What the device supports (derived per backend).
168    fn capabilities(&self) -> &Capabilities;
169
170    /// All storages on the device.
171    async fn storages(&self) -> Result<Vec<StorageInfo>, Error>;
172
173    /// Fetch info for a single storage by id.
174    async fn storage_info(&self, storage: StorageId) -> Result<StorageInfo, Error>;
175
176    /// List the children of `parent` on `storage` (cancellable, streaming).
177    async fn list(
178        &self,
179        storage: StorageId,
180        parent: Option<ObjectHandle>,
181        cancel: Option<&CancelToken>,
182    ) -> Result<BackendListing, Error>;
183
184    /// Metadata for one object (with the full >4 GB size resolved).
185    async fn object_info(&self, obj: ObjectHandle) -> Result<ObjectInfo, Error>;
186
187    /// A streaming download of `obj` over `range`. Holds the backend resource for the transfer.
188    async fn download(&self, obj: ObjectHandle, range: ByteRange)
189        -> Result<BackendDownload, Error>;
190
191    /// A single-shot buffered read of `[offset, offset+len)` (or to EOF when `len` is `None`).
192    async fn read_range(
193        &self,
194        obj: ObjectHandle,
195        offset: u64,
196        len: Option<u32>,
197    ) -> Result<Vec<u8>, Error>;
198
199    /// Fetch the thumbnail bytes for `obj`.
200    async fn thumbnail(&self, obj: ObjectHandle) -> Result<Vec<u8>, Error>;
201
202    /// Create a new object under `parent` on `storage` and stream its data.
203    async fn upload(
204        &self,
205        storage: StorageId,
206        parent: Option<ObjectHandle>,
207        info: NewObjectInfo,
208        data: UploadStream<'_>,
209        progress: Option<ProgressFn<'_>>,
210    ) -> Result<ObjectHandle, UploadError>;
211
212    /// Create a folder named `name` under `parent` on `storage`.
213    async fn create_folder(
214        &self,
215        storage: StorageId,
216        parent: Option<ObjectHandle>,
217        name: &str,
218    ) -> Result<ObjectHandle, Error>;
219
220    /// Delete `obj` (cancellable before the request is issued).
221    async fn delete(&self, obj: ObjectHandle, cancel: Option<&CancelToken>) -> Result<(), Error>;
222
223    /// Move `obj` to `new_parent` (optionally on `new_storage`).
224    async fn move_object(
225        &self,
226        obj: ObjectHandle,
227        new_parent: ObjectHandle,
228        new_storage: StorageId,
229    ) -> Result<(), Error>;
230
231    /// Copy `obj` to `new_parent` (optionally on `new_storage`), returning the copy's handle.
232    async fn copy_object(
233        &self,
234        obj: ObjectHandle,
235        new_parent: ObjectHandle,
236        new_storage: StorageId,
237    ) -> Result<ObjectHandle, Error>;
238
239    /// Rename `obj`.
240    async fn rename(&self, obj: ObjectHandle, new_name: &str) -> Result<(), Error>;
241
242    /// Await the next device event (indefinitely; wrap in a timeout).
243    async fn next_event(&self) -> Result<DeviceEvent, Error>;
244
245    /// Close the connection (best-effort). Also happens on drop of the backing resource.
246    async fn close(&self) -> Result<(), Error>;
247}