Files
Deploymentcenter/scripts/deploy.py
T
Deploymentcenter BotandClaude Opus 5 9d0261306c chore(cleanup): Altlasten entfernt, Webroot-Sperre und Supervisor-Route ergaenzt
Fruehjahrsputz. Alles hier ist unabhaengig von der laufenden Supervisor-
Arbeit im Arbeitsverzeichnis - die bleibt unangetastet.

Entfernt:
- Die zwoelf Symbole unter public/assets/ waren bitgleiche Dubletten der
  Dateien in public/assets/icons/. Referenziert wurde ausschliesslich der
  Pfad mit icons/. getMonitorIconUrl() setzt flach gespeicherte Altwerte
  aus der Datenbank jetzt auf icons/ um, damit bestehende Monitore ihr
  Symbol behalten.
- System.CommandLine (2.0.0-beta4) aus dem Packager. Kein using, kein
  Symbol, die Argumente werden von Hand geparst. Build bleibt fehlerfrei.

Geschlossen:
- client-php/ und mcp/ wurden weder von der .htaccess gesperrt noch vom
  Deployment ausgeschlossen. Die .mjs-Datei haette der Webserver im
  Klartext ausgeliefert. Beide gehoeren nicht ins Webroot: die PHP-Datei
  wird ins Zielprojekt kopiert, der MCP-Server laeuft lokal ueber stdio.
  Jetzt in beiden Ausschlusslisten - wie client-dotnet.
- Fuer /api/supervisor/v1/ fehlte die Rewrite-Regel. Der Datei-Fallback
  haette /api/supervisor/v1/findings auf eine nicht vorhandene Datei
  geleitet, obwohl Code und OpenAPI diese Schreibweise vorsehen.

Ergaenzt:
- __pycache__/ und *.pyc in .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 12:20:33 +02:00

219 lines
6.6 KiB
Python

