Show a determinate progress bar during export

The export loop can run for a while on large lists. It already knows the
message count up front, so draw a braille spinner + progress bar (frames
and blue borrowed from warez) from inside the Python loop. Written to
stderr so it never pollutes the captured MM3HK| stdout, gated on
isatty() so cron/piped runs stay silent, throttled to ~10 fps, cleared
in finally. Progress bytes never touch the mbox, so its SHA-256 is
unaffected.
This commit is contained in:
veg 2026-05-31 14:30:56 +00:00
parent 8b00e239d2
commit a39ca3157f

View file

@ -73,7 +73,7 @@ EOF
}
result=$(mailman-web shell <<EOF
import sys, os, tempfile, gzip, hashlib
import sys, os, tempfile, gzip, hashlib, time
from hyperkitty.models import MailingList
addr = "$addr"
GZIP = $gzip_py
@ -96,14 +96,35 @@ raw = os.fdopen(fd, "w+b")
# mtime=0 keeps the gzip header deterministic (no timestamp), matching the
# web export, which uses a bare zlib gzip stream.
fh = gzip.GzipFile(fileobj=raw, mode="wb", mtime=0) if GZIP else raw
# Live progress bar on stderr: stdout is captured by the shell (the MM3HK|
# line), so a shell-side spinner can't see per-message progress, but this loop
# knows n up front. TTY-only, so cron/piped runs stay silent. Braille frames
# and blue borrowed from warez's spinner.
tty = sys.stderr.isatty()
SPIN = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
def draw(done, spin):
w = 24
fill = done * w // n
sys.stderr.write("\r\033[0;34m%s\033[0m [%s%s] %d/%d (%d%%)" % (
SPIN[spin % len(SPIN)], "█" * fill, "░" * (w - fill),
done, n, done * 100 // n))
sys.stderr.flush()
spin = 0
last = 0.0
try:
for e in ml.emails.order_by("archived_date").iterator():
for i, e in enumerate(ml.emails.order_by("archived_date").iterator(), 1):
fh.write(e.as_bytes())
fh.write(b"\n\n")
if tty:
now = time.monotonic()
if now - last >= 0.1 or i == n: # ~10 fps, plus a final 100% frame
draw(i, spin); spin += 1; last = now
finally:
if GZIP:
fh.close() # flush the gzip trailer into raw (does not close raw)
raw.flush()
if tty:
sys.stderr.write("\r\033[K"); sys.stderr.flush() # clear the bar
# Hash the on-disk bytes via the held fd, then hand root the digest so it can
# verify the file wasn't swapped/truncated by another same-uid process before
# it copies it. (A fully-compromised exporter is out of scope, since it controls