udisks2: mount/unmount without sudo, auto-mount before sync
- m key toggles mount/unmount of the selected device via udisksctl (resolves the block device from mount point or lsblk label) - rsync-style device sync auto-mounts the target first - mlove finalize unmounts via udisksctl, falling back to sudo -n umount - keybar/help/README updated
This commit is contained in:
@@ -39,6 +39,7 @@ cargo build --release
|
||||
| `↑`/`↓` `j`/`k` | move selection |
|
||||
| `s` / `Enter`| sync selected device |
|
||||
| `a` | abort running sync |
|
||||
| `m` | mount / unmount device (udisks2, no sudo) |
|
||||
| `Tab` / `1-3`| switch tab |
|
||||
| `r` | refresh device mounts |
|
||||
| `f` | toggle log follow |
|
||||
@@ -83,6 +84,13 @@ Firmware types: `apple` (podkit command), `rockbox`, `android`, `sony`
|
||||
`sudo -n` to keep FAT directory order stable. If passwordless sudo is not
|
||||
available the step is reported as a warning and the commands are printed.
|
||||
|
||||
## Mounting
|
||||
|
||||
Devices are mounted/unmounted without sudo via **udisks2**:
|
||||
`m` in the TUI mounts/unmounts the selected device, and a sync automatically
|
||||
mounts the target first (resolved by mount-point label via `lsblk`). Only the
|
||||
mlove `fatsort` step needs root (raw block device).
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
|
||||
+29
@@ -145,6 +145,34 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_mount(&mut self) {
|
||||
let Some(device) = self.selected_device().cloned() else {
|
||||
self.status_msg = Some(("no device selected".to_string(), LogLevel::Warn));
|
||||
return;
|
||||
};
|
||||
let Some(mp) = device.mount_point.clone() else {
|
||||
self.status_msg = Some(("device has no mount_point".to_string(), LogLevel::Warn));
|
||||
return;
|
||||
};
|
||||
if self.is_syncing() {
|
||||
return;
|
||||
}
|
||||
let (action, res) = if device.mounted {
|
||||
("Unmounting", crate::udisks::unmount_device(&mp))
|
||||
} else {
|
||||
("Mounting", crate::udisks::mount_device(&mp))
|
||||
};
|
||||
match res {
|
||||
Ok(msg) => {
|
||||
self.status_msg = Some((format!("{action}: {msg}"), LogLevel::Success));
|
||||
}
|
||||
Err(e) => {
|
||||
self.status_msg = Some((format!("{action} failed — {e}"), LogLevel::Error));
|
||||
}
|
||||
}
|
||||
self.refresh_mounts();
|
||||
}
|
||||
|
||||
pub fn on_key(&mut self, key: KeyEvent) {
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return;
|
||||
@@ -174,6 +202,7 @@ impl App {
|
||||
}
|
||||
KeyCode::Char('s') | KeyCode::Enter => self.start_sync(),
|
||||
KeyCode::Char('a') => self.abort_sync(),
|
||||
KeyCode::Char('m') => self.toggle_mount(),
|
||||
KeyCode::Char('r') => {
|
||||
self.refresh_mounts();
|
||||
self.status_msg = Some(("devices refreshed".to_string(), LogLevel::Info));
|
||||
|
||||
+29
-10
@@ -479,9 +479,22 @@ fn run_rsync_device(config: &Config, device: &Device, ctx: &WorkerCtx) -> Result
|
||||
.mount_point
|
||||
.as_ref()
|
||||
.ok_or_else(|| "mount_point is not set for this device".to_string())?;
|
||||
|
||||
if !mount.exists() {
|
||||
ctx.log(LogLevel::Info, format!("device not mounted at {} — trying udisksctl…", mount.display()));
|
||||
match crate::udisks::mount_device(mount) {
|
||||
Ok(msg) => ctx.log(LogLevel::Info, format!("auto-mounted: {msg}")),
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"device not found at {} — connect & mount it (m) and retry. {e}",
|
||||
mount.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !mount.exists() {
|
||||
return Err(format!(
|
||||
"device not found at {} — connect & mount it, then try again",
|
||||
"device not found at {} — connect & mount it (m) and retry",
|
||||
mount.display()
|
||||
));
|
||||
}
|
||||
@@ -650,15 +663,21 @@ fn finalize_fat(device: &Device, ctx: &WorkerCtx) -> Result<(), String> {
|
||||
let target = resolve_fatsort_target(mount)?;
|
||||
|
||||
ctx.log(LogLevel::Info, format!("Unmounting {mount_display}", mount_display = mount.display()));
|
||||
let (code, detail) = capture("sudo", &["-n", "umount", mount.to_str().unwrap_or("")])?;
|
||||
if code != 0 {
|
||||
ctx.log(
|
||||
LogLevel::Warn,
|
||||
format!(
|
||||
"sudo -n umount failed (exit {code}) {detail} — run manually: sudo umount {mount_display}",
|
||||
mount_display = mount.display()
|
||||
),
|
||||
);
|
||||
match crate::udisks::unmount_device(mount) {
|
||||
Ok(msg) => ctx.log(LogLevel::Info, format!("unmounted: {msg}")),
|
||||
Err(e) => {
|
||||
// fall back to sudo for setups without udisks
|
||||
let (code, detail) = capture("sudo", &["-n", "umount", mount.to_str().unwrap_or("")])?;
|
||||
if code != 0 {
|
||||
ctx.log(
|
||||
LogLevel::Warn,
|
||||
format!(
|
||||
"udisks failed ({e}) and sudo -n umount failed (exit {code}) {detail} — run manually: sudo umount {mount_display}",
|
||||
mount_display = mount.display()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.log(LogLevel::Info, format!("Running fatsort on {target}…"));
|
||||
|
||||
@@ -3,6 +3,7 @@ mod config;
|
||||
mod disk;
|
||||
mod engine;
|
||||
mod progress;
|
||||
mod udisks;
|
||||
mod ui;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
/// Mount/unmount devices without sudo via udisks2 (available to the active
|
||||
/// desktop user). The block device is resolved from the mount point: either
|
||||
/// the currently-mounted source, or a partition whose label matches the mount
|
||||
/// point's basename (e.g. `/media/sebastian/IPOD` → label `IPOD`).
|
||||
|
||||
fn run(cmd: &str, args: &[&str]) -> (bool, String) {
|
||||
match Command::new(cmd).args(args).output() {
|
||||
Ok(o) => {
|
||||
let out = String::from_utf8_lossy(&o.stdout).trim().to_string();
|
||||
let err = String::from_utf8_lossy(&o.stderr).trim().to_string();
|
||||
let detail = if !err.is_empty() { err } else { out };
|
||||
(o.status.success(), detail)
|
||||
}
|
||||
Err(e) => (false, format!("cannot run {cmd}: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_block_device(path: &Path) -> Option<String> {
|
||||
if path.is_dir() {
|
||||
let (ok, out) = run("findmnt", &["-n", "-o", "SOURCE", "-T", path.to_str()?]);
|
||||
if ok {
|
||||
let src = out.trim();
|
||||
if src.starts_with("/dev/") {
|
||||
return Some(src.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
let wanted = path.file_name()?.to_string_lossy().to_lowercase();
|
||||
let (ok, out) = run("lsblk", &["-o", "PATH,LABEL", "-n", "-l"]);
|
||||
if !ok {
|
||||
return None;
|
||||
}
|
||||
for line in out.lines() {
|
||||
let mut parts = line.split_whitespace();
|
||||
let Some(dev) = parts.next() else { continue };
|
||||
if let Some(label) = parts.next() {
|
||||
if !label.is_empty() && label.to_lowercase() == wanted {
|
||||
return Some(dev.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn mount_device(path: &Path) -> Result<String, String> {
|
||||
let Some(dev) = resolve_block_device(path) else {
|
||||
return Err(format!("no block device found for {}", path.display()));
|
||||
};
|
||||
let (ok, detail) = run("udisksctl", &["mount", "-b", dev.as_str()]);
|
||||
if ok {
|
||||
Ok(detail)
|
||||
} else {
|
||||
Err(format!("udisksctl mount {dev} failed: {detail}"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unmount_device(path: &Path) -> Result<String, String> {
|
||||
let Some(dev) = resolve_block_device(path) else {
|
||||
return Err(format!("no block device found for {}", path.display()));
|
||||
};
|
||||
let (ok, detail) = run("udisksctl", &["unmount", "-b", dev.as_str()]);
|
||||
if ok {
|
||||
Ok(detail)
|
||||
} else {
|
||||
Err(format!("udisksctl unmount {dev} failed: {detail}"))
|
||||
}
|
||||
}
|
||||
@@ -353,6 +353,7 @@ pub fn draw_keys(f: &mut Frame, area: Rect, _app: &App) {
|
||||
("↑↓/jk", "nav"),
|
||||
("s", "sync"),
|
||||
("a", "abort"),
|
||||
("m", "mount"),
|
||||
("Tab/1-3", "tab"),
|
||||
("r", "refresh"),
|
||||
("f", "follow"),
|
||||
|
||||
@@ -81,6 +81,7 @@ fn draw_help(f: &mut Frame) {
|
||||
" ↑↓ / j k move selection",
|
||||
" s / Enter sync selected device",
|
||||
" a abort running sync",
|
||||
" m mount / unmount selected device (udisks)",
|
||||
" Tab / 1-3 switch tab",
|
||||
" r refresh devices",
|
||||
" f toggle log follow",
|
||||
|
||||
Reference in New Issue
Block a user