diff --git a/build/push_gitea.py b/build/push_gitea.py index 5d3b39f..cf2180f 100644 --- a/build/push_gitea.py +++ b/build/push_gitea.py @@ -1,72 +1,103 @@ # -*- coding: utf-8 -*- -"""Laedt Dateien ueber die Gitea-Contents-API hoch (create oder update).""" +"""Laedt alle Projektdateien in EINEM Commit nach Gitea (ChangeFiles-API). + +Aufruf: python3 build/push_gitea.py "Commit-Nachricht" + +Uebersprungen werden die Pfade, die die Workflows selbst pflegen. Sonst wuerde +ein lokaler Altstand die Ergebnisse eines Laufs ueberschreiben. +""" import base64, json, os, sys, urllib.request, urllib.error -def env(): +# Von den Workflows gepflegt, nie von hier ueberschreiben: +NICHT_UEBERSCHREIBEN = {'artikel-index.json', 'status-importe.json'} +NICHT_UEBERSCHREIBEN_ORDNER = ('briefe/', 'nachbessern/', 'artikel/') +AUS_ORDNER = {'.git', 'node_modules', '__pycache__'} +AUS_DATEIEN = {'.env'} + + +def env(pfad='.env'): d = {} - for line in open('.env', encoding='utf-8'): - line = line.strip() - if not line or line.startswith('#') or '=' not in line: continue - k, v = line.split('=', 1) + for zeile in open(pfad, encoding='utf-8'): + zeile = zeile.strip() + if not zeile or zeile.startswith('#') or '=' not in zeile: + continue + k, v = zeile.split('=', 1) v = v.strip() - if v.startswith('"') and v.endswith('"'): v = v[1:-1].replace('\\$', '$').replace('\\"', '"') + if v.startswith('"') and v.endswith('"'): + v = v[1:-1].replace('\\$', '$').replace('\\"', '"') d[k] = v return d + E = env() BASE = E['GITEA_BASE_URL'].rstrip('/') -REPO = f"{BASE}/api/v1/repos/{E['GITEA_OWNER']}/{E['GITEA_REPO']}/contents" +REPO = f"{BASE}/api/v1/repos/{E['GITEA_OWNER']}/{E['GITEA_REPO']}" KOPF = {'Authorization': 'token ' + E['GITEA_TOKEN'], 'Content-Type': 'application/json'} + def ruf(methode, url, daten=None): r = urllib.request.Request(url, method=methode, headers=KOPF, data=json.dumps(daten).encode() if daten else None) try: - with urllib.request.urlopen(r, timeout=60) as a: - return a.status, json.loads(a.read().decode()) + with urllib.request.urlopen(r, timeout=90) as a: + roh = a.read().decode() + return a.status, (json.loads(roh) if roh else {}) except urllib.error.HTTPError as e: - return e.code, json.loads(e.read().decode() or '{}') + roh = e.read().decode() + return e.code, (json.loads(roh) if roh.startswith(('{', '[')) else {'text': roh[:300]}) -def hoch(pfad, nachricht): - with open(pfad, 'rb') as f: - inhalt = base64.b64encode(f.read()).decode() - ziel = pfad.replace(os.sep, '/') - code, _ = ruf('GET', f"{REPO}/{ziel}?ref={E.get('branch','main')}") - sha = None + +def sammle(): + raus = [] + for wurzel, ordner, namen in os.walk('.'): + ordner[:] = [o for o in ordner if o not in AUS_ORDNER] + for n in namen: + p = os.path.relpath(os.path.join(wurzel, n), '.').replace(os.sep, '/') + if p in AUS_DATEIEN or p in NICHT_UEBERSCHREIBEN: + continue + if p.startswith(NICHT_UEBERSCHREIBEN_ORDNER): + continue + raus.append(p) + return sorted(raus) + + +def main(): + nachricht = sys.argv[1] if len(sys.argv) > 1 else 'Update' + dateien = sammle() + + # Vorhandene Dateien brauchen operation "update" plus sha, neue "create". + code, vorhanden = ruf('GET', f"{REPO}/git/trees/main?recursive=1") + bekannt = {} if code == 200: - code_g, body = ruf('GET', f"{REPO}/{ziel}?ref=main") - sha = body.get('sha') - daten = {'content': inhalt, 'message': nachricht, 'branch': 'main'} - if sha: - daten['sha'] = sha - c, b = ruf('PUT', f"{REPO}/{ziel}", daten) + for e in (vorhanden.get('tree') or []): + if e.get('type') == 'blob': + bekannt[e['path']] = e['sha'] + + eintraege = [] + for p in dateien: + with open(p, 'rb') as f: + inhalt = base64.b64encode(f.read()).decode() + e = {'path': p, 'content': inhalt} + if p in bekannt: + e['operation'] = 'update' + e['sha'] = bekannt[p] + else: + e['operation'] = 'create' + eintraege.append(e) + + code, antwort = ruf('POST', f"{REPO}/contents", + {'files': eintraege, 'message': nachricht, 'branch': 'main'}) + + if code in (200, 201): + sha = ((antwort.get('commit') or {}).get('sha') or '')[:8] + neu = sum(1 for e in eintraege if e['operation'] == 'create') + print(f"Ein Commit {sha}: {len(eintraege)} Dateien ({neu} neu, {len(eintraege)-neu} aktualisiert)") + print("Uebersprungen (von den Workflows gepflegt): " + + ', '.join(sorted(NICHT_UEBERSCHREIBEN) + list(NICHT_UEBERSCHREIBEN_ORDNER))) else: - c, b = ruf('POST', f"{REPO}/{ziel}", daten) - ok = c in (200, 201) - print(f" {'ok ' if ok else 'FEHLER'} {ziel}" + ('' if ok else f" -> {c} {str(b)[:160]}")) - return ok + print(f"FEHLER {code}: {json.dumps(antwort)[:400]}") + sys.exit(1) -AUS = {'.git', '.env', 'node_modules', '__pycache__'} -# Diese Pfade schreiben die Workflows selbst. Wuerde das Skript sie hochladen, -# ueberschreibt ein lokaler Altstand die Ergebnisse eines Laufs. -NICHT_UEBERSCHREIBEN = {'artikel-index.json', 'status-importe.json'} -NICHT_UEBERSCHREIBEN_ORDNER = ('briefe' + os.sep, 'nachbessern' + os.sep, 'artikel' + os.sep) -dateien = [] -for wurzel, ordner, namen in os.walk('.'): - ordner[:] = [o for o in ordner if o not in AUS] - for n in namen: - p = os.path.relpath(os.path.join(wurzel, n), '.') - if p in AUS or p.startswith('.env') and p != '.env.example': continue - if p == '.env': continue - if p in NICHT_UEBERSCHREIBEN or p.startswith(NICHT_UEBERSCHREIBEN_ORDNER): - continue - dateien.append(p) - -nachricht = sys.argv[1] if len(sys.argv) > 1 else 'Update' -print("Uebersprungen (von den Workflows gepflegt): " - + ', '.join(sorted(NICHT_UEBERSCHREIBEN)) + ', ' + ', '.join(NICHT_UEBERSCHREIBEN_ORDNER)) -fehler = 0 -for p in sorted(dateien): - if not hoch(p, nachricht): fehler += 1 -print(f"\n{len(dateien)} Dateien, {fehler} Fehler") +if __name__ == '__main__': + main()