Build and publish container / build (pull_request) Successful in 2m33s
The transfer looked hung. rsync prints nothing while it builds its file list, which on fifty thousand files over USB is several minutes of silence, and --info=progress2 does not help: with incremental recursion its percentage is computed against a list rsync has not finished discovering, so it moves backwards as often as forwards. The script now counts what needs copying first and says so, then renders its own single line that rewrites in place, showing the album currently going across and a percentage against a total that is actually known. Counting costs a second pass over the tree. That is the price of a percentage meaning something, and it is cheaper than staring at a blank terminal wondering whether the thing has died. Directories are excluded from the count. rsync reports those too, and including them puts the figure past a hundred per cent. Piped to a log the line becomes a plain one every thirty seconds, because a log full of carriage returns and escape codes is not a log anybody reads.
90 lines
2.8 KiB
Python
Executable File
90 lines
2.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Render rsync's per-file output as a single updating status line.
|
|
|
|
Fed the paths rsync reports with --out-format='%n', one per line. Prints one
|
|
line that rewrites itself, showing how far through the transfer is and which
|
|
album is currently going across, rather than either scrolling fifty thousand
|
|
filenames past or -- as rsync does while it builds its file list -- saying
|
|
nothing at all for several minutes.
|
|
|
|
Falls back to periodic plain lines when stderr is not a terminal, so a log does
|
|
not fill up with carriage returns.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import time
|
|
|
|
|
|
def album_of(path):
|
|
"""Return "Artist / Album" for a mirror-relative path."""
|
|
parts = [part for part in path.strip("/").split("/") if part]
|
|
if len(parts) >= 3:
|
|
return f"{parts[0]} / {parts[1]}"
|
|
if len(parts) == 2:
|
|
return parts[0]
|
|
return ""
|
|
|
|
|
|
def fit(text, width):
|
|
"""Trim to the terminal, from the left: the album matters more than the artist."""
|
|
if width <= 1 or len(text) <= width:
|
|
return text
|
|
return "…" + text[-(width - 1) :]
|
|
|
|
|
|
def render(done, total, label, width):
|
|
if total > 0:
|
|
share = min(100, done * 100 // total)
|
|
head = f"[{done:>6,} / {total:<6,} {share:>3}%] "
|
|
else:
|
|
head = f"[{done:>6,} files] "
|
|
return head + fit(label, max(0, width - len(head)))
|
|
|
|
|
|
def main(argv=None, stream=None, out=None):
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--total", type=int, default=0, help="files expected")
|
|
parser.add_argument("--interval", type=float, default=0.1, help="seconds between redraws")
|
|
args = parser.parse_args(argv)
|
|
|
|
stream = stream or sys.stdin
|
|
out = out or sys.stderr
|
|
interactive = out.isatty()
|
|
width = shutil.get_terminal_size((100, 24)).columns - 1
|
|
|
|
done = 0
|
|
last_drawn = 0.0
|
|
label = ""
|
|
for line in stream:
|
|
path = line.rstrip("\n")
|
|
# rsync reports directories too, with a trailing slash. They are not
|
|
# files and counting them would put the percentage past a hundred.
|
|
if not path or path.endswith("/"):
|
|
continue
|
|
done += 1
|
|
label = album_of(path) or os.path.basename(path)
|
|
|
|
now = time.monotonic()
|
|
if interactive:
|
|
if now - last_drawn >= args.interval:
|
|
out.write("\r\033[2K" + render(done, args.total, label, width))
|
|
out.flush()
|
|
last_drawn = now
|
|
elif now - last_drawn >= 30:
|
|
out.write(render(done, args.total, label, width) + "\n")
|
|
out.flush()
|
|
last_drawn = now
|
|
|
|
if interactive:
|
|
out.write("\r\033[2K")
|
|
out.write(render(done, args.total, label, width).rstrip() + "\n")
|
|
out.flush()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|