The first pure-Rust MTP/PTP library with no C dependencies.
Talk to Android phones, e-book readers incl. Kindle, and digital cameras over USB.
No libmtp, no libusb, no FFI—just async Rust built on nusb.
Why this matters:
- Cross-compile without system library headaches
- No
pkg-config, no-syscrates, no build.rs surprises - Works anywhere Rust compiles (including musl, cross-compilation targets)
- Fully async and runtime-agnostic
- Connect to devices over USB
- List, download, upload, delete, move, copy, and rename files
- Create, delete, and rename folders
- Stream large file downloads with continued progress indication
- Listen for device events (file added, storage removed, etc.)
- Also exposes a lower-level interface for PTP, so it can be used for cameras too.
- MTPZ (the DRM extension some old devices used)
- Playlists, tracks, albums, and custom operations
- Vendor-specific extensions
- Legacy Android device quirks (pre-5.0 devices)
We intentionally didn't want to support these because they're rarely needed now, and it'd be a nightmare to test. libmtp has an impressive collection of device quirks, but it's LGPL-1.1 licensed, and I wanted to do MIT/Apache-2.0 for broader access. So copying that code was also not an option.
A simple test would be this:
use mtp_rs::mtp::MtpDevice;
#[tokio::main]
async fn main() -> Result<(), mtp_rs::Error> {
// Connect to the first MTP device
let device = MtpDevice::open_first().await?;
println!("Connected to {} {}",
device.device_info().manufacturer,
device.device_info().model);
// List storages (internal storage, SD card, etc.)
for storage in device.storages().await? {
println!("{}: {:.2} GB free",
storage.info().description,
storage.info().free_space_bytes as f64 / 1e9);
// List files in root
for file in storage.list_objects(None).await? {
let icon = if file.is_folder() { "📁" } else { "📄" };
println!(" {} {}", icon, file.filename);
}
}
Ok(())
}Add to your Cargo.toml:
[dependencies]
mtp-rs = "0.1"You'll also need an async runtime. The library is runtime-agnostic, but tokio is the most common choice:
[dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }You may need udev rules to access USB devices without root. Create /etc/udev/rules.d/99-mtp.rules:
SUBSYSTEM=="usb", ATTR{idVendor}=="*", MODE="0666"
Then run sudo udevadm control --reload-rules.
It's a bit of a nightmare because macOS's built-in ptpcamerad daemon automatically claims MTP/PTP devices right on
connection, blocking other apps. This sucks because it it NOT MTP, just PTP, so Android phones, Kindles, etc.
won't be able to sync files through it, and at the same time, other apps (like potentially yours if you're looking at
this) will be unable to access the device. 🤯
One more potential offender is Android File Transfer: If installed, it spawns a process that also grabs devices. You must quit it before trying to connect to an MTP device using this (or, honestly, any) library.
Workarounds:
-
Kill loop: Run this in Terminal while using your app:
while true; do pkill -9 ptpcamerad 2>/dev/null; sleep 1; done
-
Disable
ptpcamerad: Persistent, but may break Photos.app:sudo launchctl disable system/com.apple.ptpcamerad
Other tips for app developers:
- This library provides
Error::is_exclusive_access(). Use this to detect this condition and guide users to apply one of the workarounds above. - Query IORegistry for
UsbExclusiveOwnerto show which process (pid, name) holds the device for even more helpful info - App Store sandboxed apps cannot kill processes. If your app is such, then provide the command for users to run manually. If your app isn't in the App Store, then you're in a better position and may be able to use the workarounds, BUT it's a bit murky territory with Apple.
- See Cmdr and Commander One for UX inspiration on handling this gracefully.
Should work, and no dependencies needed, but we haven't tested it.
These might come in handy:
let storage = &device.storages().await?[0];
// Find a file
let files = storage.list_objects(None).await?;
let photo = files.iter().find(|f| f.filename == "photo.jpg").unwrap();
// Download it
let data = storage.download(photo.handle).await?.collect().await?;
std::fs::write("photo.jpg", data)?;use mtp_rs::mtp::NewObjectInfo;
use bytes::Bytes;
let content = std::fs::read("document.pdf")?;
let info = NewObjectInfo::file("document.pdf", content.len() as u64);
let stream = futures::stream::iter(vec![Ok::<_, std::io::Error>(Bytes::from(content))]);
let handle = storage.upload(None, info, Box::pin(stream)).await?;
println!("Uploaded with handle {:?}", handle);let mut download = storage.download_stream(file.handle).await?;
println!("Downloading {} bytes...", download.size());
while let Some(chunk) = download.next_chunk().await {
let bytes = chunk?;
// Process bytes...
println!("{:.1}%", download.progress() * 100.0);
}loop {
match device.next_event().await {
Ok(event) => match event {
DeviceEvent::ObjectAdded { handle } => {
println!("New file: {:?}", handle);
}
DeviceEvent::StoreRemoved { storage_id } => {
println!("Storage unplugged: {:?}", storage_id);
}
_ => {}
},
Err(Error::Timeout) => continue,
Err(Error::Disconnected) => break,
Err(e) => eprintln!("Error: {}", e),
}
}The library has two layers:
This is what most people want. Friendly types, automatic session management, streaming.
MtpDevice- Connect to devices, get info, list storagesStorage- File operations (list, download, upload, delete, move, copy)DownloadStream- Streaming downloads with progressDeviceEvent- Events from the device
For when you need raw protocol access (for cameras or maybe debugging).
PtpDevice- Raw device connectionPtpSession- Manual session control, raw operationsOperationCode,ResponseCode- Protocol constants- Container types for building/parsing protocol messages
With this, you can copy stuff to/from cameras, but there are no other features like reading the battery level, trigger capture, read supported formats/sizes, etc. This is intentional, didn't want to bloat the library with camera-specific code because this is mainly for MTP and file transfer.
The library uses futures traits and is runtime-agnostic. It's tested with tokio but should work with async-std or any other runtime.
We use nusb for USB access, which is also runtime-agnostic.
| Limitation | Details |
|---|---|
| Files >4GB | Size reported as 4GB due to protocol limitation |
| Filename length | Max 254 characters |
| Non-empty folder delete | Fails; delete contents first |
| One connection per device | Can't open the same device twice |
| Upload cancellation | Partial files may remain on device |
| Recursive listing speed | Manual traversal is slower (~1 request per folder) |
Android's MTP implementation has some quirks that this library handles automatically:
- Behavior: Recursive listing broken
- What happens:
ObjectHandle::ALLreturns incomplete results (folders only, no files) - How we handle it: Auto-detected; uses manual folder traversal instead. Although, note that it takes a lot more time! Like, if the device supported this, it'd be pretty fast, while with the workaround, in the tests it took 9 minutes to list ~20k files in ~2k folders.
- What happens:
- Behavior: Can't create in root
- What happens: Creating files/folders in storage root fails with
InvalidObjectHandle - How we handle it: Use a subfolder like
Download/as the parent
- What happens: Creating files/folders in storage root fails with
- Behavior: Large responses span transfers
- What happens: Data >64KB comes in multiple USB transfers
- How we handle it: Automatically reassembled before parsing
- Behavior: Composite USB devices
- What happens: Most phones report as USB class 0 (composite)
- How we handle it: We inspect interfaces to find MTP
The library detects Android devices via the "android.com" vendor extension and applies appropriate handling automatically.
You generally don't need to worry about these details.
Tip: When uploading files, use a known folder like Download/ rather than the storage root:
// Find the Download folder
let objects = storage.list_objects(None).await?;
let download = objects.iter().find(|o| o.filename == "Download").unwrap();
// Upload to Download folder (not root)
storage.upload(Some(download.handle), file_info, data).await?;"Full support" really means "Full support, except for general Android quirks listed above".
| Device | Android | Notes |
|---|---|---|
| Google Pixel 9 Pro XL | 15 | Full support |
| Samsung Galaxy S23 Ultra (SM-S918B) | 14 | No root listing |
Samsung quirk: Samsung devices return InvalidObjectHandle when listing the root folder with handle 0.
The library automatically detects this and falls back to recursive listing with filtering. This is transparent to users.
We welcome reports of other tested devices! Please open an issue or PR with your device model, Android version, and any issues encountered.
libmtp is 20+ years old, battle-tested, and very comprehensive. libmtp-rs provides a Rust interface to it. But:
libmtpis a C library with all the FFI pain that entails- It has a massive device quirks database for hardware from 2006
- The API is synchronous and callback-heavy
- It pulls in
libusb,libudev, and other system dependencies
In contrast, mtp-rs targets modern Android devices that all behave the same way. If you need to support a weird
MP3 player from 2008, use libmtp. If you're building a modern Android sync tool, mtp-rs is a better fit.
ptp and libptp both use libusb v0.3 for USB access, which is a C dependency.
mtp-rs uses nusb instead, which is pure Rust.
Note that libptp is much more mature, though!
winmtp wraps the Windows COM API—Windows only. mtp-rs works on Linux, macOS, and Windows.
- I used Opus 4.5 extensively for this implementation. I know it's controversial these days, but the bottom line to me is that the implementation WORKS, it has a bunch of integration tests which pass, and hey, I can use it to copy data to/from my phone and other phones and I can display async progress and I don't need to rely on C libraries. So no hate, please. If you dislike or distrust AI-gen code, use the alternatives listed above (if you can live with the libmtp dependency), handcraft your own Rust implementation, or fork this repo and add your human thing and use it. PRs are also welcome.
- For the protocol spec, I tried to use usb.org's Media Transfer Protocol v.1.1 Spec but it was a pain to get AI agents to work from it, so I've converted it to Markdown. You can find it here: https://github.com/vdavid/mtp-v1_1-spec-md I've also shared it back with the USB.org team, so they might link it on the official page.
See CONTRIBUTING.md for guidelines.
MIT OR Apache-2.0, at your option.