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> @
159 lines
4.5 KiB
Python
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.")
|