Files
iwz/lib/deploy_lastdiff.py
Michael Stangl 2379f4ac9f @
Skripte nach bin/ und lib/ umstrukturiert + WSL-Log-Analyse

Alle .bat-Skripte liegen jetzt in bin/, alle .py in lib/, .env(.example)
in bin/. setenv.bat setzt IWZ_* (inkl. IWZ_ENV) und PYTHONPATH; die
Launcher rufen setenv, aktivieren die .venv falls vorhanden und starten
das zugehoerige lib/-Skript. Die Python-Skripte lesen die .env ueber
IWZ_ENV (Fallback bin/.env).

Neu: Nutzungsanalyse per GoAccess (analyze.py/.bat) - laedt die
IONOS-Access-Logs per SFTP (paramiko) und erzeugt einen HTML-Report.
GoAccess laeuft ueber WSL (GOACCESS_MODE=wsl, Pfad-Uebersetzung nach
/mnt/c). install_py.bat prueft WSL+GoAccess und zeigt sonst den
Installationsweg. Doku in doc/analyse.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-08-28 16:44:56 +02:00

159 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""
IWZ Ferienwohnungen - Deploy nur geaenderter Dateien
Baut die Hugo-Seite und laedt per FTP zur Bunny.net Storage Zone nur die
Dateien hoch, die sich seit dem letzten Deploy mit diesem Skript geaendert
haben (Vergleich per Datei-Hash gegen ein lokales Manifest).
"""
import ftplib
import hashlib
import json
import os
import subprocess
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/
PUBLIC_DIR = PROJECT_ROOT / "public"
MANIFEST_PATH = PROJECT_ROOT / ".deploy_manifest.json"
FTP_PORT = 21
def env_file():
"""Pfad zur .env: bevorzugt IWZ_ENV (von setenv.bat), sonst bin/.env."""
from_var = os.environ.get("IWZ_ENV")
if from_var:
return Path(from_var)
return PROJECT_ROOT / "bin" / ".env"
def load_env(path):
env = {}
if not path.exists():
return env
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
env[key.strip()] = value.strip()
return env
def hugo_build():
print("\n-> Hugo Build...")
result = subprocess.run("hugo --gc --minify", shell=True)
if result.returncode != 0:
print("\nFEHLER: Hugo Build fehlgeschlagen!")
sys.exit(1)
def hash_file(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def scan_public():
manifest = {}
for entry in PUBLIC_DIR.rglob("*"):
if entry.is_file():
rel = entry.relative_to(PUBLIC_DIR).as_posix()
manifest[rel] = hash_file(entry)
return manifest
def load_manifest():
if not MANIFEST_PATH.exists():
return {}
return json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
def save_manifest(manifest):
MANIFEST_PATH.write_text(json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8")
def diff_manifest(old, new):
changed = [rel for rel, digest in new.items() if old.get(rel) != digest]
deleted = [rel for rel in old if rel not in new]
return sorted(changed), sorted(deleted)
def ftp_mkdirs(ftp, remote_dir):
parts = remote_dir.split("/")
path = ""
for part in parts:
if not part:
continue
path = f"{path}/{part}" if path else part
try:
ftp.mkd(path)
except ftplib.error_perm:
pass # existiert bereits
def ftp_upload_files(ftp, rel_paths):
for rel in rel_paths:
local_path = PUBLIC_DIR / rel
remote_dir = "/".join(rel.split("/")[:-1])
if remote_dir:
ftp_mkdirs(ftp, remote_dir)
with open(local_path, "rb") as f:
ftp.storbinary(f"STOR {rel}", f)
print(f" {rel}")
def ftp_delete_files(ftp, rel_paths):
for rel in rel_paths:
try:
ftp.delete(rel)
print(f" gelöscht: {rel}")
except ftplib.error_perm as e:
print(f" konnte nicht löschen: {rel} ({e})")
def deploy_to_bunny(host, zone, password, changed, deleted):
print(f"\n-> Deploy zu Bunny.net Storage Zone '{zone}'...")
with ftplib.FTP() as ftp:
ftp.connect(host, FTP_PORT)
ftp.login(zone, password)
ftp.set_pasv(True)
if changed:
print(f"\n-> Lade {len(changed)} geänderte Datei(en) hoch:")
ftp_upload_files(ftp, changed)
if deleted:
print(f"\n-> Entferne {len(deleted)} gelöschte Datei(en):")
ftp_delete_files(ftp, deleted)
if __name__ == "__main__":
print("=== IWZ Deploy (nur Änderungen) ===")
env = {**load_env(env_file()), **os.environ}
zone = env.get("BUNNY_STORAGE_ZONE")
host = env.get("BUNNY_STORAGE_HOST", "storage.bunnycdn.com")
password = env.get("BUNNY_STORAGE_PASSWORD")
if not zone or not password:
print("\nFEHLER: BUNNY_STORAGE_ZONE / BUNNY_STORAGE_PASSWORD fehlen.")
print("Siehe bin/.env.example -> bin/.env kopieren und befuellen.")
sys.exit(1)
hugo_build()
old_manifest = load_manifest()
new_manifest = scan_public()
changed, deleted = diff_manifest(old_manifest, new_manifest)
if not changed and not deleted:
print("\nKeine Änderungen seit dem letzten Deploy. Nichts hochzuladen.")
sys.exit(0)
deploy_to_bunny(host, zone, password, changed, deleted)
save_manifest(new_manifest)
print("\nFertig! Änderungen sind live.")