veroeffentliche_04.py: Upload mit rsync --delete und Nachpruefung
Der Anlass war messbar. Der Live-Stand vor dem Skript:
/OR/ 200
/OR/anhang-glossar-literatur.html 200 <- Leiche des vorigen Stands
/OR/anhang-glossar.html 404
/OR/notebooks.html 404
/OR/Notebooks_04.zip 404
/OR/Notebooks_04/ 403
scp -r ueberschreibt nur, es loescht nie - die alte Glossar-Seite bliebe
dauerhaft erreichbar. Das Zielverzeichnis vorher zu leeren waere die grobe
Antwort: Bricht die Uebertragung von 23 MB in 213 Dateien ab, ist die Website
tot. rsync --delete uebertraegt nur Geaendertes und entfernt genau das, was
verschwunden ist.
Der zweite Grund wiegt schwerer: Die Website hing wochenlang einen Bau
zurueck, ohne dass es auffiel. Vier Schritte:
1. Vorpruefung - ist eine Quelldatei neuer als die gebaute Website, Abbruch.
Ebenso: PDF der Website byte-genau wie das gebaute, Verzeichnis-
Weiterleitungen vorhanden (der Grund fuer den 403).
2. Probelauf - zaehlt Uebertragungen und Loeschungen. Mehr als 30 Loeschungen
brechen ab; das ist der Schutz gegen einen falschen Zielpfad, mit dem
--delete sonst ein fremdes Verzeichnis ausraeumt. --loeschgrenze hebt an.
3. Uebertragung (rsync -rltz --chmod=D755,F644 ueber ssh).
4. Nachpruefung - 14 Adressen per HTTP, Statuscode gegen Erwartung. Darunter
die Probe, dass die entfernte Seite wirklich 404 liefert.
Anmeldung per SSH-Schluessel, nicht per Passwort in der .env: rsync und scp
haben keine Passwort-Option, ein Passwort braeuchte sshpass und staende
waehrend der Uebertragung in der Prozessliste. Die .env (in .gitignore,
Vorlage .env.beispiel) enthaelt nur Host, Benutzer, Zielpfad und URL.
Beim ersten Test lief die Pruefung in eine irrefuehrende Meldung ("kein rsync
installiert"), waehrend in Wahrheit schon die Anmeldung fehlschlug. Die
Pruefung unterscheidet die Faelle jetzt am ssh-Rueckgabewert 255 und nennt
ssh-copy-id.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
567bfa799a
commit
d566cf8dac
5 changed files with 450 additions and 2 deletions
20
.env.beispiel
Normal file
20
.env.beispiel
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Vorlage fuer .env - kopieren, ausfuellen, absichern:
|
||||
#
|
||||
# cp .env.beispiel .env && chmod 600 .env
|
||||
#
|
||||
# Gelesen von veroeffentliche_04.py. Hier steht KEIN Geheimnis: rsync und scp
|
||||
# haben keine Passwort-Option, die Anmeldung laeuft ueber den SSH-Schluessel.
|
||||
# Einmalig einrichten mit
|
||||
#
|
||||
# ssh-copy-id dschlueter@jamulix.de
|
||||
#
|
||||
# Danach fragt nichts mehr nach einem Passwort.
|
||||
|
||||
OR_HOST=jamulix.de
|
||||
OR_BENUTZER=dschlueter
|
||||
OR_ZIEL=/var/www/jamulix.de/html/OR/
|
||||
OR_URL=https://jamulix.de/OR/
|
||||
|
||||
# Optional - nur setzen, wenn vom Ueblichen abweichend:
|
||||
# OR_SSH_PORT=22
|
||||
# OR_SSH_KEY=~/.ssh/id_ed25519
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -22,3 +22,8 @@ svg-inkscape/
|
|||
Operations_Research_mit_Python_Version_04_Programme/output/
|
||||
produktionsmix.xlsx
|
||||
produktionsmix_ergebnis.xlsx
|
||||
|
||||
# Zugangsdaten fuers Veroeffentlichen (veroeffentliche_04.py). Enthaelt zwar
|
||||
# kein Passwort - die Anmeldung laeuft ueber den SSH-Schluessel -, aber Host,
|
||||
# Benutzer und Serverpfad gehen niemanden etwas an. Vorlage: .env.beispiel
|
||||
.env
|
||||
|
|
|
|||
342
Operations_Research_mit_Python_Version_04/veroeffentliche_04.py
Executable file
342
Operations_Research_mit_Python_Version_04/veroeffentliche_04.py
Executable file
|
|
@ -0,0 +1,342 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# veroeffentliche_04.py
|
||||
#
|
||||
# Laedt OR_HTML_04/ auf den Webserver - mit rsync, nicht mit scp.
|
||||
#
|
||||
# Warum das der Unterschied ist: scp ueberschreibt nur, es loescht nie. Nach
|
||||
# dem Teilen von Anhang E/F blieb 'anhang-glossar-literatur.html' online
|
||||
# erreichbar, obwohl die Datei aus dem Buch verschwunden war - verlinkt von
|
||||
# nirgendwo, aber auffindbar. Das Zielverzeichnis vorher zu leeren waere die
|
||||
# grobe Antwort darauf: Bricht die Uebertragung von 23 MB in 213 Dateien ab,
|
||||
# ist die Website tot. rsync --delete uebertraegt nur Geaendertes und entfernt
|
||||
# genau das, was verschwunden ist.
|
||||
#
|
||||
# Der zweite Grund fuer dieses Skript wiegt schwerer: Die Website hing
|
||||
# wochenlang einen Bau zurueck, ohne dass es auffiel. Deshalb prueft Schritt 1,
|
||||
# ob der Stand ueberhaupt aktuell ist, und Schritt 4 prueft hinterher per HTTP
|
||||
# nach, ob das Ergebnis stimmt.
|
||||
#
|
||||
# Anmeldung per SSH-Schluessel. rsync und scp haben keine Passwort-Option; ein
|
||||
# Passwort braeuchte sshpass und staende waehrend der Uebertragung in der
|
||||
# Prozessliste. Einmalig:
|
||||
#
|
||||
# ssh-copy-id dschlueter@jamulix.de
|
||||
#
|
||||
# Aufruf:
|
||||
# python3 Operations_Research_mit_Python_Version_04/veroeffentliche_04.py
|
||||
# ... --probelauf
|
||||
#
|
||||
# Konfiguration in Version_04/.env (chmod 600, nicht im Repository) - siehe
|
||||
# .env.beispiel. Ein Geheimnis steht nicht darin.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
HIER = os.path.dirname(os.path.abspath(__file__))
|
||||
BASIS = os.path.dirname(HIER)
|
||||
|
||||
VERSION = "04"
|
||||
HTML_NAME = f"OR_HTML_{VERSION}"
|
||||
HTML_VERZ = os.path.join(BASIS, HTML_NAME)
|
||||
WEB_NAME = f"web_{VERSION}"
|
||||
PDF_DATEINAME = f"Operations_Research_mit_Python_Version_{VERSION}.pdf"
|
||||
|
||||
ENV_DATEI = os.path.join(BASIS, ".env")
|
||||
|
||||
# Verzeichnisse, deren Aenderung einen neuen Bau noetig macht. Ist eine Datei
|
||||
# darin neuer als die juengste erzeugte Seite, ist die Website veraltet.
|
||||
QUELLEN = [
|
||||
os.path.join(BASIS, f"Operations_Research_mit_Python_Version_{VERSION}"),
|
||||
os.path.join(BASIS, f"bilder_{VERSION}"),
|
||||
os.path.join(BASIS, WEB_NAME),
|
||||
]
|
||||
|
||||
# Was nach dem Hochladen erreichbar sein MUSS bzw. NICHT MEHR erreichbar sein
|
||||
# darf. Die 404-Zeile ist die eigentliche Probe darauf, dass --delete gewirkt
|
||||
# hat: Sie war der Anlass fuer dieses Skript.
|
||||
NACHPRUEFUNG = [
|
||||
("", 200),
|
||||
("index.html", 200),
|
||||
("notebooks.html", 200),
|
||||
("programme.html", 200),
|
||||
("anhang-glossar.html", 200),
|
||||
("anhang-literatur.html", 200),
|
||||
("stichwortverzeichnis.html", 200),
|
||||
("Notebooks_04.zip", 200),
|
||||
("Notebooks_04/", 200),
|
||||
("programme/", 200),
|
||||
("assets/site.css", 200),
|
||||
("katex/katex.min.js", 200),
|
||||
(PDF_DATEINAME, 200),
|
||||
("anhang-glossar-literatur.html", 404),
|
||||
]
|
||||
|
||||
LOESCHGRENZE = 30
|
||||
|
||||
|
||||
def lies_env() -> dict[str, str]:
|
||||
"""Liest .env - Zeilen der Form SCHLUESSEL=wert, '#' leitet einen
|
||||
Kommentar ein. Bewusst kein python-dotenv: fuenf Schluessel rechtfertigen
|
||||
keine Abhaengigkeit."""
|
||||
if not os.path.exists(ENV_DATEI):
|
||||
raise SystemExit(
|
||||
f"FEHLER: {ENV_DATEI} fehlt.\n"
|
||||
f" Vorlage kopieren und ausfuellen:\n"
|
||||
f" cp .env.beispiel .env && chmod 600 .env")
|
||||
werte = {}
|
||||
with open(ENV_DATEI, encoding="utf-8") as f:
|
||||
for zeile in f:
|
||||
zeile = zeile.split("#", 1)[0].strip()
|
||||
if not zeile or "=" not in zeile:
|
||||
continue
|
||||
schluessel, _, wert = zeile.partition("=")
|
||||
werte[schluessel.strip()] = wert.strip().strip('"').strip("'")
|
||||
fehlend = [s for s in ("OR_HOST", "OR_BENUTZER", "OR_ZIEL", "OR_URL")
|
||||
if not werte.get(s)]
|
||||
if fehlend:
|
||||
raise SystemExit(f"FEHLER: In .env fehlen: {', '.join(fehlend)}")
|
||||
return werte
|
||||
|
||||
|
||||
def juengste_datei(verzeichnisse: list[str]) -> tuple[float, str]:
|
||||
"""Liefert (Zeitstempel, Pfad) der zuletzt geaenderten Datei.
|
||||
|
||||
Dieses Skript selbst zaehlt nicht mit: Es aendert die Website nicht, sein
|
||||
Bearbeiten duerfte den Bau also nicht als veraltet erscheinen lassen."""
|
||||
neuste, pfad = 0.0, ""
|
||||
for verzeichnis in verzeichnisse:
|
||||
for wurzel, verz, dateien in os.walk(verzeichnis):
|
||||
verz[:] = [v for v in verz
|
||||
if v != "__pycache__" and not v.startswith(".")]
|
||||
for name in dateien:
|
||||
voll = os.path.join(wurzel, name)
|
||||
if os.path.abspath(voll) == os.path.abspath(__file__):
|
||||
continue
|
||||
try:
|
||||
zeit = os.path.getmtime(voll)
|
||||
except OSError:
|
||||
continue
|
||||
if zeit > neuste:
|
||||
neuste, pfad = zeit, voll
|
||||
return neuste, pfad
|
||||
|
||||
|
||||
def vorpruefung() -> None:
|
||||
"""Jede Verletzung bricht ab. Ein veralteter oder unvollstaendiger Stand
|
||||
soll gar nicht erst auf den Server kommen."""
|
||||
def scheitern(text: str) -> None:
|
||||
raise SystemExit(f"FEHLER: {text}")
|
||||
|
||||
bau_aufruf = (f"python3 Operations_Research_mit_Python_Version_{VERSION}/"
|
||||
f"build_version_{VERSION}.py --pdf --html")
|
||||
if not os.path.exists(os.path.join(HTML_VERZ, "index.html")):
|
||||
scheitern(f"{HTML_NAME}/index.html fehlt - erst bauen:\n"
|
||||
f" {bau_aufruf}")
|
||||
|
||||
# Ist der Bau aktuell? Verglichen wird gegen die juengste erzeugte Seite,
|
||||
# nicht gegen die aelteste: Der Bau schreibt alle Seiten in einem Zug.
|
||||
quell_zeit, quell_datei = juengste_datei(QUELLEN)
|
||||
bau_zeit, _ = juengste_datei([HTML_VERZ])
|
||||
if quell_zeit > bau_zeit:
|
||||
scheitern(
|
||||
f"Die Quelle ist neuer als die Website:\n"
|
||||
f" {os.path.relpath(quell_datei, BASIS)}\n"
|
||||
f" Erst bauen, dann hochladen - sonst geht ein alter Stand "
|
||||
f"online.\n"
|
||||
f" (Bewusst trotzdem hochladen: --ohne-vorpruefung)")
|
||||
|
||||
# Das PDF wurde schon einmal aus der Wurzel kopiert, ohne dass die Kopie
|
||||
# nachgezogen wurde - die Website bot ein aelteres PDF an als das Buch.
|
||||
pdf_wurzel = os.path.join(BASIS, PDF_DATEINAME)
|
||||
pdf_kopie = os.path.join(HTML_VERZ, PDF_DATEINAME)
|
||||
if not os.path.exists(pdf_kopie):
|
||||
scheitern(f"{HTML_NAME}/{PDF_DATEINAME} fehlt - mit --pdf --html bauen.")
|
||||
if os.path.exists(pdf_wurzel):
|
||||
gross_wurzel = os.path.getsize(pdf_wurzel)
|
||||
gross_kopie = os.path.getsize(pdf_kopie)
|
||||
if gross_wurzel != gross_kopie:
|
||||
scheitern(
|
||||
f"Das PDF der Website weicht vom gebauten ab "
|
||||
f"({gross_kopie:n} statt {gross_wurzel:n} Bytes).\n"
|
||||
f" Nochmal mit --html bauen, das kopiert es nach.")
|
||||
|
||||
# Ohne die beiden Weiterleitungen antwortet der Webserver auf die
|
||||
# Verzeichnisse mit 403 - genau das war der Fehlerbericht.
|
||||
pflicht = ["notebooks.html", "programme.html", f"Notebooks_{VERSION}.zip",
|
||||
f"Notebooks_{VERSION}/index.html", "programme/index.html"]
|
||||
fehlend = [d for d in pflicht
|
||||
if not os.path.exists(os.path.join(HTML_VERZ, d))]
|
||||
if fehlend:
|
||||
scheitern(f"In {HTML_NAME}/ fehlen: {', '.join(fehlend)}\n"
|
||||
f" Erst mit --html bauen.")
|
||||
|
||||
print("Vorpruefung bestanden: Bau ist aktuell und vollstaendig.")
|
||||
|
||||
|
||||
def ssh_optionen(env: dict[str, str]) -> str:
|
||||
teile = ["ssh"]
|
||||
if env.get("OR_SSH_PORT"):
|
||||
teile += ["-p", env["OR_SSH_PORT"]]
|
||||
if env.get("OR_SSH_KEY"):
|
||||
teile += ["-i", os.path.expanduser(env["OR_SSH_KEY"])]
|
||||
return " ".join(teile)
|
||||
|
||||
|
||||
def rsync_befehl(env: dict[str, str], probelauf: bool) -> list[str]:
|
||||
# -rlt statt -a: Eigentuemer und Gruppe lassen sich als normaler Nutzer
|
||||
# ohnehin nicht setzen, -a erzeugte dafuer nur Warnungen. --chmod sorgt
|
||||
# dafuer, dass der Webserver alles lesen kann, unabhaengig von der lokalen
|
||||
# umask.
|
||||
befehl = ["rsync", "-rltz", "--delete", "--chmod=D755,F644",
|
||||
"--skip-compress=pdf/zip/woff2/png/jpg/svg",
|
||||
"--itemize-changes",
|
||||
"-e", ssh_optionen(env)]
|
||||
if probelauf:
|
||||
befehl.append("--dry-run")
|
||||
ziel = env["OR_ZIEL"]
|
||||
befehl += [HTML_VERZ + os.sep,
|
||||
f'{env["OR_BENUTZER"]}@{env["OR_HOST"]}:{ziel}']
|
||||
return befehl
|
||||
|
||||
|
||||
def pruefe_ziel_erreichbar(env: dict[str, str]) -> None:
|
||||
"""Ein Handschlag, bevor 23 MB losgeschickt werden - und er unterscheidet
|
||||
die beiden Fehlerursachen, die sonst leicht verwechselt werden: Anmeldung
|
||||
scheitert (ssh liefert 255) gegen rsync fehlt am Ziel."""
|
||||
kennung = f'{env["OR_BENUTZER"]}@{env["OR_HOST"]}'
|
||||
# BatchMode: keine Passwortabfrage, damit der Aufruf nicht haengt. Der
|
||||
# ssh-agent funktioniert weiterhin. stderr laeuft absichtlich durch, damit
|
||||
# ssh seine eigene Diagnose zeigen kann.
|
||||
ergebnis = subprocess.run(
|
||||
ssh_optionen(env).split() + ["-o", "BatchMode=yes", kennung,
|
||||
"command -v rsync || echo KEIN_RSYNC"],
|
||||
stdout=subprocess.PIPE, text=True)
|
||||
if ergebnis.returncode == 255:
|
||||
raise SystemExit(
|
||||
f"FEHLER: Keine SSH-Verbindung zu {kennung} (siehe Meldung oben).\n"
|
||||
f" Haeufigste Ursache: Der oeffentliche Schluessel liegt noch\n"
|
||||
f" nicht auf dem Server. Einmalig:\n"
|
||||
f" ssh-copy-id {kennung}\n"
|
||||
f" Bei passwortgeschuetztem Schluessel: ssh-add einmal "
|
||||
f"ausfuehren.")
|
||||
if ergebnis.returncode != 0:
|
||||
raise SystemExit(f"FEHLER: ssh {kennung} endete mit Code "
|
||||
f"{ergebnis.returncode}.")
|
||||
if "KEIN_RSYNC" in ergebnis.stdout or not ergebnis.stdout.strip():
|
||||
raise SystemExit(
|
||||
"FEHLER: Auf dem Server ist kein rsync installiert.\n"
|
||||
" Ausweg ohne rsync - nach OR.neu/ hochladen und tauschen;\n"
|
||||
" das entfernt ebenfalls Altes und ist fuer Besucher fast\n"
|
||||
" unterbrechungsfrei:\n"
|
||||
f" scp -r {HTML_NAME}/* {kennung}:{env['OR_ZIEL']}../OR.neu/\n"
|
||||
f' ssh {kennung} '
|
||||
f'"mv OR OR.alt && mv OR.neu OR && rm -rf OR.alt"')
|
||||
|
||||
|
||||
def zaehle_aenderungen(ausgabe: str) -> tuple[list[str], list[str]]:
|
||||
"""Trennt die --itemize-changes-Ausgabe in Uebertragungen und Loeschungen."""
|
||||
uebertragen, geloescht = [], []
|
||||
for zeile in ausgabe.splitlines():
|
||||
if zeile.startswith("*deleting"):
|
||||
geloescht.append(zeile.split(None, 1)[-1])
|
||||
elif zeile[:1] in ("<", ">", "c", "h") and " " in zeile:
|
||||
uebertragen.append(zeile.split(None, 1)[-1])
|
||||
return uebertragen, geloescht
|
||||
|
||||
|
||||
def nachpruefung(basis_url: str) -> bool:
|
||||
"""Holt jede Adresse und vergleicht den Statuscode mit der Erwartung."""
|
||||
print("\nNachpruefung:")
|
||||
alles_gut = True
|
||||
for pfad, erwartet in NACHPRUEFUNG:
|
||||
url = basis_url.rstrip("/") + "/" + pfad
|
||||
anfrage = urllib.request.Request(url, method="HEAD")
|
||||
try:
|
||||
with urllib.request.urlopen(anfrage, timeout=20) as antwort:
|
||||
code = antwort.status
|
||||
except urllib.error.HTTPError as fehler:
|
||||
code = fehler.code
|
||||
except OSError as fehler:
|
||||
code = f"{type(fehler).__name__}"
|
||||
passt = code == erwartet
|
||||
alles_gut = alles_gut and passt
|
||||
print(f" {'OK ' if passt else 'FEHLER'} {code!s:>6} "
|
||||
f"(erwartet {erwartet}) /{pfad}")
|
||||
return alles_gut
|
||||
|
||||
|
||||
def main() -> int:
|
||||
zerleger = argparse.ArgumentParser(
|
||||
description="Laedt OR_HTML_04/ auf den Webserver (rsync ueber SSH).")
|
||||
zerleger.add_argument("--probelauf", action="store_true",
|
||||
help="alles ausser der echten Uebertragung")
|
||||
zerleger.add_argument("--loeschgrenze", type=int, default=LOESCHGRENZE,
|
||||
help=f"Abbruch, wenn mehr Dateien geloescht wuerden "
|
||||
f"(Vorgabe: {LOESCHGRENZE})")
|
||||
zerleger.add_argument("--ohne-vorpruefung", action="store_true",
|
||||
help="Schritt 1 ueberspringen (bewusst alter Stand)")
|
||||
zerleger.add_argument("--ohne-nachpruefung", action="store_true",
|
||||
help="Schritt 4 ueberspringen (kein Netz zum Server)")
|
||||
argumente = zerleger.parse_args()
|
||||
|
||||
env = lies_env()
|
||||
ziel = f'{env["OR_BENUTZER"]}@{env["OR_HOST"]}:{env["OR_ZIEL"]}'
|
||||
print(f"Ziel: {ziel}")
|
||||
|
||||
if not argumente.ohne_vorpruefung:
|
||||
vorpruefung()
|
||||
pruefe_ziel_erreichbar(env)
|
||||
|
||||
# Schritt 2: Immer erst zaehlen. Der Probelauf kostet Sekunden und ist die
|
||||
# einzige Gelegenheit, einen falschen Zielpfad zu bemerken, BEVOR --delete
|
||||
# eine fremde Website ausraeumt.
|
||||
probe = subprocess.run(rsync_befehl(env, probelauf=True),
|
||||
capture_output=True, text=True)
|
||||
if probe.returncode != 0:
|
||||
print(probe.stderr, file=sys.stderr)
|
||||
return probe.returncode
|
||||
uebertragen, geloescht = zaehle_aenderungen(probe.stdout)
|
||||
print(f"\nZu uebertragen: {len(uebertragen)} Datei(en)")
|
||||
for name in uebertragen[:15]:
|
||||
print(f" + {name}")
|
||||
if len(uebertragen) > 15:
|
||||
print(f" ... und {len(uebertragen) - 15} weitere")
|
||||
print(f"Zu loeschen: {len(geloescht)} Datei(en)")
|
||||
for name in geloescht:
|
||||
print(f" - {name}")
|
||||
|
||||
if len(geloescht) > argumente.loeschgrenze:
|
||||
print(f"\nFEHLER: {len(geloescht)} Loeschungen ueberschreiten die Grenze "
|
||||
f"von {argumente.loeschgrenze}.\n"
|
||||
f" Das ist meist ein falscher Zielpfad oder ein halb "
|
||||
f"gebautes {HTML_NAME}/.\n"
|
||||
f" Wenn es stimmt: --loeschgrenze {len(geloescht)}",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if argumente.probelauf:
|
||||
print("\nProbelauf - es wurde nichts uebertragen.")
|
||||
return 0
|
||||
|
||||
if not uebertragen and not geloescht:
|
||||
print("\nNichts zu tun - der Server ist auf dem Stand des Baus.")
|
||||
else:
|
||||
print("\nUebertrage ...")
|
||||
lauf = subprocess.run(rsync_befehl(env, probelauf=False))
|
||||
if lauf.returncode != 0:
|
||||
return lauf.returncode
|
||||
print(f"Fertig: {len(uebertragen)} uebertragen, "
|
||||
f"{len(geloescht)} geloescht.")
|
||||
|
||||
if argumente.ohne_nachpruefung:
|
||||
return 0
|
||||
return 0 if nachpruefung(env["OR_URL"]) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
39
PROGRESS.md
39
PROGRESS.md
|
|
@ -2078,6 +2078,45 @@ Gegenprobe: drei Läufe mit `PYTHONHASHSEED=random` liefern dieselbe Prüfsumme
|
|||
`3da5b9b2…`. Keine Hauptzeile trägt mehr einen doppelten Link; die 219 Fachbegriffe und 328
|
||||
Indexmarken bleiben unverändert.
|
||||
|
||||
### ✅ `veroeffentliche_04.py` — der Upload wird nachprüfbar
|
||||
|
||||
Der Anlass war messbar. Der Live-Stand vor dem Skript:
|
||||
|
||||
```
|
||||
/OR/ 200
|
||||
/OR/anhang-glossar-literatur.html 200 <- Leiche des vorigen Stands
|
||||
/OR/anhang-glossar.html 404
|
||||
/OR/notebooks.html 404
|
||||
/OR/Notebooks_04.zip 404
|
||||
/OR/Notebooks_04/ 403
|
||||
```
|
||||
|
||||
**`scp -r` überschreibt nur, es löscht nie.** Die alte Glossar-Seite bliebe nach einem
|
||||
`scp`-Upload dauerhaft erreichbar. Das Zielverzeichnis vorher zu leeren wäre die grobe
|
||||
Antwort: Bricht die Übertragung von 23 MB in 213 Dateien ab, ist die Website tot.
|
||||
`rsync --delete` überträgt nur Geändertes und entfernt genau das, was verschwunden ist.
|
||||
|
||||
Der zweite Grund wiegt schwerer: **Die Website hing wochenlang einen Bau zurück**, ohne dass
|
||||
es auffiel. Deshalb prüft das Skript vorher, ob eine Quelldatei neuer ist als die gebaute
|
||||
Website, ob das PDF der Website byte-genau dem gebauten entspricht und ob die
|
||||
Verzeichnis-Weiterleitungen vorhanden sind (der Grund für den 403). Und es prüft
|
||||
**hinterher per HTTP** vierzehn Adressen nach — einschließlich der Probe, dass die entfernte
|
||||
Seite wirklich 404 liefert.
|
||||
|
||||
Dazwischen ein Probelauf, der zählt: Mehr als 30 Löschungen brechen ab. Das ist der Schutz
|
||||
gegen einen falschen Zielpfad — mit `--delete` räumt ein Tippfehler sonst ein fremdes
|
||||
Verzeichnis aus.
|
||||
|
||||
**Anmeldung per SSH-Schlüssel, nicht per Passwort in der `.env`.** `rsync` und `scp` haben
|
||||
keine Passwort-Option; ein Passwort bräuchte `sshpass` und stünde während der Übertragung in
|
||||
der Prozessliste. Die `.env` (in `.gitignore`, Vorlage `.env.beispiel`) enthält deshalb nur
|
||||
Host, Benutzer, Zielpfad und URL.
|
||||
|
||||
**Beim ersten Test lief die Prüfung in eine irreführende Meldung:** „Auf dem Server ist kein
|
||||
rsync installiert" — tatsächlich schlug schon die Anmeldung fehl, weil der öffentliche
|
||||
Schlüssel noch nicht auf `jamulix.de` liegt. Die Prüfung unterscheidet die Fälle jetzt am
|
||||
ssh-Rückgabewert 255 und nennt `ssh-copy-id`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Commit-Historie des V04-Strangs
|
||||
|
|
|
|||
46
README.md
46
README.md
|
|
@ -115,6 +115,7 @@ pip install -r Operations_Research_mit_Python_Version_04_Programme/requirements.
|
|||
| --- | --- |
|
||||
| `Operations_Research_mit_Python_Version_04/` | **Quelle**: 37 Kapiteldateien (inkl. 5 Teil-Synthesen) + Build-Skripte |
|
||||
| `bilder_04/` | **Quelle**: 33 Diagramme (SVG) + 16 `erzeuge_*.py`-Generatoren |
|
||||
| `web_04/` | **Quelle**: `site.css`, `site.js`, `icons.svg`, `plotly.min.js`, `katex/` — die statischen Bestandteile der Website |
|
||||
| `Kritik_und_Verbesserungsvorschlaege/` | **Quelle**: die Rezensionen und Vorschläge, die den Umbau ausgelöst haben |
|
||||
| `pyproject.toml`, `Dockerfile`, `.dockerignore` | **Quelle**: Abhängigkeitsgruppen und Kurs-Image |
|
||||
| `pandoc-defaults-*.yaml`, `pandoc/` | **Quelle**: Konfiguration des PDF-Baus |
|
||||
|
|
@ -166,8 +167,9 @@ Prüfung hinterlassen.
|
|||
| `inkscape` | SVG-Grafiken im PDF | LaTeX bricht bei `\includesvg` ab |
|
||||
| `dot` (Graphviz) | 5 der 16 Bildgeneratoren | nur beim Neuerzeugen dieser Diagramme nötig |
|
||||
|
||||
**`OR_HTML_04/katex/`** ist externes Material und wird von keinem Skript erzeugt. Fehlt es,
|
||||
bleiben die Formeln auf der Website ungesetzt.
|
||||
**`web_04/katex/`** ist externes Material und wird von keinem Skript erzeugt — es liegt
|
||||
deshalb als Quelle im Repository und wird bei jedem Bau nach `OR_HTML_04/katex/` kopiert.
|
||||
Fehlt es, bleiben die Formeln auf der Website ungesetzt.
|
||||
|
||||
**Warum die PDF-Konfiguration doppelt vorliegt:** `pandoc-defaults-basis.yaml` ist eine Kopie
|
||||
der Benutzerdatei `~/.config/pandoc/defaults.yaml` — mit **relativem** Pfad auf
|
||||
|
|
@ -177,6 +179,46 @@ Benutzerdatei zurück, falls sie fehlt.
|
|||
|
||||
---
|
||||
|
||||
## Veröffentlichen
|
||||
|
||||
`OR_HTML_04/` ist selbstgenügsam: Der Ordner lässt sich direkt auf einen Webserver kopieren.
|
||||
Für den regelmäßigen Fall gibt es ein Skript.
|
||||
|
||||
```bash
|
||||
# einmalig: den eigenen SSH-Schlüssel auf den Server legen
|
||||
ssh-copy-id benutzer@example.org
|
||||
|
||||
# einmalig: Ziel eintragen
|
||||
cp .env.beispiel .env && chmod 600 .env # dann .env ausfüllen
|
||||
|
||||
# jedes Mal
|
||||
python3 Operations_Research_mit_Python_Version_04/veroeffentliche_04.py
|
||||
```
|
||||
|
||||
Das Skript benutzt **`rsync --delete`, nicht `scp`** — und das ist der entscheidende
|
||||
Unterschied: `scp` überschreibt nur, es entfernt nie. Eine Seite, die aus dem Buch
|
||||
verschwindet, bliebe sonst dauerhaft online erreichbar. `rsync` überträgt zudem nur, was sich
|
||||
geändert hat; ein zweiter Lauf direkt danach überträgt nichts.
|
||||
|
||||
Vier Schritte, jeder mit einer eigenen Aufgabe:
|
||||
|
||||
1. **Vorprüfung.** Ist eine Quelldatei neuer als die gebaute Website, bricht das Skript ab —
|
||||
ein alter Stand soll gar nicht erst hochgeladen werden. Ebenso geprüft: dass das PDF der
|
||||
Website byte-genau dem gebauten entspricht und dass die Verzeichnis-Weiterleitungen da
|
||||
sind.
|
||||
2. **Probelauf.** Zählt und zeigt, was übertragen und was gelöscht würde. Über 30 Löschungen
|
||||
ist der Lauf verdächtig (falscher Zielpfad, halb gebautes Verzeichnis) und bricht ab;
|
||||
`--loeschgrenze N` hebt die Grenze an.
|
||||
3. **Übertragung.**
|
||||
4. **Nachprüfung per HTTP.** Vierzehn Adressen werden abgerufen und ihr Statuscode mit der
|
||||
Erwartung verglichen — einschließlich der Probe, dass eine entfernte Seite wirklich 404
|
||||
liefert.
|
||||
|
||||
`--probelauf` führt nur die Schritte 1 und 2 aus. In der `.env` steht **kein Geheimnis**:
|
||||
`rsync` und `scp` haben keine Passwort-Option, die Anmeldung läuft über den SSH-Schlüssel.
|
||||
|
||||
---
|
||||
|
||||
## Notebooks und Google Colab
|
||||
|
||||
Jedes Kapitel hat ein Notebook in `Notebooks_04/`, und die Website verlinkt es zum
|
||||
|
|
|
|||
Loading…
Reference in a new issue