@
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:
+226
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
IWZ Ferienwohnungen - Nutzungsanalyse
|
||||
|
||||
Laedt die Apache-Access-Logs per SFTP von IONOS herunter, entpackt die
|
||||
rotierten .gz-Dateien und erzeugt mit GoAccess einen HTML-Report.
|
||||
|
||||
Kein JavaScript-Tracking, keine Cookies beim Besucher -> DSGVO-freundlich.
|
||||
Die Auswertung passiert rein aus den Server-Logs.
|
||||
|
||||
Voraussetzungen:
|
||||
- pip install -r requirements.txt (paramiko)
|
||||
- GoAccess Windows-Build (Pfad in .env: GOACCESS_EXE)
|
||||
- .env mit IONOS_SFTP_* befuellt (siehe .env.example)
|
||||
|
||||
Aufruf:
|
||||
python analyze.py # Logs laden + Report bauen + oeffnen
|
||||
python analyze.py --no-download # nur neu auswerten (schon geladene Logs)
|
||||
"""
|
||||
|
||||
import gzip
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import webbrowser
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import paramiko
|
||||
except ImportError:
|
||||
print("FEHLER: paramiko fehlt. Bitte 'pip install -r requirements.txt' ausfuehren.")
|
||||
sys.exit(1)
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/
|
||||
LOGS_DIR = PROJECT_ROOT / "logs" # heruntergeladene Rohlogs (in .gitignore)
|
||||
RESULTS_DIR = PROJECT_ROOT / "results" # erzeugte Reports (in .gitignore)
|
||||
SFTP_PORT = 22
|
||||
|
||||
|
||||
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"
|
||||
|
||||
# Nur diese Dateien aus dem IONOS /logs-Ordner interessieren uns.
|
||||
# Alles andere (sftp.log, traffic.db, forensic/, info.*) wird ignoriert.
|
||||
ACCESS_LOG_PREFIX = "access.log"
|
||||
|
||||
|
||||
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 download_access_logs(host, port, user, password, remote_dir):
|
||||
"""Laedt alle access.log*-Dateien aus dem IONOS /logs-Ordner nach LOGS_DIR."""
|
||||
print(f"\n-> Verbinde per SFTP mit {host}:{port} ...")
|
||||
transport = paramiko.Transport((host, port))
|
||||
try:
|
||||
transport.connect(username=user, password=password)
|
||||
sftp = paramiko.SFTPClient.from_transport(transport)
|
||||
sftp.chdir(remote_dir)
|
||||
|
||||
LOGS_DIR.mkdir(exist_ok=True)
|
||||
count = 0
|
||||
for attr in sftp.listdir_attr("."):
|
||||
name = attr.filename
|
||||
if not name.startswith(ACCESS_LOG_PREFIX):
|
||||
continue
|
||||
if stat.S_ISDIR(attr.st_mode):
|
||||
continue
|
||||
local_path = LOGS_DIR / name
|
||||
# Schon vorhanden und gleich gross? -> ueberspringen (rotierte Logs
|
||||
# aendern sich nicht mehr; nur .current / aktueller Tag waechst).
|
||||
if (local_path.exists()
|
||||
and local_path.stat().st_size == attr.st_size
|
||||
and not name.endswith(".current")):
|
||||
continue
|
||||
print(f" lade {name} ({attr.st_size} Bytes)")
|
||||
sftp.get(name, str(local_path))
|
||||
count += 1
|
||||
sftp.close()
|
||||
print(f" {count} Datei(en) geladen/aktualisiert.")
|
||||
finally:
|
||||
transport.close()
|
||||
|
||||
|
||||
def collect_log_lines():
|
||||
"""Fuehrt alle access.log*-Dateien (auch .gz) zu einem kombinierten Log zusammen.
|
||||
|
||||
Gibt den Pfad zur kombinierten Klartext-Logdatei zurueck.
|
||||
"""
|
||||
combined = LOGS_DIR / "_combined.log"
|
||||
sources = sorted(
|
||||
p for p in LOGS_DIR.iterdir()
|
||||
if p.is_file()
|
||||
and p.name.startswith(ACCESS_LOG_PREFIX)
|
||||
and p.name != combined.name
|
||||
)
|
||||
if not sources:
|
||||
print("\nFEHLER: keine access.log*-Dateien in ./logs/ gefunden.")
|
||||
print("Erst 'python analyze.py' (ohne --no-download) ausfuehren.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n-> Fuehre {len(sources)} Logdatei(en) zusammen ...")
|
||||
with open(combined, "wb") as out:
|
||||
for src in sources:
|
||||
if src.suffix == ".gz":
|
||||
with gzip.open(src, "rb") as f:
|
||||
shutil.copyfileobj(f, out)
|
||||
else:
|
||||
with open(src, "rb") as f:
|
||||
shutil.copyfileobj(f, out)
|
||||
return combined
|
||||
|
||||
|
||||
def to_wsl_path(win_path):
|
||||
"""Uebersetzt einen Windows-Pfad (C:\\...) in einen WSL-Pfad (/mnt/c/...)."""
|
||||
p = Path(win_path).resolve()
|
||||
drive = p.drive.rstrip(":").lower() # "C:" -> "c"
|
||||
rest = p.as_posix()[len(p.drive):] # Rest ohne Laufwerk, mit /
|
||||
return f"/mnt/{drive}{rest}"
|
||||
|
||||
|
||||
def goaccess_args(log_format):
|
||||
"""Gemeinsame GoAccess-Optionen (ohne Ein-/Ausgabedateien)."""
|
||||
return [
|
||||
"--log-format=" + log_format,
|
||||
"--anonymize-ip", # DSGVO: IPs im Report anonymisieren
|
||||
"--ignore-crawlers", # Bots aus den Besucherzahlen filtern
|
||||
"--html-report-title=IWZ Ferienwohnungen - Zugriffsstatistik",
|
||||
]
|
||||
|
||||
|
||||
def run_goaccess(mode, goaccess_exe, log_file, report_file, log_format):
|
||||
"""Ruft GoAccess auf und erzeugt einen eigenstaendigen HTML-Report.
|
||||
|
||||
mode="wsl": GoAccess laeuft in der WSL (Standard), Pfade werden nach
|
||||
/mnt/c/... uebersetzt.
|
||||
mode="exe": nativer Windows-Build unter GOACCESS_EXE.
|
||||
"""
|
||||
print(f"\n-> GoAccess ({mode}) -> {report_file.name}")
|
||||
|
||||
if mode == "wsl":
|
||||
cmd = [
|
||||
"wsl", "goaccess",
|
||||
to_wsl_path(log_file),
|
||||
"-o", to_wsl_path(report_file),
|
||||
] + goaccess_args(log_format)
|
||||
not_found_hint = (
|
||||
"GoAccess ist in der WSL nicht installiert (oder WSL fehlt).\n"
|
||||
" In der WSL installieren: wsl -e sudo apt update ^&^& "
|
||||
"sudo apt install -y goaccess\n"
|
||||
"Siehe auch bin/install_py.bat (prueft WSL + GoAccess) und doc/analyse.md."
|
||||
)
|
||||
else:
|
||||
cmd = [
|
||||
goaccess_exe,
|
||||
str(log_file),
|
||||
"-o", str(report_file),
|
||||
] + goaccess_args(log_format)
|
||||
not_found_hint = (
|
||||
f"GoAccess-EXE nicht gefunden ('{goaccess_exe}').\n"
|
||||
" Windows-Build laden: https://goaccess.io/download, entpacken,\n"
|
||||
" dann GOACCESS_EXE in bin/.env auf die goaccess.exe setzen."
|
||||
)
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd)
|
||||
except FileNotFoundError:
|
||||
print("\nFEHLER: GoAccess konnte nicht gestartet werden.")
|
||||
print(not_found_hint)
|
||||
sys.exit(1)
|
||||
if result.returncode != 0:
|
||||
print("\nFEHLER: GoAccess fehlgeschlagen.")
|
||||
print("Pruefe die GoAccess-Installation und das Log-Format (LOG_FORMAT).")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== IWZ Nutzungsanalyse ===")
|
||||
no_download = "--no-download" in sys.argv
|
||||
|
||||
env = {**load_env(env_file()), **os.environ}
|
||||
host = env.get("IONOS_SFTP_HOST")
|
||||
port = int(env.get("IONOS_SFTP_PORT", SFTP_PORT))
|
||||
user = env.get("IONOS_SFTP_USER")
|
||||
password = env.get("IONOS_SFTP_PASSWORD")
|
||||
remote_dir = env.get("IONOS_LOGS_DIR", "/logs")
|
||||
# GoAccess-Aufruf: "wsl" (Standard) oder "exe" (nativer Windows-Build).
|
||||
goaccess_mode = env.get("GOACCESS_MODE", "wsl").strip().lower()
|
||||
goaccess_exe = env.get("GOACCESS_EXE", "goaccess")
|
||||
# IONOS liefert i.d.R. Apache "Combined Log Format". Bei anderen
|
||||
# Formaten hier anpassen (siehe doc/analyse.md).
|
||||
log_format = env.get("LOG_FORMAT", "COMBINED")
|
||||
|
||||
if not no_download:
|
||||
if not host or not user or not password:
|
||||
print("\nFEHLER: IONOS_SFTP_HOST / _USER / _PASSWORD fehlen in bin/.env.")
|
||||
print("Siehe bin/.env.example. Oder mit --no-download nur neu auswerten.")
|
||||
sys.exit(1)
|
||||
download_access_logs(host, port, user, password, remote_dir)
|
||||
else:
|
||||
print("\n-> Download uebersprungen (--no-download).")
|
||||
|
||||
combined = collect_log_lines()
|
||||
|
||||
RESULTS_DIR.mkdir(exist_ok=True)
|
||||
report_file = RESULTS_DIR / f"report_{date.today().isoformat()}.html"
|
||||
run_goaccess(goaccess_mode, goaccess_exe, combined, report_file, log_format)
|
||||
|
||||
print(f"\nFertig! Report: {report_file}")
|
||||
webbrowser.open(report_file.as_uri())
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
IWZ Ferienwohnungen - Deploy Script
|
||||
Baut die Hugo-Seite und deployt sie per FTP zur Bunny.net Storage Zone.
|
||||
"""
|
||||
|
||||
import ftplib
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/
|
||||
PUBLIC_DIR = PROJECT_ROOT / "public"
|
||||
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 ftp_upload_dir(ftp, local_dir, remote_dir=""):
|
||||
for entry in sorted(local_dir.iterdir()):
|
||||
remote_path = f"{remote_dir}/{entry.name}" if remote_dir else entry.name
|
||||
if entry.is_dir():
|
||||
try:
|
||||
ftp.mkd(remote_path)
|
||||
except ftplib.error_perm:
|
||||
pass # existiert bereits
|
||||
ftp_upload_dir(ftp, entry, remote_path)
|
||||
else:
|
||||
with open(entry, "rb") as f:
|
||||
ftp.storbinary(f"STOR {remote_path}", f)
|
||||
print(f" {remote_path}")
|
||||
|
||||
|
||||
def deploy_to_bunny(host, zone, password):
|
||||
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)
|
||||
ftp_upload_dir(ftp, PUBLIC_DIR)
|
||||
|
||||
|
||||
def purge_cdn_cache(pull_zone_url, api_key):
|
||||
"""Leert den Bunny CDN-Cache der Pull Zone per Wildcard-URL-Purge.
|
||||
|
||||
Ohne Purge liefert das CDN unveraenderte Dateinamen (z.B. /css/style.css)
|
||||
weiter aus dem Cache -> online sieht man die alte Version.
|
||||
"""
|
||||
if not pull_zone_url or not api_key:
|
||||
print("\n-> CDN-Purge uebersprungen "
|
||||
"(BUNNY_PULL_ZONE_URL / BUNNY_API_KEY nicht gesetzt).")
|
||||
print(" Cache ggf. manuell im Bunny-Dashboard leeren.")
|
||||
return
|
||||
|
||||
wildcard = pull_zone_url.rstrip("/") + "/*"
|
||||
query = urllib.parse.urlencode({"url": wildcard, "async": "false"})
|
||||
url = f"https://api.bunny.net/purge?{query}"
|
||||
print(f"\n-> CDN-Cache leeren: {wildcard}")
|
||||
|
||||
req = urllib.request.Request(url, method="POST")
|
||||
req.add_header("AccessKey", api_key)
|
||||
req.add_header("Accept", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
if resp.status == 200:
|
||||
print(" Cache geleert.")
|
||||
else:
|
||||
print(f" WARNUNG: unerwarteter Status {resp.status}")
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f" WARNUNG: Purge fehlgeschlagen (HTTP {e.code}). "
|
||||
"Ist BUNNY_API_KEY der Account-API-Key?")
|
||||
except urllib.error.URLError as e:
|
||||
print(f" WARNUNG: Purge fehlgeschlagen ({e.reason}).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== IWZ Deploy ===")
|
||||
|
||||
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")
|
||||
pull_zone_url = env.get("BUNNY_PULL_ZONE_URL")
|
||||
api_key = env.get("BUNNY_API_KEY")
|
||||
|
||||
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()
|
||||
deploy_to_bunny(host, zone, password)
|
||||
purge_cdn_cache(pull_zone_url, api_key)
|
||||
|
||||
print("\nFertig! Seite ist live.")
|
||||
@@ -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.")
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
IWZ Ferienwohnungen - erzeugt einen befristeten, signierten Vorschau-Link
|
||||
fuer die per Bunny Token Authentication geschuetzte Pull Zone.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent # Skript liegt in lib/
|
||||
GUELTIGKEIT_SEKUNDEN = 7 * 24 * 60 * 60 # 7 Tage
|
||||
URL_PATH = "/"
|
||||
|
||||
|
||||
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 build_signed_url(pull_zone_url, auth_key, url_path, expires):
|
||||
hashable = f"{auth_key}{url_path}{expires}"
|
||||
token_hash = hashlib.sha256(hashable.encode()).digest()
|
||||
token = base64.urlsafe_b64encode(token_hash).decode().rstrip("=")
|
||||
return f"{pull_zone_url}{url_path}?token={token}&expires={expires}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
env = load_env(env_file())
|
||||
pull_zone_url = env.get("BUNNY_PULL_ZONE_URL", "").rstrip("/")
|
||||
auth_key = env.get("BUNNY_AUTH_KEY")
|
||||
|
||||
if not pull_zone_url or not auth_key:
|
||||
print("\nFEHLER: BUNNY_PULL_ZONE_URL / BUNNY_AUTH_KEY fehlen in bin/.env.")
|
||||
print("Siehe .env.example. Auth Key: Pull Zone -> Security -> Token Authentication.")
|
||||
sys.exit(1)
|
||||
|
||||
expires = int(time.time()) + GUELTIGKEIT_SEKUNDEN
|
||||
link = build_signed_url(pull_zone_url, auth_key, URL_PATH, expires)
|
||||
|
||||
gueltig_bis = time.strftime("%d.%m.%Y %H:%M", time.localtime(expires))
|
||||
print("\n=== Vorschau-Link (Bunny Token Authentication) ===\n")
|
||||
print(link)
|
||||
print(f"\nGueltig bis: {gueltig_bis}\n")
|
||||
Reference in New Issue
Block a user