@
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> @
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
#!/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.")
|
||||
Reference in New Issue
Block a user