f566162c29
Engine / safety: - verify mount points against /proc/self/mountinfo instead of path existence; refuse to sync into a leftover (unmounted) directory that would otherwise receive the library on the root filesystem - resolve udisks block devices by label when the path is not mounted, and never let findmnt fall back to the containing root filesystem - abort now SIGTERMs the whole child process group (sh -c/podkit and rsync children included), escalating to SIGKILL after 2s - quitting while a sync runs aborts and waits for the engine instead of orphaning the child - atomically mark the engine busy before spawning to prevent double syncs; persist state before publishing completion - don't follow symlinked directories while scanning (recursion loops) - prune empty directories recursively when removing stale files UI: - help overlay was clipped (hardcoded height): size it from content - speed/ETA were pushed onto a line the progress panel clipped; show them right-aligned on the bytes row - leaving log follow with u/d now scrolls from the bottom instead of jumping to the top of the log - reload config with r (label updated), refresh mounts on completion so last-sync ages are current - fix width underflows on narrow terminals; terminal-aware sidebar scrolling Tests: mountinfo unescape, process-group abort, and a TestBackend render smoke test covering all tabs, small terminals and the help overlay.
105 lines
3.2 KiB
Rust
105 lines
3.2 KiB
Rust
use std::path::Path;
|
|
|
|
/// Whether `path` is currently a mount point.
|
|
///
|
|
/// Scanning `/proc/self/mountinfo` is used instead of `path.exists()` so a
|
|
/// leftover mount-point directory (e.g. `/media/sebastian/IPOD` after an
|
|
/// unclean eject) is not mistaken for a mounted device. Writing a device sync
|
|
/// into an unmounted directory would silently fill the root filesystem.
|
|
pub fn is_mounted(path: &Path) -> bool {
|
|
let Some(target) = path.to_str() else {
|
|
return false;
|
|
};
|
|
let target = target.trim_end_matches('/');
|
|
|
|
#[cfg(target_os = "linux")]
|
|
if let Ok(text) = std::fs::read_to_string("/proc/self/mountinfo") {
|
|
return text.lines().any(|line| {
|
|
let Some(field) = line.split(' ').nth(4) else {
|
|
return false;
|
|
};
|
|
unescape_octal(field).trim_end_matches('/') == target
|
|
});
|
|
}
|
|
|
|
// Fallback for platforms without /proc: best effort only.
|
|
path.exists()
|
|
}
|
|
|
|
/// Undo the octal escaping (`\040` etc.) used by /proc mount tables.
|
|
fn unescape_octal(s: &str) -> String {
|
|
let bytes = s.as_bytes();
|
|
let mut out = String::with_capacity(s.len());
|
|
let mut i = 0;
|
|
while i < bytes.len() {
|
|
let octal = bytes[i] == b'\\'
|
|
&& i + 3 < bytes.len()
|
|
&& bytes[i + 1..=i + 3].iter().all(|b| (b'0'..=b'7').contains(b));
|
|
if octal {
|
|
let v = (bytes[i + 1] - b'0') * 64 + (bytes[i + 2] - b'0') * 8 + (bytes[i + 3] - b'0');
|
|
out.push(v as char);
|
|
i += 4;
|
|
} else {
|
|
out.push(bytes[i] as char);
|
|
i += 1;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Total and available bytes on a mounted filesystem (statvfs).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct DiskStats {
|
|
pub total: u64,
|
|
pub available: u64,
|
|
}
|
|
|
|
pub fn disk_stats(path: &Path) -> Option<DiskStats> {
|
|
let c = std::ffi::CString::new(path.to_str()?).ok()?;
|
|
let mut s: libc::statvfs = unsafe { std::mem::zeroed() };
|
|
if unsafe { libc::statvfs(c.as_ptr(), &mut s) } != 0 {
|
|
return None;
|
|
}
|
|
let frsize = (s.f_frsize.max(1)) as u64;
|
|
Some(DiskStats {
|
|
total: s.f_blocks as u64 * frsize,
|
|
available: s.f_bavail as u64 * frsize,
|
|
})
|
|
}
|
|
|
|
pub fn fmt_bytes(n: u64) -> String {
|
|
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
let mut v = n as f64;
|
|
let mut u = 0;
|
|
while v >= 1024.0 && u < 4 {
|
|
v /= 1024.0;
|
|
u += 1;
|
|
}
|
|
if u == 0 {
|
|
format!("{v:.0} {}", UNITS[u])
|
|
} else {
|
|
format!("{v:.1} {}", UNITS[u])
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{fmt_bytes, unescape_octal};
|
|
|
|
#[test]
|
|
fn unescapes_mountinfo_paths() {
|
|
assert_eq!(unescape_octal("/media/sebastian/IPOD"), "/media/sebastian/IPOD");
|
|
assert_eq!(unescape_octal("/media/My\\040Disk"), "/media/My Disk");
|
|
assert_eq!(unescape_octal("/media/back\\134slash"), "/media/back\\slash");
|
|
}
|
|
|
|
#[test]
|
|
fn formats_bytes() {
|
|
assert_eq!(fmt_bytes(0), "0 B");
|
|
assert_eq!(fmt_bytes(512), "512 B");
|
|
assert_eq!(fmt_bytes(1024), "1.0 KiB");
|
|
assert_eq!(fmt_bytes(137438953472), "128.0 GiB");
|
|
assert_eq!(fmt_bytes(1_099_511_627_776), "1.0 TiB");
|
|
}
|
|
}
|