rsync progress: smooth byte-weighted bar via --info=progress2
- Use rsync --info=progress2 + --out-format=%n for device sync so the gauge shows byte-weighted overall progress instead of per-completed-file jumps - Fix oscillation: starting a new file no longer resets the bar to 0% (set_current_file only updates the filename, never the percentage) - Mirror step: per-file byte segments only update speed/ETA, not the overall pct
This commit is contained in:
+21
-39
@@ -147,6 +147,15 @@ impl WorkerCtx {
|
|||||||
}
|
}
|
||||||
self.changed();
|
self.changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update only the "currently syncing" file display without touching the
|
||||||
|
/// progress percentage (so a new file's start never resets the bar to 0).
|
||||||
|
fn set_current_file(&self, path: &str) {
|
||||||
|
if let Ok(mut s) = self.state.lock() {
|
||||||
|
s.device_prog.current = path.to_string();
|
||||||
|
}
|
||||||
|
self.changed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
@@ -345,6 +354,7 @@ fn run_mirror(config: &Config, device: &Device, ctx: &WorkerCtx) -> Result<(), S
|
|||||||
return stream(ctx, "rsync", &args, |line| {
|
return stream(ctx, "rsync", &args, |line| {
|
||||||
if let Some(l) = parse_rsync_line(line) {
|
if let Some(l) = parse_rsync_line(line) {
|
||||||
match l {
|
match l {
|
||||||
|
// overall byte-weighted progress (progress2)
|
||||||
RsyncLine::Progress(p) => {
|
RsyncLine::Progress(p) => {
|
||||||
if let Ok(mut s) = ctx.state.lock() {
|
if let Ok(mut s) = ctx.state.lock() {
|
||||||
s.mirror.pct = p.pct;
|
s.mirror.pct = p.pct;
|
||||||
@@ -355,9 +365,9 @@ fn run_mirror(config: &Config, device: &Device, ctx: &WorkerCtx) -> Result<(), S
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
// per-file byte segment: only speed/eta, never the pct
|
||||||
RsyncLine::ByteProgress(p) => {
|
RsyncLine::ByteProgress(p) => {
|
||||||
if let Ok(mut s) = ctx.state.lock() {
|
if let Ok(mut s) = ctx.state.lock() {
|
||||||
s.mirror.pct = p.pct;
|
|
||||||
s.mirror.speed = p.speed.clone();
|
s.mirror.speed = p.speed.clone();
|
||||||
s.mirror.eta = p.eta.clone();
|
s.mirror.eta = p.eta.clone();
|
||||||
}
|
}
|
||||||
@@ -535,33 +545,28 @@ fn run_rsync_device(config: &Config, device: &Device, ctx: &WorkerCtx) -> Result
|
|||||||
"-t".to_string(),
|
"-t".to_string(),
|
||||||
"--files-from".to_string(),
|
"--files-from".to_string(),
|
||||||
tmp.clone(),
|
tmp.clone(),
|
||||||
"--progress".to_string(),
|
"--info=progress2".to_string(),
|
||||||
|
"--out-format=%n".to_string(),
|
||||||
"--exclude=.DS_Store".to_string(),
|
"--exclude=.DS_Store".to_string(),
|
||||||
"--exclude=._*".to_string(),
|
"--exclude=._*".to_string(),
|
||||||
format!("{}/", src_dir.display()),
|
format!("{}/", src_dir.display()),
|
||||||
dest.display().to_string(),
|
dest.display().to_string(),
|
||||||
];
|
];
|
||||||
let total = to_copy.len() as u64;
|
let total = to_copy.len() as u64;
|
||||||
let mut xfr_done: u64 = 0;
|
|
||||||
let mut cur_pct: u16 = 0;
|
|
||||||
stream(ctx, "rsync", &args, |line| {
|
stream(ctx, "rsync", &args, |line| {
|
||||||
match parse_rsync_line(line) {
|
match parse_rsync_line(line) {
|
||||||
|
// overall byte-weighted progress (progress2) — use directly
|
||||||
Some(RsyncLine::Progress(p)) => {
|
Some(RsyncLine::Progress(p)) => {
|
||||||
xfr_done = p.done.max(1);
|
let done = if p.done > 0 { p.done } else { total };
|
||||||
cur_pct = 100;
|
ctx.set_device_progress(done, p.total.max(total), &p.speed, &p.eta, "", Some(p.pct));
|
||||||
let pct = smooth_pct(xfr_done, cur_pct, total);
|
|
||||||
ctx.set_device_progress(xfr_done, total, &p.speed, &p.eta, "", Some(pct));
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Some(RsyncLine::ByteProgress(p)) => {
|
|
||||||
cur_pct = p.pct;
|
|
||||||
let pct = smooth_pct(xfr_done, cur_pct, total);
|
|
||||||
ctx.set_device_progress(xfr_done, total, &p.speed, &p.eta, "", Some(pct));
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
// live per-file byte segment: consumed, overall % comes from
|
||||||
|
// the progress2 line above
|
||||||
|
Some(RsyncLine::ByteProgress(_)) => true,
|
||||||
Some(RsyncLine::Total(_)) => true,
|
Some(RsyncLine::Total(_)) => true,
|
||||||
Some(RsyncLine::File(path)) => {
|
Some(RsyncLine::File(path)) => {
|
||||||
ctx.set_device_progress(0, total, "", "", &path, None);
|
ctx.set_current_file(&path);
|
||||||
ctx.log(LogLevel::Transfer, path);
|
ctx.log(LogLevel::Transfer, path);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -626,16 +631,6 @@ fn write_file_list(items: &[String]) -> Result<String, String> {
|
|||||||
|
|
||||||
// ── mlove finalize ────────────────────────────────────────────────────────────
|
// ── mlove finalize ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Combine completed files with the current file's byte fraction for a smooth
|
|
||||||
/// overall percentage, e.g. 3 files done + file at 50% over 5 total → 70%.
|
|
||||||
fn smooth_pct(xfr_done: u64, cur_pct: u16, total: u64) -> u16 {
|
|
||||||
if total == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let ratio = xfr_done as f64 + (cur_pct as f64 / 100.0);
|
|
||||||
((ratio / total as f64) * 100.0).clamp(0.0, 100.0) as u16
|
|
||||||
}
|
|
||||||
|
|
||||||
fn capture(cmd: &str, args: &[&str]) -> Result<(i32, String), String> {
|
fn capture(cmd: &str, args: &[&str]) -> Result<(i32, String), String> {
|
||||||
let out = Command::new(cmd)
|
let out = Command::new(cmd)
|
||||||
.args(args)
|
.args(args)
|
||||||
@@ -811,18 +806,5 @@ where
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::smooth_pct;
|
// engine logic is exercised through parser + integration tests
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn smooth_progress_advances_within_a_file() {
|
|
||||||
// 3 files done, current at 50%, 5 total → (3 + 0.5)/5 = 70%
|
|
||||||
assert_eq!(smooth_pct(3, 50, 5), 70);
|
|
||||||
// 3 files done, current at 0% → 60%
|
|
||||||
assert_eq!(smooth_pct(3, 0, 5), 60);
|
|
||||||
// 1 file done, current at 100% → 40% (xfr#1 line arrives before next file)
|
|
||||||
assert_eq!(smooth_pct(1, 100, 5), 40);
|
|
||||||
// everything done
|
|
||||||
assert_eq!(smooth_pct(5, 100, 5), 100);
|
|
||||||
assert_eq!(smooth_pct(0, 100, 0), 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,4 +256,18 @@ mod tests {
|
|||||||
other => panic!("expected byte progress, got {other:?}"),
|
other => panic!("expected byte progress, got {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_progress2_overall_pct() {
|
||||||
|
// --info=progress2 line: byte-weighted overall progress
|
||||||
|
match parse_rsync_line(" 209.715.200 66% 566,48MB/s 0:00:00 (xfr#1, to-chk=1/2)") {
|
||||||
|
Some(RsyncLine::Progress(p)) => {
|
||||||
|
assert_eq!(p.pct, 66);
|
||||||
|
assert_eq!(p.done, 1);
|
||||||
|
assert_eq!(p.total, 2);
|
||||||
|
assert_eq!(p.speed, "566,48MB/s");
|
||||||
|
}
|
||||||
|
other => panic!("expected progress, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user