#!/usr/bin/env python3 """ Trilha B, segunda metade: leva da pasta Downloads para o S3 e apaga. O baixador-navegador.js despeja os arquivos em Downloads com o handle no proprio nome (BRAS__20.500.12156.1_10377__arquivo.jpg). Este script recolhe, confere SHA-256, sobe pro bucket e APAGA na hora -- o disco nunca acumula. Rode em paralelo com o navegador, em modo --vigiar: set -a; . .s3env; set +a python3 subidor.py --vigiar --catalogo catalogo.jsonl Uma passada so, sem vigiar: python3 subidor.py --catalogo catalogo.jsonl """ import argparse, hashlib, json, os, sqlite3, subprocess, sys, time from pathlib import Path S3_ENDPOINT = os.environ.get("S3_ENDPOINT", "https://hel1.your-objectstorage.com") BUCKET = os.environ.get("ARCHIVE_BUCKET", "brasiliana") PREFIXO = "BRAS__" ROOT = Path(__file__).resolve().parent DB = ROOT / "subidor.sqlite" def db(): con = sqlite3.connect(DB) con.executescript(""" CREATE TABLE IF NOT EXISTS enviados ( handle TEXT, nome TEXT, bytes INTEGER, sha256 TEXT, quando TEXT, PRIMARY KEY (handle, nome));""") cols = [r[1] for r in con.execute("PRAGMA table_info(enviados)")] if "tipo" not in cols: # tipo entra na migracao, mas a PK antiga (handle,nome) colidiria # entre preservacao/thumbnail com o mesmo nome de arquivo -- refaz # a tabela com PK composta, preservando as linhas ja existentes # (todas eram 'preservacao', de antes do thumbnail existir). con.executescript(""" ALTER TABLE enviados RENAME TO enviados_old; CREATE TABLE enviados ( handle TEXT, nome TEXT, bytes INTEGER, sha256 TEXT, quando TEXT, tipo TEXT DEFAULT 'preservacao', PRIMARY KEY (handle, tipo, nome)); INSERT INTO enviados (handle,nome,bytes,sha256,quando,tipo) SELECT handle,nome,bytes,sha256,quando,'preservacao' FROM enviados_old; DROP TABLE enviados_old; """) con.commit() return con def s3_put(local, key): return subprocess.run( ["aws", "--endpoint-url", S3_ENDPOINT, "s3", "cp", str(local), f"s3://{BUCKET}/{key}", "--only-show-errors"], capture_output=True, text=True).returncode == 0 def sha256(p): h = hashlib.sha256() with p.open("rb") as fh: for b in iter(lambda: fh.read(1 << 20), b""): h.update(b) return h.hexdigest() def estavel(p): """Chrome ainda escrevendo? Compara o tamanho com um intervalo curto.""" try: a = p.stat().st_size time.sleep(1.5) return a == p.stat().st_size and a > 0 except FileNotFoundError: return False def carrega_catalogo(caminho): if not caminho: return {} d = {} for linha in open(caminho, encoding="utf-8"): if linha.strip(): r = json.loads(linha) d[r["handle"]] = r return d def escreve_metadados(con, handle, cat): """metadata.json + manifest-sha256.txt do item.""" rec = cat.get(handle) if not rec: return pasta = handle.replace("/", "_") # nome com PID: baixador.py roda na mesma pasta e pode escrever no # mesmo .tmp ao mesmo tempo -- nome fixo faz um processo apagar o # arquivo que o outro acabou de criar (achado 2026-09-10, crash real). tmp = ROOT / ".tmp"; tmp.mkdir(exist_ok=True) mj = tmp / f"metadata.{os.getpid()}.json" mj.write_text(json.dumps(rec, ensure_ascii=False, indent=2), encoding="utf-8") s3_put(mj, f"itens/{pasta}/metadata.json") linhas = [f"{sha} {tipo}/{nome}" for nome, sha, tipo in con.execute("SELECT nome,sha256,tipo FROM enviados WHERE handle=?", (handle,))] linhas.append(f"{sha256(mj)} metadata.json") mf = tmp / f"manifest-sha256.{os.getpid()}.txt" mf.write_text("\n".join(linhas) + "\n", encoding="utf-8") s3_put(mf, f"itens/{pasta}/manifest-sha256.txt") mj.unlink(missing_ok=True); mf.unlink(missing_ok=True) def uma_passada(con, pasta_downloads, cat, verboso=True): n = 0 for p in sorted(pasta_downloads.glob(PREFIXO + "*")): if p.suffix == ".crdownload" or not p.is_file(): continue if not estavel(p): continue resto = p.name[len(PREFIXO):] if "__" not in resto: print(f" ignorado (nome inesperado): {p.name}") continue pasta_handle, nome = resto.split("__", 1) handle = pasta_handle.replace("_", "/", 1) if "/" not in pasta_handle else pasta_handle # 20.500.12156.1_10377 -> 20.500.12156.1/10377 if "_" in pasta_handle: base, num = pasta_handle.rsplit("_", 1) handle = f"{base}/{num}" # THUMB__ no nome roteia pra subpasta thumbnail/ em vez de preservacao/ eh_thumb = nome.startswith("THUMB__") if eh_thumb: nome = nome[len("THUMB__"):] subpasta = "thumbnail" if eh_thumb else "preservacao" # valida que e imagem de verdade antes de subir -- achado 2026-09-10: # o palpite de sequencia do achado 2.8 (seq+1 = thumbnail) as vezes # cai no bitstream de LICENCA do DSpace em vez da miniatura (62% dos # thumbs de alguns lotes!). Rejeita qualquer coisa que nao comece com # os magic bytes de JPEG/PNG -- mais barato que confiar no nome. with p.open("rb") as fh: cabecalho = fh.read(8) eh_jpeg = cabecalho[:2] == b"\xff\xd8" eh_png = cabecalho[:8] == b"\x89PNG\r\n\x1a\n" if not (eh_jpeg or eh_png): print(f" REJEITADO (nao e imagem, primeiros bytes: {cabecalho[:20]!r}): {p.name}", flush=True) p.unlink() continue dig, tam = sha256(p), p.stat().st_size if s3_put(p, f"itens/{pasta_handle}/{subpasta}/{nome}"): con.execute("INSERT OR REPLACE INTO enviados VALUES (?,?,?,?,?,?)", (handle, nome, tam, dig, time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), subpasta)) con.commit() escreve_metadados(con, handle, cat) p.unlink() # apaga SEMPRE que subiu n += 1 if verboso: print(f" {handle:<24} {tam/1048576:6.1f} MB -> S3, local apagado", flush=True) else: print(f" FALHA no upload: {p.name} (mantido em disco)", flush=True) return n def main(): ap = argparse.ArgumentParser() ap.add_argument("--downloads", default=str(Path.home() / "Downloads")) ap.add_argument("--catalogo") ap.add_argument("--vigiar", action="store_true") ap.add_argument("--intervalo", type=int, default=20) args = ap.parse_args() if not os.environ.get("AWS_ACCESS_KEY_ID"): sys.exit("credenciais S3 ausentes. Rode: set -a; . .s3env; set +a") con = db() pasta = Path(args.downloads) cat = carrega_catalogo(args.catalogo) print(f"vigiando {pasta} -> s3://{BUCKET}/ ({len(cat)} itens no catalogo)\n" if args.vigiar else f"passada unica em {pasta}\n") total = 0 try: while True: total += uma_passada(con, pasta, cat) if not args.vigiar: break time.sleep(args.intervalo) except KeyboardInterrupt: pass enviados, bytes_ = con.execute( "SELECT COUNT(*), COALESCE(SUM(bytes),0) FROM enviados").fetchone() print(f"\nnesta sessao: {total} | acumulado: {enviados} arquivos, {bytes_/1024**3:.2f} GB") if __name__ == "__main__": main()