feat: --retry 自动补传 + --verify 完成后校验

This commit is contained in:
AskaEth committed 2026-08-22 14:01:00 +08:00
1 parent a9fe6ff068
commit 7ab32bad24
3 files changed
+103 -9

No files matched your search

+69 -5
View File
@@ -118,6 +118,7 @@ class TransferConfig:
verify: bool = False
logdir: str = ""
policy: str = "overwrite"
retry: int = 0
# ---------------------------------------------------------------- 输出解析器
@@ -291,6 +292,17 @@ class RsyncBackend:
return cmd
@staticmethod
def build_verify_cmd(src: str, dst: str, bwlimit: str = "") -> list[str]:
"""校验命令: rsync -c --dry-run, 无输出=一致"""
src = src.rstrip("/") + "/"
cmd = ["rsync", "-a", "-c", "--dry-run", "--out-format=%i|%n"]
if bwlimit:
cmd += [f"--bwlimit={bwlimit}"]
cmd += [src, dst]
return cmd
class RcloneBackend:
name = "rclone"
@@ -415,7 +427,9 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser.add_argument("--checksum", action="store_true",
help="rsync 校验模式 (-c)")
parser.add_argument("--verify", action="store_true",
help="传输完成后校验")
help="传输完成后校验 (-c)")
parser.add_argument("--retry", type=int, default=0,
help="失败自动补传轮数 (默认 0)")
parser.add_argument("--mode", choices=["cli", "tui"], default=None,
help="运行模式 (默认: 有任务=cli, 无任务=tui)")
parser.add_argument("--keep-logs", type=int, default=20,
@@ -559,7 +573,26 @@ class TaskRunner:
# -- 执行
def run(self) -> int:
"""同步执行传输, 返回退出码 (进度由目录轮询驱动, 绕过后端输出缓冲)"""
"""执行传输: 失败自动补传(retry) + 完成后校验(verify)"""
rc = 0
for attempt in range(self.cfg.retry + 1):
if attempt > 0:
self.lm.write(f"=== 补传轮次 {attempt}/{self.cfg.retry} ===")
if self.on_log:
self.on_log(f"[yellow]补传轮次 {attempt}/{self.cfg.retry}[/]")
time.sleep(3) # 等守护恢复设备
rc = self._run_once()
if rc == 0:
break
self.lm.write(f"attempt {attempt + 1} failed rc={rc}")
if self.on_error and attempt < self.cfg.retry:
self.on_error(f"本轮退出码 {rc}, 将自动补传 ({attempt + 1}/{self.cfg.retry})")
if rc == 0 and self.cfg.verify and self.cfg.backend in ("rsync",):
self._verify()
return rc
def _run_once(self) -> int:
"""单次传输, 返回退出码 (进度由目录轮询驱动, 绕过后端输出缓冲)"""
cmd = self.build_command()
self.lm.open_session(f"{self.cfg.backend}_{os.path.basename(self.cfg.source.rstrip('/'))}")
self.lm.write(f"cmd: {' '.join(cmd)}")
@@ -625,6 +658,35 @@ class TaskRunner:
self.on_log(f"已传 {human_size(done)} / {human_size(self.total_bytes)} "
f"({self.stats.pct:.0f}%) {human_speed(self.stats.speed_bps)}")
def _verify(self) -> None:
"""完成后校验: rsync -c --dry-run, 统计差异"""
self.lm.write("verify: 开始校验 (-c dry-run)")
if self.on_log:
self.on_log("[cyan]校验中 (rsync -c --dry-run)...[/]")
cmd = RsyncBackend.build_verify_cmd(self.cfg.source, self.cfg.dest,
self.cfg.bwlimit)
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True)
except OSError as exc:
self.lm.write_error(f"verify: 启动失败 {exc}")
return
assert proc.stdout is not None
diffs = 0
for line in proc.stdout:
line = line.rstrip()
if not line or line.startswith(("sending", "sent ", "total ")):
continue
diffs += 1
self.lm.write(f"verify diff: {line}")
rc = proc.wait()
self.lm.write(f"verify: 完成 rc={rc} 差异文件={diffs}")
if self.on_log:
if diffs == 0 and rc == 0:
self.on_log("[green]✔ 校验通过: 源与目标完全一致[/]")
else:
self.on_log(f"[red]✘ 校验发现 {diffs} 个差异 (rc={rc})[/]")
def stop(self) -> None:
self._stop.set()
if self.proc and self.proc.poll() is None:
@@ -873,7 +935,7 @@ class TransferApp(App):
source=args.source, dest=args.dest, backend=args.backend,
bwlimit=args.bwlimit, transfers=args.transfers,
resume=args.resume, checksum=args.checksum, verify=args.verify,
logdir=self.logdir, policy=args.policy,
logdir=self.logdir, policy=args.policy, retry=args.retry,
)
if cfg.policy == "ask" and cfg.backend == "rsync":
self._flash("[yellow]预扫描冲突...[/]")
@@ -1035,14 +1097,16 @@ def main() -> int:
backend=args.backend, bwlimit=args.bwlimit,
transfers=args.transfers, resume=args.resume,
checksum=args.checksum, verify=args.verify,
logdir=args.logdir, policy=args.policy)
logdir=args.logdir, policy=args.policy,
retry=args.retry)
TransferApp(preset=cfg, logdir=args.logdir, keep_logs=args.keep_logs).run()
return 0
cfg = TransferConfig(source=args.source, dest=args.dest,
backend=args.backend, bwlimit=args.bwlimit,
transfers=args.transfers, resume=args.resume,
checksum=args.checksum, verify=args.verify,
logdir=args.logdir, policy=args.policy)
logdir=args.logdir, policy=args.policy,
retry=args.retry)
return run_cli(cfg, args)