mtp-mount
Mount MTP devices as local filesystems via FUSE, with write support! This is pure Rust, not built on libmtp.
To use it, plug in your Android phone or camera, run mtp-mount /mnt/phone, and use ls, cp, cat, rm, mv
on the device's storage like you would on any local directory. Or run the mtp-mountd daemon and skip the mounting
step: every device you plug in shows up under $XDG_RUNTIME_DIR/mtp/ on its own.
Built on mtp-rs (pure-Rust MTP stack) and
fuser.
Install
Usage
List connected devices:
Mount the first available device:
Or pick a specific device by serial number:
Mount read-only (safer for browsing, you'll make no accidental deletes):
Unmount:
Ride out a worn-out cable that keeps dropping the link (off by default, see below):
Run mtp-mount --help for the full list of flags, examples, and troubleshooting tips.
When the cable glitches
Old and worn USB cables drop the device for a second and bring it back. mtp-mount can ride that out, but you have to
ask for it: pass --reconnect-timeout SECONDS.
- The mount stays up, and the device is reopened by serial number, so a replug can't land you on a different phone.
- Filesystem calls made while the device is away wait for it instead of failing, and they resume once it's back. Files you have open keep working, bytes already read stay cached, and a file you were writing still uploads.
- If the device doesn't come back in time,
mtp-mountprints why and unmounts, rather than leaving a mount point that answers every command with an I/O error.
It's off by default because waiting blocks. Calls don't fail during the window, they hang, and that includes anything else touching the mount point: a file manager listing it, a backup job walking it. On a device that's really gone, a 30-second freeze across the desktop is a worse default than a mount that goes away. Turn it on when you're fighting a specific cable, and pick a window you'd be happy waiting out.
MTP identifies files by session-scoped handles, so all of them go stale the moment the device reconnects. mtp-mount
looks each one up again by path, keeping inode numbers unchanged, which is what lets already-open files carry on.
Mount devices automatically: mtp-mountd
Running mtp-mount by hand, per device, is fine for a one-off copy. For a desktop that just works, there's a second
binary: mtp-mountd watches for MTP devices and mounts each one as it's plugged in.
# Try it in a terminal first
RUST_LOG=info
# Then leave it running as a user service
Mounts appear under $XDG_RUNTIME_DIR/mtp/, one directory per device, named after the device's serial number:
/run/user/1000/mtp/2A31FDH200ABC/Internal shared storage/DCIM/
/run/user/1000/mtp/2A31FDH200ABC/SD card/
- One mount per device, with each storage (internal memory, SD card) as a subdirectory. One phone is one place to look, and one thing to clean up if anything goes wrong.
- Devices that report no serial number get
usb-<vendor>-<product>-<port>instead. That name lasts as long as the device stays in the same port; without a serial there's nothing else that survives a replug. - Unplug a device and its mount goes away immediately, forcibly, even mid-copy. A FUSE mount whose device is gone wedges anything that walks it, so leaving one behind is worse than never mounting.
- Stopping the service unmounts everything.
systemctl --user stop mtp-mountdleaves nothing behind. If a previous daemon was killed outright and did leave mounts, the next one clears them at startup. - No reconnect window. A device that drops off is unmounted, and hotplug mounts it again when it comes back, which beats freezing every process on the mount point while waiting.
--mount-root PATHmoves the mounts,--spool-dir PATHmoves the spool,-rmounts everything read-only. Runmtp-mountd --helpfor the rest.
Checking a device without mounting it: --dry-run
Watches for devices and prints what it would do, mounting nothing: the fields each device reports, the mount key derived from them, and the full path it would mount at. No directory is created and the device is never opened, so this runs anywhere, including a machine with no FUSE.
It's there to answer one question about your device: plug it in, wait for the PLUGGED IN block, then unplug it, and
the UNPLUGGED block says whether the key matches the arrival's. It has to, because that key is how a departure finds
the mount to take down; a device whose two keys disagree would leave its mount behind every time. Do that a few times
and read the summary at the end. If it says PROBLEM,
open an issue with the output: that's a device we need to know about.
If $XDG_RUNTIME_DIR isn't set (an SSH login without a logind session, a container), mounts go under
$XDG_CACHE_HOME/mtp-mount/mounts instead, or ~/.cache/mtp-mount/mounts. Never /tmp, which other users can write to.
Running gvfs at the same time? It grabs the USB interface first and mtp-mountd then can't open the device. Stop it
grabbing devices with systemctl --user mask gvfs-mtp-volume-monitor.
What works
- Read:
cat,cp,head,less, and random-access seeks (media scrubbing,tail -c, partialdd) - Write: create files, overwrite existing files
- Directories:
ls,mkdir,rmdir - Delete:
rm - Rename and move:
mv - Large files: files larger than 4 GB read end-to-end (no 32-bit truncation)
- Flaky cables: the mount survives a device that drops off the bus and comes back
What doesn't work (and why)
MTP is an object-based protocol, not a block device, so some POSIX features just don't map:
- Hardlinks and symlinks (MTP has no concept of them)
- File permissions:
chmodandchownare no-ops, everything shows as0644/0755 - Extended attributes
- Sparse files and random-access writes: files are uploaded whole on close
How it works
The FUSE layer translates filesystem calls into MTP operations:
- Reads are byte-range on-demand. Each FUSE
read(offset, size)fetches only the missing bytes via MTP'sGetPartialObject64, writes them into a sparse tempfile, and serves the requested slice. Repeated reads of the same region hit the local cache. Scrubbing a 10 GB video only downloads what you actually touch. - Writes spool to disk, then upload to the device on close. The spool lives in your cache directory, not
/tmp: on most current Linux distros/tmpis a tmpfs (RAM), and a 4 GB upload buffered there would fill memory. The upload streams from the spool file in 64 KiB chunks, so memory stays flat no matter how big the file is. - Overwrites use a safe upload-then-delete-then-rename sequence when the device supports rename. So if the upload fails, the original is still there. Falls back to delete-then-upload with a warning log on devices that don't support rename.
- Directory listings are cached and refreshed on
opendir. A background event monitor watchesMtpDevice::next_event()and invalidates entries when files are added, removed, or modified on the device itself (so taking a photo while the phone is mounted just shows up).
Why gvfs can't write (and this can)
On most Linux desktops, MTP goes through gvfs, and gvfs can't write. This happens:
$ cp photo.jpg "/run/user/1000/gvfs/mtp:host=Pixel_9/Internal shared storage/DCIM/"
cp: cannot create regular file '...': Operation not supported
That's not really a gvfs bug, it's a mismatch between two designs. POSIX opens a file and starts writing without knowing
how big it will be. MTP wants the size up front, in SendObjectInfo, before you send a single byte. gvfs gives up and
returns EOPNOTSUPP. People have been hitting this for ~20 years.
mtp-mount spools the file locally while you write, then uploads it on close(), when the size is finally known. So
cp, rsync, and "save as" from any app work the way you'd expect.
Where the spool lives
Writes (and the read cache) go to unlinked temp files under:
- Linux:
$XDG_CACHE_HOME/mtp-mount/spool, or~/.cache/mtp-mount/spoolwhenXDG_CACHE_HOMEis unset - macOS:
~/Library/Caches/mtp-mount/spool
Point it somewhere else with --spool-dir /path/to/dir, for example when your home directory is small and you're
copying a 60 GB video off a phone. The directory is created if missing, and mtp-mount won't start if it can't write
there. The files are unlinked, so their space comes back on its own if the process stops, and nothing is left behind
to clean up.
Requirements
Disk space for the spool: copying a file to or from the device buffers it under your cache directory first, so keep
roughly as much free space there as the largest file you'll transfer (or pass --spool-dir).
You need a FUSE implementation:
- Linux:
sudo apt install libfuse3-dev(Debian/Ubuntu) orfuse3(Fedora/Arch) - macOS: macFUSE or FUSE-T (may need manual
pkg-configwiring)
Build from source
Testing
Unit tests run without FUSE:
Integration tests mount a virtual MTP device via FUSE (Linux only, needs libfuse3-dev):
128 tests total (93 unit, 27 integration, 8 daemon), all passing on Linux. They use mtp-rs's virtual device
transport, so CI runs without any physical hardware.
License
MIT OR Apache-2.0