review pass: mount safety, abort/quit robustness, UI fixes
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.
This commit is contained in:
+55
-1
@@ -1,5 +1,52 @@
|
||||
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 {
|
||||
@@ -37,7 +84,14 @@ pub fn fmt_bytes(n: u64) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::fmt_bytes;
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user