progress: barra di avanzamento per download ed estrazione
Aggiunge src/progress.{c,h}: barra a una riga attiva solo quando stdout e' un
terminale (con output rediretto/pipeline non stampa nulla, quindi l'output resta
identico al client Python: parita' 30/30 invariata).
- due fasi: "Scaricamento" (byte da Content-Length) ed "Estrazione" (byte
compressi consumati + membri processati: nuovo contatore in gz_reader e
callback tar_progress_fn/tar_extract_cb)
- controllo con CELLAR_PROGRESS=auto|bar|plain|off (default auto)
- nessun codice ANSI, solo '\r' e riempimento con spazi; glifi ASCII se la
locale non e' UTF-8, larghezza da TIOCGWINSZ e misurata in colonne (i glifi
UTF-8 sono multi-byte), ridisegno throttled, velocita' a media mobile, ETA
- percorsi d'errore: progress_abort() chiude la riga senza riepilogo; il file
parziale resta come nel client Python
- tests/progress_test.sh: pipe silenziosa, PTY via script (barra, ETA, riepilogo,
nessun ANSI, righe entro la larghezza), modalita' plain e off, integrita' del
file scaricato e della bottiglia installata (13/13 verdi)
- tests/parity_test.sh resta 30/30; mock_server.py con --throttle per i test
This commit is contained in:
+32
-4
@@ -19,6 +19,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
@@ -75,8 +76,14 @@ def parse_multipart(body: bytes, content_type: str):
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
state: State
|
||||
throttle = 0 # byte/s, 0 = nessun limite (per testare la barra di avanzamento)
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
@classmethod
|
||||
def _pace(cls, nbytes: int) -> None:
|
||||
if cls.throttle > 0:
|
||||
time.sleep(nbytes / cls.throttle)
|
||||
|
||||
def log_message(self, *args): # silenzioso
|
||||
pass
|
||||
|
||||
@@ -90,7 +97,20 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
def _body(self) -> bytes:
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
return self.rfile.read(length) if length else b""
|
||||
if not length:
|
||||
return b""
|
||||
if Handler.throttle <= 0:
|
||||
return self.rfile.read(length)
|
||||
chunks, left = [], length
|
||||
while left > 0:
|
||||
n = min(left, 65536)
|
||||
data = self.rfile.read(n)
|
||||
if not data:
|
||||
break
|
||||
chunks.append(data)
|
||||
left -= len(data)
|
||||
Handler._pace(len(data))
|
||||
return b"".join(chunks)
|
||||
|
||||
def do_GET(self):
|
||||
path = self.path.split("?")[0]
|
||||
@@ -113,13 +133,19 @@ class Handler(BaseHTTPRequestHandler):
|
||||
f = self.state.storage / r["stored_name"]
|
||||
if not f.exists():
|
||||
return self._json(404, {"detail": "Stored file not found."})
|
||||
data = f.read_bytes()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", r.get("content_type") or "application/octet-stream")
|
||||
self.send_header("Content-Disposition", f'attachment; filename="{r["file_name"]}"')
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.send_header("Content-Length", str(f.stat().st_size))
|
||||
self.end_headers()
|
||||
return self.wfile.write(data)
|
||||
with f.open("rb") as fh:
|
||||
while True:
|
||||
chunk = fh.read(65536)
|
||||
if not chunk:
|
||||
break
|
||||
self.wfile.write(chunk)
|
||||
Handler._pace(len(chunk))
|
||||
return None
|
||||
return self._json(404, {"detail": "Archive not found."})
|
||||
if path == "/openapi.json":
|
||||
return self._json(200, {"info": {"title": "Bottle Archive Server", "version": "0.1.0"}})
|
||||
@@ -181,8 +207,10 @@ def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--port", type=int, default=18099)
|
||||
ap.add_argument("--state", required=True)
|
||||
ap.add_argument("--throttle", type=int, default=0, help="byte/s massimi (per testare la barra)")
|
||||
args = ap.parse_args()
|
||||
Handler.state = State(Path(args.state))
|
||||
Handler.throttle = args.throttle
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", args.port), Handler)
|
||||
srv.serve_forever()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user