Skip to content
/ mtp-rs Public

Talk to MTP/PTP devices in pure Rust. No libmtp, no FFI, just async USB (uses nusb). Async streaming uploads/downloads, device events, two-level API for Android phones or raw camera access.

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

vdavid/mtp-rs

mtp-rs

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 -sys crates, no build.rs surprises
  • Works anywhere Rust compiles (including musl, cross-compilation targets)
  • Fully async and runtime-agnostic

What it does

  • 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.

What it doesn't do

  • 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.

Quick start

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(())
}

Installation

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"] }

Platform notes

Linux

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.

macOS

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:

  1. Kill loop: Run this in Terminal while using your app:

    while true; do pkill -9 ptpcamerad 2>/dev/null; sleep 1; done
  2. 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 UsbExclusiveOwner to 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.

Windows

Should work, and no dependencies needed, but we haven't tested it.

Examples

These might come in handy:

Download a file

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)?;

Upload a file

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);

Download with progress

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);
}

Listen for events

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),
    }
}

API overview

The library has two layers:

High-level API (mtp::)

This is what most people want. Friendly types, automatic session management, streaming.

  • MtpDevice - Connect to devices, get info, list storages
  • Storage - File operations (list, download, upload, delete, move, copy)
  • DownloadStream - Streaming downloads with progress
  • DeviceEvent - Events from the device

Low-level API (ptp::)

For when you need raw protocol access (for cameras or maybe debugging).

  • PtpDevice - Raw device connection
  • PtpSession - Manual session control, raw operations
  • OperationCode, 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.

Runtime compatibility

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.

Known limitations

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 weirdnesses

Android's MTP implementation has some quirks that this library handles automatically:

  • Behavior: Recursive listing broken
    • What happens: ObjectHandle::ALL returns 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.
  • 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
  • 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?;

Tested devices

"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.

Comparison with other libraries

vs libmtp / libmtp-rs

libmtp is 20+ years old, battle-tested, and very comprehensive. libmtp-rs provides a Rust interface to it. But:

  • libmtp is 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.

vs existing Rust PTP crates

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!

vs winmtp

winmtp wraps the Windows COM API—Windows only. mtp-rs works on Linux, macOS, and Windows.

Implementation notes

  • 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.

Contributing

See CONTRIBUTING.md for guidelines.

License

MIT OR Apache-2.0, at your option.

About

Talk to MTP/PTP devices in pure Rust. No libmtp, no FFI, just async USB (uses nusb). Async streaming uploads/downloads, device events, two-level API for Android phones or raw camera access.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Stars

Watchers

Forks

Releases

No releases published