feat: --retry 自动补传 + --verify 完成后校验
This commit is contained in:
1 parent
a9fe6ff068
commit
7ab32bad24
3 files changed
+103
-9
No files matched your search
+69
-5
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user