mod buffer;
mod error;
mod fs;
mod inode;
use clap::Parser;
use crate::fs::MtpFs;
#[derive(Parser, Debug)]
#[command(
version,
about,
after_long_help = "\
EXAMPLES:
Mount the first available device:
mtp-mount /mnt/phone
List connected devices (shows serial numbers for -d):
mtp-mount --list
Mount a specific device:
mtp-mount -d ABC123 /mnt/phone
Mount read-only (safer for browsing, no accidental deletes):
mtp-mount -r /mnt/phone
Unmount:
umount /mnt/phone
Show debug output (handy for troubleshooting):
RUST_LOG=debug mtp-mount /mnt/phone
TROUBLESHOOTING:
\"No MTP device found\"
Make sure the phone is unlocked, USB mode is set to \"File Transfer\"
(not \"Charging only\"), and the USB debugging prompt is accepted.
\"interface is busy\"
Another program already claimed the USB interface. On Linux, check
if gvfs-mtp auto-mounted it: `gio mount -l` and unmount first.
\"Permission denied\" on /dev/bus/usb
Add yourself to the `plugdev` group, or set up a udev rule.
See: https://github.com/vdavid/mtp-mount#requirements
NOTES:
Files are uploaded to the device when you close them, not on each write.
MTP doesn't support partial writes, hardlinks, symlinks, or chmod."
)]
struct Cli {
mountpoint: Option<String>,
#[arg(short, long, value_name = "SERIAL")]
device: Option<String>,
#[arg(short, long, default_value_t = true)]
foreground: bool,
#[arg(short, long)]
read_only: bool,
#[arg(short, long)]
list: bool,
}
fn list_devices() {
match mtp_rs::MtpDevice::list_devices() {
Ok(devices) if devices.is_empty() => {
println!("No MTP devices found.");
println!();
println!("Make sure your device is unlocked, USB mode is set to");
println!("\"File Transfer\", and the USB debugging prompt is accepted.");
}
Ok(devices) => {
println!("Found {} MTP device(s):\n", devices.len());
for (i, dev) in devices.iter().enumerate() {
let mfr = dev.manufacturer.as_deref().unwrap_or("Unknown");
let product = dev.product.as_deref().unwrap_or("Unknown");
let serial = dev.serial_number.as_deref().unwrap_or("(no serial)");
println!(
" [{}] {} {} (serial: {}, USB {:04x}:{:04x})",
i, mfr, product, serial, dev.vendor_id, dev.product_id
);
}
println!();
println!("Use -d SERIAL to mount a specific device.");
}
Err(e) => {
eprintln!("Failed to list devices: {e}");
std::process::exit(1);
}
}
}
fn main() {
env_logger::init();
let cli = Cli::parse();
if cli.list {
list_devices();
return;
}
let mountpoint = match &cli.mountpoint {
Some(m) => m,
None => {
eprintln!("Error: <MOUNTPOINT> is required (or use --list to see devices)");
std::process::exit(1);
}
};
let device_label = cli.device.as_deref().unwrap_or("first available device");
println!("Mounting {device_label} at {mountpoint}...");
let rt = tokio::runtime::Runtime::new().expect("failed to create tokio runtime");
let handle = rt.handle().clone();
let device = rt.block_on(async {
if let Some(serial) = &cli.device {
mtp_rs::MtpDevice::open_by_serial(serial).await
} else {
mtp_rs::MtpDevice::open_first().await
}
});
let device = match device {
Ok(d) => d,
Err(e) => {
eprintln!("Failed to open MTP device: {e}");
std::process::exit(1);
}
};
let mtp_fs = MtpFs::new(device, cli.read_only, handle);
let mount_options = mtp_fs.mount_options();
let mut config = fuser::Config::default();
config.mount_options = mount_options;
println!("Mounted. Press Ctrl+C to unmount.");
if let Err(e) = fuser::mount2(mtp_fs, mountpoint, &config) {
eprintln!("Mount failed: {e}");
std::process::exit(1);
}
}