#!/usr/bin/env python3
import os
import json
import ftplib
import sys
import hashlib
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
PROJECT_DIR = SCRIPT_DIR.parent
CONFIG_FILE = SCRIPT_DIR / 'deploy_config.json'
CACHE_FILE = SCRIPT_DIR / '.deploy_cache.json'
IGNORE_PATTERNS = {
'.git',
'.gitignore',
'scripts',
'.deploy_cache.json',
'Serverdaten.txt',
'Serverdaten.txt.bak',
'bin',
'obj',
# Laufzeitdaten gehoeren dem Server, nicht dem Arbeitsplatz. var/.htaccess
# wird dennoch uebertragen, damit das Verzeichnis existiert und gesperrt ist.
'log',
'__pycache__',
'client-dotnet',
# Bibliothek zum Mitnehmen ins Zielprojekt bzw. lokal ueber stdio
# betriebener MCP-Server - beide gehoeren nicht ins Webroot.
'client-php',
'mcp',
# Gebaute Installer-Binaries. Sie gehoeren nach /installer/ und werden
# von upload_installer.py dorthin gebracht - ueber diesen Weg landeten
# sonst ~100 MB zusaetzlich unter /artifacts/ im Webroot.
'artifacts',
}
def load_config():
if not CONFIG_FILE.exists():
print(f"Error: Config file {CONFIG_FILE} does not exist.")
sys.exit(1)
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
def load_cache():
if CACHE_FILE.exists():
try:
with open(CACHE_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
return {}
return {}
def save_cache(cache):
with open(CACHE_FILE, 'w', encoding='utf-8') as f:
json.dump(cache, f, indent=2)
def compute_hash(filepath):
h = hashlib.md5()
with open(filepath, 'rb') as f:
while chunk := f.read(8192):
h.update(chunk)
return h.hexdigest()
def ensure_remote_dir(ftp, remote_path):
dirs = [d for d in remote_path.strip('/').split('/') if d]
current = ''
for d in dirs:
current += '/' + d
try:
ftp.cwd(current)
except ftplib.error_perm:
try:
ftp.mkd(current)
print(f"Created remote directory: {current}")
except Exception as e:
print(f"Warning: Could not create directory {current}: {e}")
def should_ignore(rel_path):
parts = Path(rel_path).parts
if not parts:
return False
for part in parts:
if part in IGNORE_PATTERNS:
return True
if part == '.htaccess':
continue
if part.startswith('.'):
return True
return False
def collect_changes(cache):
"""Ermittelt alle Dateien, die sich seit dem letzten Deployment geaendert haben."""
files_to_upload = []
for root, dirs, files in os.walk(PROJECT_DIR):
rel_dir = os.path.relpath(root, PROJECT_DIR)
if rel_dir == '.':
rel_dir = ''
dirs[:] = [d for d in dirs if not should_ignore(os.path.join(rel_dir, d))]
for f in files:
rel_file = os.path.normpath(os.path.join(rel_dir, f)).replace('\\', '/')
if should_ignore(rel_file):
continue
full_path = os.path.join(root, f)
file_hash = compute_hash(full_path)
if cache.get(rel_file) != file_hash:
files_to_upload.append((rel_file, full_path, file_hash))
files_to_upload.sort(key=upload_priority)
return files_to_upload
def upload_priority(entry):
"""
Reihenfolge des Uploads.
Abhaengigkeiten zuerst: waere public/index.php schon oben, waehrend
src/bootstrap.php noch fehlt, liefe die Seite in der Zwischenzeit in einen
Fehler. Die .htaccess im Wurzelverzeichnis kommt zuletzt, damit neue Routen
erst greifen, wenn ihre Zielskripte vorhanden sind.
"""
rel_file = entry[0]
if rel_file == '.htaccess':
rank = 4
elif rel_file.startswith('public/'):
rank = 3
elif rel_file.startswith(('src/', 'config/', 'sql/', 'var/')):
rank = 1
else:
rank = 2
return (rank, rel_file)
def dry_run():
"""Zeigt an, was uebertragen wuerde, ohne eine Verbindung aufzubauen."""
cache = load_cache()
changes = collect_changes(cache)
if not changes:
print("Keine Aenderungen - der Server ist auf dem aktuellen Stand.")
return
total = sum(os.path.getsize(p) for _, p, _ in changes)
print(f"{len(changes)} Datei(en) wuerden uebertragen ({total / 1024:.1f} KB):\n")
for rel_file, full_path, _ in changes:
marker = 'NEU' if rel_file not in cache else ' '
print(f" {marker} {rel_file} ({os.path.getsize(full_path) / 1024:.1f} KB)")
def deploy():
config = load_config()
cache = load_cache()
new_cache = dict(cache)
print(f"Connecting to FTP {config['host']}...")
try:
if config.get('secure', False):
ftp = ftplib.FTP_TLS()
ftp.connect(config['host'], config.get('port', 21))
ftp.login(config['user'], config['pass'])
ftp.prot_p()
else:
ftp = ftplib.FTP()
ftp.connect(config['host'], config.get('port', 21))
ftp.login(config['user'], config['pass'])
print("Logged in successfully.")
except Exception as e:
print(f"FTP Connection failed: {e}")
sys.exit(1)
files_to_upload = collect_changes(cache)
if not files_to_upload:
print("No changed files to upload. Remote is up to date!")
ftp.quit()
return
print(f"Found {len(files_to_upload)} file(s) to upload:")
for rel_file, full_path, file_hash in files_to_upload:
print(f" -> {rel_file}")
uploaded = 0
try:
for rel_file, full_path, file_hash in files_to_upload:
remote_file_path = f"/{rel_file}"
remote_dir = os.path.dirname(remote_file_path).replace('\\', '/')
if remote_dir and remote_dir != '/':
ensure_remote_dir(ftp, remote_dir)
ftp.cwd('/')
print(f"Uploading {rel_file} ...")
with open(full_path, 'rb') as f:
ftp.storbinary(f"STOR {remote_file_path}", f)
new_cache[rel_file] = file_hash
uploaded += 1
finally:
# Der Cache wird auch bei einem Abbruch geschrieben. Sonst beginnt der
# naechste Lauf wieder bei null und ueberträgt alles erneut.
save_cache(new_cache)
try:
ftp.quit()
except Exception:
pass
print(f"\nDeployment abgeschlossen: {uploaded} von {len(files_to_upload)} Datei(en) uebertragen.")
if __name__ == '__main__':
if '--dry-run' in sys.argv or '-n' in sys.argv:
dry_run()
else:
deploy()