Files
app-installer/app.py
SimolZimol 1eac702d7d modified: app.py
new file:   file_manager.py
	modified:   requirements.txt
	new file:   sessions_db.json
	modified:   templates/base.html
	new file:   templates/file_manager.html
	new file:   templates/file_manager_new.html
	new file:   templates/login.html
	new file:   templates/profile.html
	new file:   templates/users_management.html
	new file:   user_management.py
	new file:   users_db.json
2025-07-10 00:00:59 +02:00

2964 lines
120 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from flask import Flask, render_template, request, jsonify, flash, redirect, url_for, session
import requests
import os
import subprocess
import json
import re
import time
import shutil
import urllib.request
import urllib.error
import tempfile
import socket
import stat
from urllib.parse import urlparse
import docker
import yaml
from datetime import datetime
# User Management System importieren
from user_management import (
user_manager, UserRole, Permission,
login_required, role_required, permission_required,
api_login_required, api_permission_required
)
app = Flask(__name__)
app.secret_key = 'your-secret-key-change-this'
from flask import send_from_directory
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
# Configuration
CONFIG_FILE = 'config.json'
PROJECTS_DIR = 'projects'
APPS_DIR = 'apps'
class FileManager:
"""File Manager für Dateioperationen im Web-Interface"""
def __init__(self, base_path=None):
self.base_path = base_path or os.path.abspath('.')
self.allowed_extensions = {
'text': {'.txt', '.md', '.py', '.js', '.html', '.css', '.json', '.xml', '.yml', '.yaml', '.sh', '.bat', '.cfg', '.ini', '.conf'},
'image': {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.ico'},
'archive': {'.zip', '.tar', '.gz', '.rar', '.7z'},
'document': {'.pdf', '.doc', '.docx', '.xls', '.xlsx'}
}
def get_safe_path(self, path):
"""Normalisiere und validiere Pfad"""
if not path:
return self.base_path
# Relative Pfade relativ zur base_path auflösen
if not os.path.isabs(path):
full_path = os.path.join(self.base_path, path)
else:
full_path = path
# Normalisiere den Pfad
full_path = os.path.normpath(full_path)
# Sicherheitscheck: Pfad muss innerhalb der base_path liegen
if not full_path.startswith(self.base_path):
raise ValueError("Zugriff außerhalb des erlaubten Bereichs")
return full_path
def list_directory(self, path=""):
"""Liste Dateien und Ordner in einem Verzeichnis"""
try:
safe_path = self.get_safe_path(path)
if not os.path.exists(safe_path):
return {'error': 'Verzeichnis existiert nicht'}
if not os.path.isdir(safe_path):
return {'error': 'Pfad ist kein Verzeichnis'}
items = []
for item in os.listdir(safe_path):
item_path = os.path.join(safe_path, item)
rel_path = os.path.relpath(item_path, self.base_path).replace('\\', '/')
item_info = {
'name': item,
'path': rel_path,
'is_directory': os.path.isdir(item_path),
'size': 0,
'modified': 0,
'type': 'folder' if os.path.isdir(item_path) else self.get_file_type(item)
}
if not os.path.isdir(item_path):
try:
stat = os.stat(item_path)
item_info['size'] = stat.st_size
item_info['modified'] = stat.st_mtime
except:
pass
items.append(item_info)
# Sortiere: Ordner zuerst, dann Dateien
items.sort(key=lambda x: (not x['is_directory'], x['name'].lower()))
# Aktueller Pfad relativ zur base_path
current_path = os.path.relpath(safe_path, self.base_path).replace('\\', '/')
if current_path == '.':
current_path = ''
# Parent-Pfad für Navigation
parent_path = ''
if current_path:
parent_path = os.path.dirname(current_path).replace('\\', '/')
if parent_path == '.':
parent_path = ''
return {
'success': True,
'current_path': current_path,
'parent_path': parent_path,
'items': items
}
except Exception as e:
return {'error': str(e)}
def get_file_type(self, filename):
"""Bestimme Dateityp anhand der Dateiendung"""
ext = os.path.splitext(filename)[1].lower()
for file_type, extensions in self.allowed_extensions.items():
if ext in extensions:
return file_type
return 'unknown'
def read_file(self, path):
"""Lese Dateiinhalt"""
try:
safe_path = self.get_safe_path(path)
if not os.path.exists(safe_path):
return {'error': 'Datei existiert nicht'}
if os.path.isdir(safe_path):
return {'error': 'Pfad ist ein Verzeichnis'}
# Prüfe Dateigröße (max 10MB für Text-Dateien)
file_size = os.path.getsize(safe_path)
if file_size > 10 * 1024 * 1024:
return {'error': 'Datei zu groß (max 10MB)'
}
# Versuche als Text zu lesen
try:
with open(safe_path, 'r', encoding='utf-8') as f:
content = f.read()
return {
'success': True,
'content': content,
'size': file_size,
'type': 'text'
}
except UnicodeDecodeError:
return {'error': 'Datei ist nicht als Text lesbar'}
except Exception as e:
return {'error': str(e)}
def write_file(self, path, content):
"""Schreibe Dateiinhalt"""
try:
safe_path = self.get_safe_path(path)
# Erstelle Verzeichnis falls nicht vorhanden
os.makedirs(os.path.dirname(safe_path), exist_ok=True)
with open(safe_path, 'w', encoding='utf-8') as f:
f.write(content)
return {'success': True, 'message': 'Datei erfolgreich gespeichert'}
except Exception as e:
return {'error': str(e)}
def delete_item(self, path):
"""Lösche Datei oder Verzeichnis"""
try:
safe_path = self.get_safe_path(path)
if not os.path.exists(safe_path):
return {'error': 'Datei/Verzeichnis existiert nicht'}
if os.path.isdir(safe_path):
shutil.rmtree(safe_path)
return {'success': True, 'message': 'Verzeichnis erfolgreich gelöscht'}
else:
os.remove(safe_path)
return {'success': True, 'message': 'Datei erfolgreich gelöscht'}
except Exception as e:
return {'error': str(e)}
def create_directory(self, path):
"""Erstelle neues Verzeichnis"""
try:
safe_path = self.get_safe_path(path)
if os.path.exists(safe_path):
return {'error': 'Verzeichnis existiert bereits'}
os.makedirs(safe_path, exist_ok=True)
return {'success': True, 'message': 'Verzeichnis erfolgreich erstellt'}
except Exception as e:
return {'error': str(e)}
def rename_item(self, old_path, new_name):
"""Benenne Datei oder Verzeichnis um"""
try:
safe_old_path = self.get_safe_path(old_path)
if not os.path.exists(safe_old_path):
return {'error': 'Datei/Verzeichnis existiert nicht'}
new_path = os.path.join(os.path.dirname(safe_old_path), new_name)
safe_new_path = self.get_safe_path(os.path.relpath(new_path, self.base_path))
if os.path.exists(safe_new_path):
return {'error': 'Ziel existiert bereits'}
os.rename(safe_old_path, safe_new_path)
return {'success': True, 'message': 'Erfolgreich umbenannt'}
except Exception as e:
return {'error': str(e)}
# File Manager Instanz
file_manager = FileManager()
class ProjectManager:
def __init__(self):
self.docker_client = None
self.docker_available = False
self._init_docker()
# Erstelle notwendige Verzeichnisse
os.makedirs(PROJECTS_DIR, exist_ok=True)
os.makedirs(APPS_DIR, exist_ok=True)
def _init_docker(self):
"""Initialisiere Docker-Verbindung mit Retry-Logik"""
try:
self.docker_client = docker.from_env()
# Teste Docker-Verbindung
self.docker_client.ping()
self.docker_available = True
print("✓ Docker erfolgreich verbunden")
except docker.errors.DockerException as e:
print(f"⚠ Docker-Verbindungsfehler: {e}")
print(" Hinweis: Docker Desktop starten und /api/docker_reconnect aufrufen")
self.docker_client = None
self.docker_available = False
except Exception as e:
print(f"⚠ Docker nicht verfügbar: {e}")
print(" Hinweis: Docker Desktop installieren oder starten")
self.docker_client = None
self.docker_available = False
def reconnect_docker(self):
"""Versuche Docker-Verbindung wiederherzustellen"""
print("🔄 Versuche Docker-Reconnect...")
# Alte Verbindung schließen
if self.docker_client:
try:
self.docker_client.close()
except:
pass
# Neu initialisieren
self._init_docker()
return self.docker_available
def load_config(self):
"""Lade Konfiguration aus config.json"""
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return {
'project_list_url': '',
'auto_refresh_minutes': 30,
'docker_registry': '',
'projects': []
}
def save_config(self, config):
"""Speichere Konfiguration in config.json"""
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
def fetch_project_list(self, url):
"""Hole Projektliste von einer URL"""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
# Versuche JSON zu parsen
try:
return response.json()
except:
# Falls kein JSON, extrahiere URLs aus HTML/Text
urls = re.findall(r'https?://[^\s<>"]+', response.text)
git_urls = [url for url in urls if any(domain in url for domain in ['github.com', 'gitlab.com', 'gitea'])]
return [{'url': url, 'name': self.extract_project_name(url)} for url in git_urls]
except Exception as e:
print(f"Fehler beim Abrufen der Projektliste: {e}")
return []
def extract_project_name(self, url):
"""Extrahiere Projektname aus URL"""
path = urlparse(url).path
parts = path.strip('/').split('/')
return parts[-1] if parts else 'unknown'
def clone_project(self, project_url, project_name, save_version=True, installation_method='clone'):
"""Klone Projekt aus Git-Repository"""
project_path = os.path.join(PROJECTS_DIR, project_name)
# Prüfe ob Projekt bereits existiert
if os.path.exists(project_path):
return False, f"Projekt {project_name} existiert bereits"
try:
# Git Clone ausführen
result = subprocess.run([
'git', 'clone', project_url, project_path
], capture_output=True, text=True, timeout=300)
if result.returncode == 0:
print(f"✓ Git Clone erfolgreich für {project_name}")
# Nach erfolgreichem Klonen die Version speichern (nur wenn gewünscht)
if save_version:
try:
self.save_project_version(project_name, project_url, installation_method=installation_method)
print(f"✓ Version für {project_name} mit Methode '{installation_method}' gespeichert")
except Exception as e:
print(f"⚠ Warnung: Konnte Version für {project_name} nicht speichern: {e}")
return True, f"Projekt {project_name} erfolgreich geklont und konfiguriert"
else:
# Bereinige bei Fehler
if os.path.exists(project_path):
try:
import shutil
shutil.rmtree(project_path)
except:
pass
return False, f"Fehler beim Klonen: {result.stderr}"
except subprocess.TimeoutExpired:
# Bereinige bei Timeout
if os.path.exists(project_path):
try:
import shutil
shutil.rmtree(project_path)
except:
pass
return False, "Timeout beim Klonen des Projekts"
except Exception as e:
# Bereinige bei Fehler
if os.path.exists(project_path):
try:
import shutil
shutil.rmtree(project_path)
except:
pass
return False, f"Fehler beim Klonen: {str(e)}"
def save_project_version(self, project_name, project_url=None, installation_method='clone'):
"""Speichere die aktuelle Version eines Projekts"""
project_path = os.path.join(PROJECTS_DIR, project_name)
# Versuche Version aus verschiedenen Quellen zu extrahieren
version = self.extract_project_version(project_path, project_name, project_url)
# Bestimme Standard-Start-Modus basierend auf installation_method
def get_default_start_mode(installation_method):
if installation_method in ['image', 'docker_registry', 'docker_url', 'docker_file', 'dockerfile', 'docker_build']:
return 'docker'
elif installation_method == 'native_python':
return 'python'
elif installation_method == 'native_nodejs':
return 'nodejs'
elif installation_method == 'native_batch':
return 'batch'
elif installation_method == 'native_shell':
return 'shell'
elif installation_method == 'clone':
# Bei Clone schauen, welche Dateien vorhanden sind
if os.path.exists(os.path.join(project_path, 'Dockerfile')):
return 'docker'
elif os.path.exists(os.path.join(project_path, 'start.bat')):
return 'batch'
elif os.path.exists(os.path.join(project_path, 'start.sh')):
return 'shell'
elif os.path.exists(os.path.join(project_path, 'package.json')):
return 'nodejs'
elif any(f.endswith('.py') for f in os.listdir(project_path) if os.path.isfile(os.path.join(project_path, f))):
return 'python'
else:
return 'docker' # Fallback
else:
return 'docker' # Standard-Fallback
if version:
# Speichere Version in der NEUEN version_app_in Datei (Hauptdatei)
version_file = os.path.join(project_path, 'version_app_in')
try:
with open(version_file, 'w', encoding='utf-8') as f:
import json
default_start_mode = get_default_start_mode(installation_method)
version_data = {
'version': version,
'installed_at': datetime.now().isoformat(),
'project_url': project_url,
'installation_method': installation_method,
'preferred_start_mode': default_start_mode,
'last_updated': datetime.now().isoformat()
}
json.dump(version_data, f, indent=2)
print(f"✅ Version {version} für Projekt {project_name} mit Methode '{installation_method}' und Start-Modus '{default_start_mode}' gespeichert")
except Exception as e:
print(f"❌ Fehler beim Speichern der version_app_in Datei: {e}")
# Erstelle auch eine Backup-Datei (.app_installer_version) für Rückwärtskompatibilität
backup_version_file = os.path.join(project_path, '.app_installer_version')
try:
with open(backup_version_file, 'w', encoding='utf-8') as f:
version_data = {
'version': version,
'installed_at': datetime.now().isoformat(),
'project_url': project_url,
'installation_method': installation_method,
'last_updated': datetime.now().isoformat()
}
json.dump(version_data, f, indent=2)
print(f"📋 Backup-Versionsdatei erstellt: .app_installer_version")
except Exception as e:
print(f"⚠️ Warnung: Backup-Versionsdatei konnte nicht erstellt werden: {e}")
def extract_project_version(self, project_path, project_name, project_url=None):
"""Extrahiere Version aus verschiedenen Quellen"""
version = None
# 1. HÖCHSTE PRIORITÄT: Versuche aus Projektliste (falls URL bekannt)
if project_url:
version = self.get_version_from_project_list(project_name, project_url)
if version:
print(f"✓ Version aus Projektliste: {version}")
return version
# 2. Versuche package.json (Node.js Projekte)
package_json_path = os.path.join(project_path, 'package.json')
if os.path.exists(package_json_path):
try:
with open(package_json_path, 'r', encoding='utf-8') as f:
import json
package_data = json.load(f)
version = package_data.get('version')
if version:
print(f"✓ Version aus package.json: {version}")
return version
except Exception as e:
print(f"⚠ Fehler beim Lesen von package.json: {e}")
# 3. Versuche pyproject.toml oder setup.py (Python Projekte)
pyproject_path = os.path.join(project_path, 'pyproject.toml')
if os.path.exists(pyproject_path):
try:
with open(pyproject_path, 'r', encoding='utf-8') as f:
content = f.read()
import re
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
if match:
version = match.group(1)
print(f"✓ Version aus pyproject.toml: {version}")
return version
except Exception as e:
print(f"⚠ Fehler beim Lesen von pyproject.toml: {e}")
# 4. Versuche aus Git Tags
try:
result = subprocess.run(['git', 'describe', '--tags', '--abbrev=0'],
cwd=project_path, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
version = result.stdout.strip()
# Bereinige Tag (entferne v prefix falls vorhanden)
if version.startswith('v'):
version = version[1:]
print(f"✓ Version aus Git Tag: {version}")
return version
except Exception as e:
print(f"⚠ Fehler beim Lesen von Git Tags: {e}")
# 5. Versuche aus Git Commit Hash (als Fallback)
try:
result = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'],
cwd=project_path, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
commit_hash = result.stdout.strip()
version = f"git-{commit_hash}"
print(f"✓ Version aus Git Commit: {version}")
return version
except Exception as e:
print(f"⚠ Fehler beim Lesen von Git Commit: {e}")
# 6. Fallback: Default Version
print(f"⚠ Keine Version gefunden für {project_name}, verwende 1.0.0")
return "1.0.0"
def get_version_from_project_list(self, project_name, project_url):
"""Hole Version aus der Projektliste (priorisiert Remote-URLs)"""
try:
# 1. ERSTE PRIORITÄT: Versuche die konfigurierte Remote-URL
config = self.load_config()
remote_url = config.get('project_list_url')
if remote_url:
print(f"🌐 Hole Version von Remote-URL: {remote_url}")
try:
remote_projects = self.fetch_project_list(remote_url)
for project in remote_projects:
if (project.get('name') == project_name or
project.get('url') == project_url):
metadata = project.get('metadata', {})
version = metadata.get('version')
if version:
print(f"✓ Remote-Version gefunden: {version}")
return version
except Exception as e:
print(f"⚠ Fehler beim Abrufen der Remote-Projektliste: {e}")
# 2. FALLBACK: Lokale Projektlisten
print(f"📁 Suche in lokalen Projektlisten...")
for project_list_file in ['projects_list.json', 'public_projects_list.json', 'projects_webserver.json']:
if os.path.exists(project_list_file):
with open(project_list_file, 'r', encoding='utf-8') as f:
projects = json.load(f)
for project in projects:
if (project.get('name') == project_name or
project.get('url') == project_url):
metadata = project.get('metadata', {})
version = metadata.get('version')
if version:
print(f"✓ Lokale Version gefunden: {version}")
return version
except Exception as e:
print(f"⚠ Fehler beim Lesen der Projektliste: {e}")
print(f"⚠ Keine Version gefunden für Projekt {project_name}")
return None
def get_installed_version(self, project_name):
"""Hole die installierte Version eines Projekts aus der version_app_in Datei"""
project_path = os.path.join(PROJECTS_DIR, project_name)
# ERSTE PRIORITÄT: Prüfe die neue version_app_in Datei
version_file = os.path.join(project_path, 'version_app_in')
if os.path.exists(version_file):
try:
with open(version_file, 'r', encoding='utf-8') as f:
import json
version_data = json.load(f)
version = version_data.get('version')
if version:
print(f"📋 Version aus version_app_in: {version}")
return version
except Exception as e:
print(f"❌ Fehler beim Lesen der version_app_in Datei: {e}")
# ZWEITE PRIORITÄT: Fallback auf alte .app_installer_version Datei
old_version_file = os.path.join(project_path, '.app_installer_version')
if os.path.exists(old_version_file):
try:
with open(old_version_file, 'r', encoding='utf-8') as f:
import json
version_data = json.load(f)
version = version_data.get('version')
# Migration: Verschiebe zu neuer Datei
if version:
print(f"🔄 Migriere Version für {project_name} von .app_installer_version zu version_app_in")
self.save_project_version(project_name, version_data.get('project_url'), version_data.get('installation_method', 'clone'))
return version
except Exception as e:
print(f"❌ Fehler beim Lesen der alten Versionsdatei: {e}")
# DRITTE PRIORITÄT: Versuche direkt aus Projekt zu extrahieren
print(f"⚠️ Keine Versionsdatei gefunden für {project_name}, extrahiere aus Projekt...")
return self.extract_project_version(project_path, project_name)
def get_project_info(self, project_name):
"""Hole Projektinformationen"""
project_path = os.path.join(PROJECTS_DIR, project_name)
if not os.path.exists(project_path):
return None
info = {
'name': project_name,
'path': project_path,
'has_dockerfile': os.path.exists(os.path.join(project_path, 'Dockerfile')),
'has_env_example': os.path.exists(os.path.join(project_path, '.env.example')),
'has_docker_compose': os.path.exists(os.path.join(project_path, 'docker-compose.yml')),
'has_start_bat': os.path.exists(os.path.join(project_path, 'start.bat')),
'has_start_sh': os.path.exists(os.path.join(project_path, 'start.sh')),
'has_package_json': os.path.exists(os.path.join(project_path, 'package.json')),
'has_python_files': any(os.path.exists(os.path.join(project_path, f)) for f in ['app.py', 'main.py', 'server.py']),
'status': self.get_container_status(project_name),
'created': datetime.fromtimestamp(os.path.getctime(project_path)).strftime('%Y-%m-%d %H:%M'),
'version': self.get_installed_version(project_name)
}
# Lese erweiterte Informationen aus version_app_in
version_file = os.path.join(project_path, 'version_app_in')
if os.path.exists(version_file):
try:
with open(version_file, 'r', encoding='utf-8') as f:
import json
version_data = json.load(f)
info['installation_method'] = version_data.get('installation_method', 'unknown')
info['preferred_start_mode'] = version_data.get('preferred_start_mode', 'docker')
info['project_url'] = version_data.get('project_url', '')
info['last_updated'] = version_data.get('last_updated', '')
except Exception as e:
print(f"⚠ Warnung: Konnte version_app_in für {project_name} nicht lesen: {e}")
info['installation_method'] = 'unknown'
info['preferred_start_mode'] = 'docker'
else:
# Fallback: Bestimme die Installationsmethode aus der Konfiguration
config = self.load_config()
for project in config.get('projects', []):
if project.get('name') == project_name:
info['installation_method'] = project.get('install_method', 'unknown')
break
else:
info['installation_method'] = 'unknown'
# Standard Start-Modus falls keine version_app_in vorhanden
if info['has_dockerfile']:
info['preferred_start_mode'] = 'docker'
elif info['has_start_bat']:
info['preferred_start_mode'] = 'batch'
elif info['has_start_sh']:
info['preferred_start_mode'] = 'shell'
elif info['has_package_json']:
info['preferred_start_mode'] = 'nodejs'
elif info['has_python_files']:
info['preferred_start_mode'] = 'python'
else:
info['preferred_start_mode'] = 'docker'
# Lese README falls vorhanden
readme_files = ['README.md', 'readme.md', 'README.txt', 'readme.txt']
for readme in readme_files:
readme_path = os.path.join(project_path, readme)
if os.path.exists(readme_path):
with open(readme_path, 'r', encoding='utf-8', errors='ignore') as f:
info['readme'] = f.read()[:1000] # Ersten 1000 Zeichen
break
return info
def get_container_status(self, project_name):
"""Prüfe Container-Status"""
if not self.docker_available or not self.docker_client:
return 'docker_unavailable'
try:
containers = self.docker_client.containers.list(all=True, filters={'name': project_name})
if containers:
return containers[0].status
return 'not_created'
except docker.errors.DockerException as e:
print(f"Docker-Fehler beim Status-Check: {e}")
return 'docker_error'
except Exception as e:
print(f"Unerwarteter Fehler beim Status-Check: {e}")
return 'unknown'
def build_project(self, project_name):
"""Baue Docker-Image für Projekt"""
project_path = os.path.join(PROJECTS_DIR, project_name)
if not os.path.exists(os.path.join(project_path, 'Dockerfile')):
return False, "Kein Dockerfile gefunden"
if not self.docker_available:
return False, "Docker ist nicht verfügbar. Bitte starten Sie Docker Desktop."
try:
# Kopiere .env.example zu .env falls nicht vorhanden
env_example = os.path.join(project_path, '.env.example')
env_file = os.path.join(project_path, '.env')
if os.path.exists(env_example) and not os.path.exists(env_file):
with open(env_example, 'r', encoding='utf-8') as src, open(env_file, 'w', encoding='utf-8') as dst:
dst.write(src.read())
# Docker Build
print(f"Building Docker image for {project_name}...")
# Analysiere Dockerfile vor dem Build
dockerfile_ports = self.analyze_dockerfile_ports(project_path)
if dockerfile_ports:
print(f"Dockerfile exponiert Ports: {dockerfile_ports}")
image, logs = self.docker_client.images.build(
path=project_path,
tag=f"{project_name}:latest",
rm=True
)
# Log build output for debugging
for log in logs:
if 'stream' in log:
print(log['stream'].strip())
# Analysiere Image nach dem Build
built_ports = self.detect_container_exposed_ports(project_name)
port_info = ""
if built_ports:
port_info = f" (Exponierte Ports: {', '.join(built_ports)})"
return True, f"Image {project_name}:latest erfolgreich gebaut{port_info}"
except docker.errors.BuildError as e:
error_msg = "Build-Fehler:\n"
for log in e.build_log:
if 'stream' in log:
error_msg += log['stream']
return False, error_msg
except docker.errors.DockerException as e:
return False, f"Docker-Fehler beim Build: {str(e)}"
except Exception as e:
return False, f"Unerwarteter Fehler beim Build: {str(e)}"
def pull_docker_image(self, image_url, project_name):
"""Lade Docker-Image herunter"""
if not self.docker_available:
return False, "Docker ist nicht verfügbar"
try:
print(f"Lade Docker-Image herunter: {image_url}")
# Docker pull ausführen
image = self.docker_client.images.pull(image_url)
# Tag Image mit Projektname für lokale Verwendung
image.tag(f"{project_name}:latest")
print(f"✓ Docker-Image {image_url} erfolgreich heruntergeladen")
# Erstelle minimales Projektverzeichnis für Konfiguration
project_path = os.path.join(PROJECTS_DIR, project_name)
os.makedirs(project_path, exist_ok=True)
# Erstelle .dockerimage Datei um zu markieren, dass es ein Image-Download ist
with open(os.path.join(project_path, '.dockerimage'), 'w', encoding='utf-8') as f:
json.dump({
'image_url': image_url,
'pulled_at': datetime.now().isoformat(),
'installation_method': 'image'
}, f, indent=2)
return True, f"Docker-Image {project_name} erfolgreich heruntergeladen"
except docker.errors.APIError as e:
return False, f"Docker-API-Fehler: {str(e)}"
except Exception as e:
return False, f"Fehler beim Herunterladen des Images: {str(e)}"
def load_docker_image_from_url(self, image_url, project_name):
"""Lade Docker-Image von einer URL herunter (z.B. .tar Datei)"""
if not self.docker_available:
return False, "Docker ist nicht verfügbar"
try:
print(f"Lade Docker-Image von URL herunter: {image_url}")
# Temporärer Dateiname für Download
with tempfile.NamedTemporaryFile(suffix='.tar', delete=False) as temp_file:
temp_path = temp_file.name
# Image von URL herunterladen
print(f"Lade Datei herunter: {image_url}")
urllib.request.urlretrieve(image_url, temp_path)
# Docker-Image aus Datei laden
print(f"Importiere Docker-Image aus Datei...")
with open(temp_path, 'rb') as f:
images = self.docker_client.images.load(f)
# Versuche das Image mit Projektname zu taggen
if images:
image = images[0] if isinstance(images, list) else images
image.tag(f"{project_name}:latest")
print(f"✓ Docker-Image von URL erfolgreich geladen und getaggt als {project_name}:latest")
else:
return False, "Kein Image aus der geladenen Datei gefunden"
# Temporäre Datei löschen
os.unlink(temp_path)
# Erstelle minimales Projektverzeichnis für Konfiguration
project_path = os.path.join(PROJECTS_DIR, project_name)
os.makedirs(project_path, exist_ok=True)
# Erstelle .dockerurl Datei um zu markieren, dass es ein URL-Download ist
with open(os.path.join(project_path, '.dockerurl'), 'w', encoding='utf-8') as f:
json.dump({
'image_url': image_url,
'loaded_at': datetime.now().isoformat(),
'installation_method': 'docker_url'
}, f, indent=2)
return True, f"Docker-Image {project_name} erfolgreich von URL geladen"
except urllib.error.URLError as e:
return False, f"URL-Fehler beim Herunterladen: {str(e)}"
except docker.errors.APIError as e:
return False, f"Docker-API-Fehler beim Laden: {str(e)}"
except Exception as e:
return False, f"Fehler beim Laden des Images von URL: {str(e)}"
finally:
# Stelle sicher, dass temporäre Datei gelöscht wird
try:
if 'temp_path' in locals() and os.path.exists(temp_path):
os.unlink(temp_path)
except:
pass
def get_installation_methods(self, project):
"""Ermittle verfügbare Installationsmethoden für ein Projekt"""
methods = []
# Prüfe neues Schema mit installation.methods
if 'installation' in project and 'methods' in project['installation']:
for method_type, method_info in project['installation']['methods'].items():
if method_info.get('available', True):
methods.append({
'type': method_type,
'url': method_info['url'],
'description': method_info.get('description', ''),
'preferred': project['installation'].get('preferred') == method_type
})
else:
# Fallback für alte Projekte ohne neues Schema
if 'url' in project:
methods.append({
'type': 'clone',
'url': project['url'],
'description': 'Quellcode klonen und bauen',
'preferred': True
})
return methods
def check_port_availability(self, port):
"""Prüfe ob ein Port verfügbar ist"""
import socket
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(1)
result = sock.connect_ex(('localhost', int(port)))
return result != 0 # Port ist frei wenn Verbindung fehlschlägt
except:
return False
def find_available_port(self, start_port=8080, max_attempts=50):
"""Finde einen verfügbaren Port beginnend ab start_port"""
for port in range(int(start_port), int(start_port) + max_attempts):
if self.check_port_availability(port):
return port
return None
def check_container_health(self, container, port):
"""Prüfe ob Container tatsächlich erreichbar ist"""
import requests
import time
max_attempts = 10
for attempt in range(max_attempts):
try:
# Prüfe Container-Status
container.reload()
if container.status != 'running':
return False, f"Container nicht am Laufen (Status: {container.status})"
# Prüfe HTTP-Erreichbarkeit wenn Port definiert
if port:
try:
response = requests.get(f"http://localhost:{port}", timeout=2)
if response.status_code < 500: # Akzeptiere alle Antworten außer Server-Errors
return True, "Container erreichbar"
except requests.exceptions.RequestException:
pass # HTTP noch nicht bereit, aber das ist OK in den ersten Sekunden
time.sleep(1)
except Exception as e:
return False, f"Health Check Fehler: {str(e)}"
return True, "Container läuft (HTTP-Check übersprungen)"
def analyze_app_files_for_ports(self, project_path):
"""Analysiere App-Dateien um verwendete Ports zu finden"""
ports = []
# Typische Dateien durchsuchen
files_to_check = ['app.py', 'main.py', 'server.py', 'index.js', 'package.json']
for filename in files_to_check:
filepath = os.path.join(project_path, filename)
if os.path.exists(filepath):
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Suche nach Port-Definitionen
import re
# Flask/Python Patterns
flask_patterns = [
r'app\.run\([^)]*port\s*=\s*(\d+)',
r'host\s*=\s*["\'][^"\']*["\'],?\s*port\s*=\s*(\d+)',
r'port\s*=\s*(\d+)',
r'PORT\s*=\s*(\d+)',
]
# Node.js Patterns
node_patterns = [
r'listen\s*\(\s*(\d+)',
r'port\s*[:=]\s*(\d+)',
r'process\.env\.PORT\s*\|\|\s*(\d+)',
]
all_patterns = flask_patterns + node_patterns
for pattern in all_patterns:
matches = re.findall(pattern, content, re.IGNORECASE)
for match in matches:
port_num = int(match)
if 1 <= port_num <= 65535: # Gültiger Port-Bereich
port_spec = f"{port_num}/tcp"
if port_spec not in ports:
ports.append(port_spec)
print(f"Found port {port_num} in {filename}")
except Exception as e:
print(f"Warning: Could not analyze {filename}: {e}")
return ports
def analyze_dockerfile_ports(self, project_path):
"""Analysiere Dockerfile um exponierte Ports zu finden"""
dockerfile_path = os.path.join(project_path, 'Dockerfile')
ports = []
if os.path.exists(dockerfile_path):
try:
with open(dockerfile_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip().upper()
if line.startswith('EXPOSE'):
# EXPOSE 8080 oder EXPOSE 8080/tcp
parts = line.split()
for part in parts[1:]: # Skip "EXPOSE"
if part.isdigit():
ports.append(f"{part}/tcp")
elif '/' in part:
ports.append(part)
except Exception as e:
print(f"Warning: Could not analyze Dockerfile: {e}")
return ports
def detect_container_exposed_ports(self, image_name):
"""Erkenne welche Ports das Container-Image exponiert"""
try:
if not self.docker_available:
return []
image = self.docker_client.images.get(f"{image_name}:latest")
config = image.attrs.get('Config', {})
exposed_ports = config.get('ExposedPorts', {})
ports = []
for port_spec in exposed_ports.keys():
ports.append(port_spec)
# Falls keine Ports im Image exponiert sind, analysiere Projekt-Dateien
if not ports:
print(f"Keine Ports im Image {image_name} exponiert, analysiere Projekt-Dateien...")
project_path = os.path.join(PROJECTS_DIR, image_name)
# Analysiere Dockerfile und App-Dateien
dockerfile_ports = self.analyze_dockerfile_ports(project_path)
app_ports = self.analyze_app_files_for_ports(project_path)
# Kombiniere gefundene Ports
all_found_ports = dockerfile_ports + app_ports
if all_found_ports:
ports = all_found_ports
print(f"Ports aus Projekt-Analyse gefunden: {ports}")
else:
# Fallback zu Standard-Ports
ports = ['5000/tcp', '8080/tcp', '80/tcp', '3000/tcp']
print(f"Keine Ports gefunden, verwende Fallback: {ports}")
else:
print(f"Image {image_name} exponiert Ports: {ports}")
return ports
except Exception as e:
print(f"Warning: Could not detect exposed ports for {image_name}: {e}")
# Als letzter Fallback, analysiere Projekt-Dateien
try:
project_path = os.path.join(PROJECTS_DIR, image_name)
app_ports = self.analyze_app_files_for_ports(project_path)
if app_ports:
print(f"Fallback: Ports aus App-Dateien: {app_ports}")
return app_ports
except:
pass
# Ultimativer Fallback
return ['5000/tcp', '8080/tcp', '80/tcp', '3000/tcp']
def create_smart_port_mapping(self, project_name, host_port):
"""Erstelle intelligentes Port-Mapping basierend auf Image-Konfiguration"""
try:
exposed_ports = self.detect_container_exposed_ports(project_name)
if not exposed_ports:
print(f"Keine Ports im Image {project_name} gefunden, verwende Fallback")
# Verwende nur einen Standard-Port
return {'8080/tcp': host_port}
# Wenn nur ein Port exponiert ist, verwende diesen
if len(exposed_ports) == 1:
return {exposed_ports[0]: host_port}
# Wenn mehrere Ports exponiert sind, priorisiere Standard-Web-Ports
web_ports = ['80/tcp', '8080/tcp', '3000/tcp', '5000/tcp']
for web_port in web_ports:
if web_port in exposed_ports:
print(f"Verwende priorisierten Web-Port: {web_port} -> {host_port}")
return {web_port: host_port}
# Falls kein Standard-Web-Port, verwende den ersten
first_port = exposed_ports[0]
print(f"Verwende ersten exponierten Port: {first_port} -> {host_port}")
return {first_port: host_port}
except Exception as e:
print(f"Error creating port mapping: {e}")
# Fallback
return {'8080/tcp': host_port}
def wait_for_container_ready(self, container, port=None, timeout=30):
"""Warte bis Container bereit ist oder Timeout erreicht"""
import time
start_time = time.time()
while time.time() - start_time < timeout:
try:
container.reload()
if container.status == 'running':
# Container läuft - mache zusätzlichen Health Check
if port:
health_ok, health_msg = self.check_container_health(container, port)
if health_ok:
return True, "Container läuft und ist erreichbar"
# Falls Health Check fehlschlägt, warte noch etwas
else:
return True, "Container läuft"
elif container.status in ['exited', 'dead']:
logs = container.logs(tail=20).decode('utf-8', errors='ignore')
return False, f"Container beendet mit Status '{container.status}'. Logs: {logs[:300]}"
time.sleep(1)
except Exception as e:
return False, f"Fehler beim Prüfen des Container-Status: {str(e)}"
# Timeout erreicht - prüfe finalen Status
try:
container.reload()
if container.status == 'running':
return True, f"Container läuft (Timeout nach {timeout}s erreicht, aber Container läuft)"
except:
pass
return False, f"Timeout beim Warten auf Container-Start (>{timeout}s)"
def start_project(self, project_name, port=None):
"""Starte Projekt-Container"""
if not self.docker_available:
return False, "Docker ist nicht verfügbar. Bitte starten Sie Docker Desktop."
try:
# Stoppe existierenden Container falls vorhanden
try:
existing = self.docker_client.containers.get(project_name)
print(f"Stopping existing container {project_name}")
existing.stop()
existing.remove()
except docker.errors.NotFound:
pass # Container existiert nicht, das ist OK
# Prüfe ob Image existiert
try:
self.docker_client.images.get(f"{project_name}:latest")
except docker.errors.ImageNotFound:
return False, f"Image {project_name}:latest nicht gefunden. Bitte bauen Sie das Projekt zuerst."
# Port-Verwaltung verbessert
if port:
port = int(port)
if not self.check_port_availability(port):
# Suche alternativen Port
alternative_port = self.find_available_port(port)
if alternative_port:
return False, f"Port {port} ist bereits belegt. Verfügbarer alternativer Port: {alternative_port}. Möchten Sie Port {alternative_port} verwenden?"
else:
return False, f"Port {port} ist bereits belegt und keine Alternative zwischen {port}-{port+50} gefunden."
else:
# Finde automatisch einen freien Port
port = self.find_available_port()
if not port:
return False, "Kein freier Port zwischen 8080-8130 gefunden."
# Intelligentes Port-Mapping basierend auf Image-Konfiguration
ports = {}
if port:
ports = self.create_smart_port_mapping(project_name, port)
# Umgebungsvariablen laden
env_vars = {}
project_path = os.path.join(PROJECTS_DIR, project_name)
env_file = os.path.join(project_path, '.env')
if os.path.exists(env_file):
try:
with open(env_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
env_vars[key.strip()] = value.strip()
except Exception as e:
print(f"Warning: Could not load .env file: {e}")
print(f"Starting container {project_name} with ports: {ports}")
print(f"Environment variables: {env_vars}")
# Erstelle Container (aber starte noch nicht)
container = self.docker_client.containers.create(
f"{project_name}:latest",
name=project_name,
ports=ports if ports else None,
environment=env_vars,
restart_policy={"Name": "unless-stopped"},
detach=True
)
print(f"Container {project_name} created with ID: {container.id}")
# Starte Container explizit
container.start()
print(f"Container {project_name} started")
# Warte auf Container-Bereitschaft mit Timeout
success, status_msg = self.wait_for_container_ready(container, port, timeout=30)
if success:
return True, f"Container {project_name} erfolgreich gestartet auf Port {port}. Zugriff über: http://localhost:{port}"
else:
# Container-Start fehlgeschlagen - cleanup
try:
container.stop(timeout=5)
container.remove()
except Exception as cleanup_error:
print(f"Cleanup error: {cleanup_error}")
return False, f"Container-Start fehlgeschlagen: {status_msg}"
except docker.errors.ContainerError as e:
return False, f"Container-Laufzeitfehler: {str(e)}"
except docker.errors.ImageNotFound as e:
return False, f"Image nicht gefunden: {str(e)}. Bitte bauen Sie das Projekt zuerst."
except docker.errors.APIError as e:
error_msg = str(e)
if "port is already allocated" in error_msg:
# Extrahiere Port aus Fehlermeldung
import re
port_match = re.search(r':(\d+)', error_msg)
if port_match:
blocked_port = port_match.group(1)
# Suche alternativen Port
alternative_port = self.find_available_port(int(blocked_port) + 1)
if alternative_port:
return False, f"Port {blocked_port} ist bereits belegt. Alternativer freier Port: {alternative_port}"
else:
return False, f"Port {blocked_port} ist bereits belegt und keine Alternative gefunden."
else:
return False, f"Port-Konflikt: {error_msg}"
else:
return False, f"Docker-API-Fehler: {error_msg}"
except Exception as e:
return False, f"Unerwarteter Fehler beim Starten: {str(e)}"
def stop_project(self, project_name):
"""Stoppe Projekt-Container"""
try:
if self.docker_client:
container = self.docker_client.containers.get(project_name)
container.stop()
return True, f"Container {project_name} gestoppt"
else:
return False, "Docker Client nicht verfügbar"
except Exception as e:
return False, f"Fehler beim Stoppen: {str(e)}"
def force_remove_directory(self, path):
"""Entferne Verzeichnis robust unter Windows (behandelt schreibgeschützte Dateien)"""
import stat
import shutil
def handle_remove_readonly(func, path, exc):
"""Behandle schreibgeschützte Dateien unter Windows"""
try:
# Entferne readonly-Attribut und versuche erneut
os.chmod(path, stat.S_IWRITE)
func(path)
except Exception as e:
print(f"Warning: Could not remove {path}: {e}")
try:
if os.path.exists(path):
print(f"Removing directory: {path}")
shutil.rmtree(path, onerror=handle_remove_readonly)
print(f"Successfully removed: {path}")
return True
except Exception as e:
print(f"Error removing directory {path}: {e}")
# Fallback: Versuche mit Windows-Kommandos
try:
import subprocess
# Windows rmdir mit Force-Parameter
result = subprocess.run(['cmd', '/c', 'rmdir', '/s', '/q', path],
capture_output=True, text=True, timeout=30)
if result.returncode == 0:
print(f"Successfully removed with Windows rmdir: {path}")
return True
else:
print(f"Windows rmdir failed: {result.stderr}")
except Exception as fallback_error:
print(f"Fallback removal failed: {fallback_error}")
return False
return True
def remove_project(self, project_name):
"""Entferne Projekt komplett"""
try:
print(f"Starting removal of project: {project_name}")
# Stoppe und entferne Container
if self.docker_client:
try:
container = self.docker_client.containers.get(project_name)
print(f"Stopping container {project_name}")
container.stop(timeout=10)
container.remove()
print(f"Container {project_name} removed")
except docker.errors.NotFound:
print(f"Container {project_name} not found (already removed)")
except Exception as container_error:
print(f"Warning: Container removal failed: {container_error}")
# Entferne Image
try:
print(f"Removing image {project_name}:latest")
self.docker_client.images.remove(f"{project_name}:latest", force=True)
print(f"Image {project_name}:latest removed")
except docker.errors.ImageNotFound:
print(f"Image {project_name}:latest not found (already removed)")
except Exception as image_error:
print(f"Warning: Image removal failed: {image_error}")
# Entferne Projektverzeichnis mit robuster Methode
project_path = os.path.join(PROJECTS_DIR, project_name)
if os.path.exists(project_path):
print(f"Removing project directory: {project_path}")
# Versuche zuerst normale Entfernung
success = self.force_remove_directory(project_path)
if not success:
return False, f"Konnte Projektverzeichnis nicht vollständig entfernen. Versuchen Sie es als Administrator oder entfernen Sie {project_path} manuell."
# Prüfe ob Verzeichnis wirklich entfernt wurde
if os.path.exists(project_path):
return False, f"Projektverzeichnis {project_path} konnte nicht vollständig entfernt werden. Bitte manuell löschen."
print(f"Project {project_name} successfully removed")
return True, f"Projekt {project_name} vollständig entfernt"
except Exception as e:
error_msg = f"Fehler beim Entfernen: {str(e)}"
print(f"Remove project error: {error_msg}")
# Gebe hilfreiche Tipps für Windows-spezifische Probleme
if "Zugriff verweigert" in str(e) or "Access is denied" in str(e):
error_msg += "\n\nTipp: Versuchen Sie:\n1. Alle Git-Clients schließen\n2. Als Administrator ausführen\n3. Antivirus temporär deaktivieren\n4. Manuell löschen: " + os.path.join(PROJECTS_DIR, project_name)
return False, error_msg
def update_project(self, project_name):
"""Update ein Projekt durch Neuinstallation"""
try:
print(f"Starting update for project: {project_name}")
# Hole Projekt-Info um URL zu bekommen
project_info = self.get_project_info(project_name)
if not project_info:
return False, f"Projekt {project_name} nicht gefunden"
# Versuche URL aus Git-Remote zu bekommen
project_path = os.path.join(PROJECTS_DIR, project_name)
git_url = None
try:
result = subprocess.run(['git', 'remote', 'get-url', 'origin'],
cwd=project_path, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
git_url = result.stdout.strip()
except:
pass
if not git_url:
return False, "Keine Git-URL für Update gefunden"
# Stoppe Container falls laufend
container_running = False
if self.docker_available and self.docker_client:
try:
container = self.docker_client.containers.get(project_name)
if container.status == 'running':
container_running = True
container.stop()
print(f"Container {project_name} gestoppt für Update")
except:
pass
# Backup erstellen
backup_path = f"{project_path}_backup_{int(time.time())}"
try:
import shutil
shutil.copytree(project_path, backup_path)
print(f"Backup erstellt: {backup_path}")
except Exception as e:
print(f"Backup-Warnung: {e}")
# Git Pull versuchen
try:
result = subprocess.run(['git', 'pull'], cwd=project_path,
capture_output=True, text=True, timeout=60)
if result.returncode == 0:
print(f"Git pull erfolgreich für {project_name}")
# Rebuild falls Dockerfile existiert
if os.path.exists(os.path.join(project_path, 'Dockerfile')):
build_success, build_msg = self.build_project(project_name)
if not build_success:
print(f"Build-Warnung: {build_msg}")
# Container wieder starten falls er lief
if container_running:
start_success, start_msg = self.start_project(project_name)
if start_success:
print(f"Container {project_name} nach Update gestartet")
# Backup entfernen bei Erfolg
try:
shutil.rmtree(backup_path)
except:
pass
# Version nach erfolgreichem Update aktualisieren
try:
self.save_project_version(project_name, git_url)
except Exception as e:
print(f"Warnung: Konnte Version nach Update nicht aktualisieren: {e}")
return True, f"Projekt {project_name} erfolgreich aktualisiert"
else:
print(f"Git pull fehlgeschlagen: {result.stderr}")
raise Exception(f"Git pull fehlgeschlagen: {result.stderr}")
except Exception as git_error:
print(f"Git pull fehlgeschlagen, versuche Neuinstallation: {git_error}")
# Entferne altes Projekt
remove_success, remove_msg = self.remove_project(project_name)
if not remove_success:
return False, f"Update fehlgeschlagen: {remove_msg}"
# Installiere neu
clone_success, clone_msg = self.clone_project(git_url, project_name)
if clone_success:
# Build nach Neuinstallation
if os.path.exists(os.path.join(PROJECTS_DIR, project_name, 'Dockerfile')):
self.build_project(project_name)
# Starte falls vorher gelaufen
if container_running:
self.start_project(project_name)
# Backup entfernen bei erfolgreichem Update
try:
if os.path.exists(backup_path):
shutil.rmtree(backup_path)
except:
pass
return True, f"Projekt {project_name} durch Neuinstallation aktualisiert"
else:
# Restore backup bei komplettem Fehler
try:
if os.path.exists(backup_path):
if os.path.exists(project_path):
shutil.rmtree(project_path)
shutil.move(backup_path, project_path)
print(f"Backup wiederhergestellt für {project_name}")
except Exception as restore_error:
print(f"Backup-Wiederherstellung fehlgeschlagen: {restore_error}")
return False, f"Update fehlgeschlagen: {clone_msg}"
except Exception as e:
print(f"Update-Fehler für {project_name}: {e}")
return False, f"Update-Fehler: {str(e)}"
# Globale Instanz
project_manager = ProjectManager()
# ===== USER MANAGEMENT ROUTES =====
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Login-Seite"""
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
if not username or not password:
flash('Benutzername und Passwort sind erforderlich', 'danger')
return render_template('login.html')
success, result = user_manager.authenticate_user(username, password)
if success:
# Erstelle Session
session_token = user_manager.create_session(username)
session['session_token'] = session_token
session['username'] = username
session['user_role'] = result['role']
flash(f'Willkommen, {result.get("first_name", username)}!', 'success')
# Weiterleitung zur ursprünglich angeforderten Seite
next_page = request.args.get('next')
if next_page:
return redirect(next_page)
return redirect(url_for('index'))
else:
flash(result, 'danger')
return render_template('login.html')
@app.route('/logout')
def logout():
"""Logout"""
session_token = session.get('session_token')
if session_token:
user_manager.destroy_session(session_token)
session.clear()
flash('Sie wurden erfolgreich abgemeldet', 'info')
return redirect(url_for('login'))
@app.route('/profile')
@login_required
def profile():
"""Benutzer-Profil"""
user = request.current_user
return render_template('profile.html', user=user)
@app.route('/change_password', methods=['POST'])
@login_required
def change_password():
"""Passwort ändern"""
old_password = request.form.get('old_password', '')
new_password = request.form.get('new_password', '')
confirm_password = request.form.get('confirm_password', '')
if not old_password or not new_password or not confirm_password:
flash('Alle Felder sind erforderlich', 'danger')
return redirect(url_for('profile'))
if new_password != confirm_password:
flash('Neue Passwörter stimmen nicht überein', 'danger')
return redirect(url_for('profile'))
if len(new_password) < 6:
flash('Passwort muss mindestens 6 Zeichen lang sein', 'danger')
return redirect(url_for('profile'))
username = request.current_user['username']
success, message = user_manager.change_password(username, old_password, new_password)
if success:
flash(message, 'success')
else:
flash(message, 'danger')
return redirect(url_for('profile'))
@app.route('/users')
@role_required(UserRole.ADMIN)
def users_management():
"""Benutzerverwaltung"""
users = user_manager.get_all_users()
roles = UserRole.get_all_roles()
return render_template('users_management.html', users=users, roles=roles)
@app.route('/users/create', methods=['POST'])
@role_required(UserRole.ADMIN)
def create_user():
"""Benutzer erstellen"""
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
role = request.form.get('role', UserRole.USER)
first_name = request.form.get('first_name', '').strip()
last_name = request.form.get('last_name', '').strip()
if not username or not email or not password:
flash('Benutzername, E-Mail und Passwort sind erforderlich', 'danger')
return redirect(url_for('users_management'))
if len(password) < 6:
flash('Passwort muss mindestens 6 Zeichen lang sein', 'danger')
return redirect(url_for('users_management'))
success, message = user_manager.create_user(
username=username,
email=email,
password=password,
role=role,
first_name=first_name,
last_name=last_name
)
if success:
flash(message, 'success')
else:
flash(message, 'danger')
return redirect(url_for('users_management'))
@app.route('/users/<username>/edit', methods=['POST'])
@role_required(UserRole.ADMIN)
def edit_user(username):
"""Benutzer bearbeiten"""
email = request.form.get('email', '').strip()
role = request.form.get('role', UserRole.USER)
first_name = request.form.get('first_name', '').strip()
last_name = request.form.get('last_name', '').strip()
enabled = request.form.get('enabled') == 'on'
success, message = user_manager.update_user(
username=username,
email=email,
role=role,
first_name=first_name,
last_name=last_name,
enabled=enabled
)
if success:
flash(message, 'success')
else:
flash(message, 'danger')
return redirect(url_for('users_management'))
@app.route('/users/<username>/delete', methods=['POST'])
@role_required(UserRole.ADMIN)
def delete_user(username):
"""Benutzer löschen"""
success, message = user_manager.delete_user(username)
if success:
flash(message, 'success')
else:
flash(message, 'danger')
return redirect(url_for('users_management'))
# ===== PROTECTED ROUTES - Bestehende Routes mit Authentifizierung =====
@app.route('/')
@login_required
def index():
"""Hauptseite mit Projektübersicht"""
config = project_manager.load_config()
# Lade lokale Projekte
projects = []
if os.path.exists(PROJECTS_DIR):
for project_name in os.listdir(PROJECTS_DIR):
project_path = os.path.join(PROJECTS_DIR, project_name)
if os.path.isdir(project_path):
info = project_manager.get_project_info(project_name)
if info:
projects.append(info)
return render_template('index.html', projects=projects, config=config)
@app.route('/refresh_projects')
@permission_required(Permission.PROJECT_VIEW)
def refresh_projects():
"""Aktualisiere Projektliste von URL"""
config = project_manager.load_config()
if not config.get('project_list_url'):
flash('Keine Projekt-URL konfiguriert', 'error')
return redirect(url_for('index'))
projects = project_manager.fetch_project_list(config['project_list_url'])
config['projects'] = projects
project_manager.save_config(config)
flash(f'{len(projects)} Projekte gefunden und aktualisiert', 'success')
return redirect(url_for('index'))
@app.route('/available_projects')
@permission_required(Permission.PROJECT_VIEW)
def available_projects():
"""Zeige verfügbare Projekte zum Installieren"""
config = project_manager.load_config()
# Versuche zuerst aktuelle Projekte von Remote-URL zu laden
projects = []
remote_url = config.get('project_list_url')
if remote_url:
print(f"🌐 Lade aktuelle Projektliste von: {remote_url}")
try:
projects = project_manager.fetch_project_list(remote_url)
if projects:
print(f"{len(projects)} Projekte von Remote-URL geladen")
# Aktualisiere auch die lokale Konfiguration
config['projects'] = projects
project_manager.save_config(config)
else:
print("⚠ Keine Projekte von Remote-URL erhalten, verwende lokale Fallback")
projects = config.get('projects', [])
except Exception as e:
print(f"⚠ Fehler beim Laden von Remote-URL: {e}")
print("📁 Verwende lokale Projektliste als Fallback")
projects = config.get('projects', [])
else:
print("📁 Keine Remote-URL konfiguriert, verwende lokale Projekte")
projects = config.get('projects', [])
return render_template('available_projects.html', projects=projects)
@app.route('/install_project', methods=['POST'])
@permission_required(Permission.PROJECT_CREATE)
def install_project():
"""Installiere Projekt"""
project_url = request.form.get('project_url')
project_name = request.form.get('project_name')
installation_method = request.form.get('installation_method', 'clone') # 'clone' oder 'image'
force_reinstall = request.form.get('force_reinstall', 'false').lower() == 'true'
if not project_url or not project_name:
return jsonify({'success': False, 'message': 'URL und Name erforderlich'})
# Prüfe ob Projekt bereits existiert
project_path = os.path.join(PROJECTS_DIR, project_name)
if os.path.exists(project_path):
print(f" Projekt {project_name} existiert bereits, führe Update durch...")
# Existierendes Projekt: Führe automatisch Update durch
success, message = project_manager.update_project(project_name)
if success:
# Hole aktuelle installierte Version nach Update
installed_version = project_manager.get_installed_version(project_name)
return jsonify({
'success': True,
'message': f'Projekt {project_name} wurde aktualisiert (Version: {installed_version})',
'updated': True,
'version': installed_version
})
else:
return jsonify({
'success': False,
'message': f'Update von {project_name} fehlgeschlagen: {message}'
})
# Neue Installation basierend auf Methode
if installation_method in ['image', 'docker_image', 'docker_registry']:
# Docker-Image herunterladen (aus Registry)
success, message = project_manager.pull_docker_image(project_url, project_name)
if success:
# Nach erfolgreichem Download die Version speichern
try:
project_manager.save_project_version(project_name, project_url, installation_method=installation_method)
except Exception as e:
print(f"Warnung: Konnte Version nach Image-Download nicht speichern: {e}")
return jsonify({
'success': True,
'message': f'{message}. Bereit zum Starten.',
'method': installation_method
})
else:
return jsonify({'success': False, 'message': message})
elif installation_method in ['docker_url']:
# Docker-Image von URL herunterladen (z.B. .tar Datei)
success, message = project_manager.load_docker_image_from_url(project_url, project_name)
if success:
try:
project_manager.save_project_version(project_name, project_url, installation_method='docker_url')
except Exception as e:
print(f"Warnung: Konnte Version nach URL-Download nicht speichern: {e}")
return jsonify({
'success': True,
'message': f'{message}. Bereit zum Starten.',
'method': 'docker_url'
})
else:
return jsonify({'success': False, 'message': message})
elif installation_method in ['docker_file']:
# Prüfe ob es eine .tar URL ist oder ein Repository
if project_url.endswith('.tar') or '/images/' in project_url:
# .tar Datei von URL laden
success, message = project_manager.load_docker_image_from_url(project_url, project_name)
method = 'docker_url'
else:
# Repository klonen und Dockerfile bauen
print(f"🐳 Dockerfile-Installation für {project_name} von {project_url}")
success, message = project_manager.clone_project(project_url, project_name, save_version=False)
if success:
# Versuche Docker-Image aus Dockerfile zu bauen
build_success, build_message = project_manager.build_project(project_name)
if build_success:
message = f'Projekt geklont und Docker-Image gebaut: {build_message}'
else:
success = False
message = f'Projekt geklont, aber Docker-Build fehlgeschlagen: {build_message}'
method = 'docker_file'
if success:
try:
project_manager.save_project_version(project_name, project_url, installation_method=method)
except Exception as e:
print(f"Warnung: Konnte Version nach Installation nicht speichern: {e}")
return jsonify({
'success': True,
'message': f'{message}. Bereit zum Starten.',
'method': method
})
else:
return jsonify({'success': False, 'message': message})
elif installation_method in ['dockerfile', 'docker_build']:
# Dockerfile-basierte Installation: Klone Repository und baue Docker-Image
print(f"🐳 Dockerfile-Installation für {project_name} von {project_url}")
# Erst klonen, dann Docker-Image bauen
success, message = project_manager.clone_project(project_url, project_name, save_version=False)
if success:
# Versuche Docker-Image aus Dockerfile zu bauen
build_success, build_message = project_manager.build_project(project_name)
if build_success:
# Nach erfolgreichem Build die Version speichern
try:
project_manager.save_project_version(project_name, project_url, installation_method='dockerfile')
except Exception as e:
print(f"Warnung: Konnte Version nach Docker-Build nicht speichern: {e}")
return jsonify({
'success': True,
'message': f'Projekt geklont und Docker-Image gebaut: {build_message}',
'method': 'dockerfile'
})
else:
return jsonify({
'success': False,
'message': f'Projekt geklont, aber Docker-Build fehlgeschlagen: {build_message}'
})
else:
return jsonify({'success': False, 'message': message})
else:
# Standard: Git Clone
success, message = project_manager.clone_project(project_url, project_name, save_version=False)
if success:
# Nach erfolgreichem Klonen die Version mit der richtigen Methode speichern
try:
project_manager.save_project_version(project_name, project_url, installation_method=installation_method)
print(f"✓ Version für {project_name} mit Methode '{installation_method}' gespeichert")
except Exception as e:
print(f"⚠ Warnung: Konnte Version für {project_name} nicht speichern: {e}")
# Versuche automatisch zu bauen
build_success, build_message = project_manager.build_project(project_name)
if build_success:
message += f" und gebaut: {build_message}"
else:
message += f" (Build fehlgeschlagen: {build_message})"
return jsonify({'success': success, 'message': message, 'method': installation_method})
@app.route('/build_project/<project_name>')
@permission_required(Permission.PROJECT_UPDATE)
def build_project(project_name):
"""Baue Projekt"""
success, message = project_manager.build_project(project_name)
flash(message, 'success' if success else 'error')
return redirect(url_for('index'))
@app.route('/start_project/<project_name>')
@permission_required(Permission.PROJECT_START)
def start_project(project_name):
"""Starte Projekt"""
port = request.args.get('port', None)
# Wenn kein Port angegeben, finde einen freien
if not port:
available_port = project_manager.find_available_port()
if available_port:
port = available_port
else:
flash('Kein freier Port verfügbar', 'error')
return redirect(url_for('project_details', project_name=project_name))
success, message = project_manager.start_project(project_name, port)
# Überprüfe ob es ein API-Request ist (AJAX)
if request.headers.get('Accept') == 'application/json':
return jsonify({'success': success, 'message': message, 'port': port})
flash(message, 'success' if success else 'error')
# Umleitung basierend auf Erfolg
if success:
return redirect(url_for('project_details', project_name=project_name))
else:
return redirect(url_for('project_details', project_name=project_name))
@app.route('/stop_project/<project_name>')
@permission_required(Permission.PROJECT_STOP)
def stop_project(project_name):
"""Stoppe Projekt"""
success, message = project_manager.stop_project(project_name)
flash(message, 'success' if success else 'error')
return redirect(url_for('index'))
@app.route('/remove_project/<project_name>')
@permission_required(Permission.PROJECT_DELETE)
def remove_project(project_name):
"""Entferne Projekt"""
success, message = project_manager.remove_project(project_name)
flash(message, 'success' if success else 'error')
return redirect(url_for('index'))
@app.route('/config', methods=['GET', 'POST'])
@permission_required(Permission.SYSTEM_CONFIG)
def config():
"""Konfigurationsseite"""
try:
if request.method == 'POST':
# Sichere Formular-Validierung
project_list_url = request.form.get('project_list_url', '').strip()
auto_refresh_minutes = request.form.get('auto_refresh_minutes', '30')
docker_registry = request.form.get('docker_registry', '').strip()
# Validiere auto_refresh_minutes
try:
auto_refresh_minutes = int(auto_refresh_minutes)
if auto_refresh_minutes < 5 or auto_refresh_minutes > 1440:
auto_refresh_minutes = 30
except (ValueError, TypeError):
auto_refresh_minutes = 30
current_config = project_manager.load_config()
config = {
'project_list_url': project_list_url,
'auto_refresh_minutes': auto_refresh_minutes,
'docker_registry': docker_registry,
'projects': current_config.get('projects', [])
}
project_manager.save_config(config)
flash('Konfiguration gespeichert', 'success')
return redirect(url_for('config'))
# GET request - lade Konfiguration
config = project_manager.load_config()
# Stelle sicher, dass alle erwarteten Konfigurationswerte vorhanden sind
default_config = {
'project_list_url': '',
'auto_refresh_minutes': 30,
'docker_registry': '',
'projects': []
}
# Merge mit defaults
for key, default_value in default_config.items():
if key not in config:
config[key] = default_value
return render_template('config.html', config=config)
except Exception as e:
print(f"Config Route Fehler: {e}")
flash(f'Fehler beim Laden der Konfiguration: {str(e)}', 'error')
# Fallback config
fallback_config = {
'project_list_url': '',
'auto_refresh_minutes': 30,
'docker_registry': '',
'projects': []
}
return render_template('config.html', config=fallback_config)
@app.route('/project_details/<project_name>')
@permission_required(Permission.PROJECT_VIEW)
def project_details(project_name):
"""Projektdetails und Konfiguration"""
info = project_manager.get_project_info(project_name)
if not info:
flash('Projekt nicht gefunden', 'error')
return redirect(url_for('index'))
# Lade .env Datei falls vorhanden
env_content = ''
env_path = os.path.join(info['path'], '.env')
if os.path.exists(env_path):
with open(env_path, 'r', encoding='utf-8') as f:
env_content = f.read()
elif info['has_env_example']:
env_example_path = os.path.join(info['path'], '.env.example')
with open(env_example_path, 'r', encoding='utf-8') as f:
env_content = f.read()
return render_template('project_details.html', project=info, env_content=env_content)
@app.route('/save_env/<project_name>', methods=['POST'])
@permission_required(Permission.PROJECT_FILES)
def save_env(project_name):
"""Speichere .env Konfiguration"""
env_content = request.form.get('env_content', '')
project_path = os.path.join(PROJECTS_DIR, project_name)
env_path = os.path.join(project_path, '.env')
try:
with open(env_path, 'w', encoding='utf-8') as f:
f.write(env_content)
flash('.env Datei gespeichert', 'success')
except Exception as e:
flash(f'Fehler beim Speichern: {str(e)}', 'error')
return redirect(url_for('project_details', project_name=project_name))
@app.route('/api/system_status')
@api_permission_required(Permission.SYSTEM_MONITORING)
def api_system_status():
"""API Endpoint für Systemstatus"""
status = {
'docker': {'available': False, 'version': None, 'status': 'checking'},
'git': {'available': False, 'version': None, 'status': 'checking'},
'project_dir': {'available': os.path.exists(PROJECTS_DIR), 'status': 'ok'},
'disk_space': {'available': True, 'free': 'Unknown', 'status': 'ok'}
}
# Docker Status
try:
if project_manager.docker_available and project_manager.docker_client:
try:
project_manager.docker_client.ping()
result = subprocess.run(['docker', '--version'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
version = result.stdout.strip()
status['docker'] = {'available': True, 'version': version, 'status': 'running'}
else:
status['docker'] = {'available': False, 'version': None, 'status': 'version_check_failed'}
except Exception:
status['docker'] = {'available': False, 'version': None, 'status': 'daemon_not_reachable'}
else:
# Versuche Docker-Installation zu prüfen
result = subprocess.run(['docker', '--version'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
version = result.stdout.strip()
status['docker'] = {'available': True, 'version': version, 'status': 'daemon_stopped'}
else:
status['docker'] = {'available': False, 'version': None, 'status': 'not_installed'}
except subprocess.TimeoutExpired:
status['docker'] = {'available': False, 'version': None, 'status': 'timeout'}
except FileNotFoundError:
status['docker'] = {'available': False, 'version': None, 'status': 'not_found'}
except Exception as e:
status['docker'] = {'available': False, 'version': None, 'status': f'error: {str(e)}'}
# Git Status
try:
result = subprocess.run(['git', '--version'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
status['git'] = {'available': True, 'version': result.stdout.strip(), 'status': 'ok'}
else:
status['git'] = {'available': False, 'version': None, 'status': 'error'}
except FileNotFoundError:
status['git'] = {'available': False, 'version': None, 'status': 'not_found'}
except Exception as e:
status['git'] = {'available': False, 'version': None, 'status': f'error: {str(e)}'}
# Festplattenspeicher hinzufügen
try:
import shutil
total, used, free = shutil.disk_usage(PROJECTS_DIR)
free_gb = round(free / (1024**3), 1)
status['disk_space'] = {
'available': free > (1024**3), # Mehr als 1GB frei
'free': f'{free_gb} GB',
'status': 'ok' if free > (1024**3) else 'low'
}
except Exception:
status['disk_space'] = {'available': False, 'free': 'Unknown', 'status': 'error'}
return jsonify(status)
@app.route('/api/test_connection', methods=['POST'])
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_test_connection():
"""Test Verbindung zu Projektliste URL"""
data = request.get_json()
url = data.get('url', '')
if not url:
return jsonify({'success': False, 'error': 'Keine URL angegeben'})
try:
projects = project_manager.fetch_project_list(url)
return jsonify({
'success': True,
'projects_found': len(projects),
'projects': projects[:5] # Erste 5 als Beispiel
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/container_logs/<project_name>')
@api_permission_required(Permission.PROJECT_LOGS)
def api_container_logs(project_name):
"""Hole Container-Logs für Debugging"""
try:
if not project_manager.docker_available:
return jsonify({'success': False, 'error': 'Docker nicht verfügbar'})
container = project_manager.docker_client.containers.get(project_name)
logs = container.logs(tail=100).decode('utf-8', errors='ignore')
return jsonify({
'success': True,
'logs': logs,
'status': container.status,
'container_id': container.id
})
except docker.errors.NotFound:
return jsonify({'success': False, 'error': 'Container nicht gefunden'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/container_inspect/<project_name>')
@api_permission_required(Permission.PROJECT_VIEW)
def api_container_inspect(project_name):
"""Detaillierte Container-Informationen für Debugging"""
try:
if not project_manager.docker_available:
return jsonify({'success': False, 'error': 'Docker nicht verfügbar'})
container = project_manager.docker_client.containers.get(project_name)
# Hole relevante Container-Details
info = {
'id': container.id,
'status': container.status,
'image': container.image.tags[0] if container.image.tags else 'unknown',
'ports': container.ports,
'environment': container.attrs.get('Config', {}).get('Env', []),
'restart_policy': container.attrs.get('HostConfig', {}).get('RestartPolicy', {}),
'exit_code': container.attrs.get('State', {}).get('ExitCode'),
'error': container.attrs.get('State', {}).get('Error'),
'started_at': container.attrs.get('State', {}).get('StartedAt'),
'finished_at': container.attrs.get('State', {}).get('FinishedAt')
}
return jsonify({
'success': True,
'container_info': info
})
except docker.errors.NotFound:
return jsonify({'success': False, 'error': 'Container nicht gefunden'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/container_stats/<project_name>')
@api_permission_required(Permission.PROJECT_VIEW)
def api_container_stats(project_name):
"""Container Statistiken abrufen"""
try:
if project_manager.docker_client:
container = project_manager.docker_client.containers.get(project_name)
stats = container.stats(stream=False)
# Vereinfachte Statistiken
cpu_percent = 0
memory_mb = 0
if 'cpu_stats' in stats and 'precpu_stats' in stats:
cpu_delta = stats['cpu_stats']['cpu_usage']['total_usage'] - stats['precpu_stats']['cpu_usage']['total_usage']
system_delta = stats['cpu_stats']['system_cpu_usage'] - stats['precpu_stats']['system_cpu_usage']
if system_delta > 0:
cpu_percent = (cpu_delta / system_delta) * 100.0
if 'memory_stats' in stats:
memory_mb = stats['memory_stats'].get('usage', 0) / 1024 / 1024
return jsonify({
'success': True,
'stats': {
'cpu': f"{cpu_percent:.1f}",
'memory': f"{memory_mb:.0f}",
'network_in': 'N/A',
'network_out': 'N/A'
}
})
else:
return jsonify({'success': False, 'error': 'Docker Client nicht verfügbar'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/restart_project/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_RESTART)
def api_restart_project(project_name):
"""Projekt Container neustarten"""
try:
if project_manager.docker_client:
container = project_manager.docker_client.containers.get(project_name)
container.restart()
return jsonify({'success': True, 'message': f'Container {project_name} neu gestartet'})
else:
return jsonify({'success': False, 'error': 'Docker Client nicht verfügbar'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/clear_cache', methods=['POST'])
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_clear_cache():
"""Cache leeren"""
try:
# Hier könnte Cache-Logik implementiert werden
return jsonify({'success': True, 'message': 'Cache geleert'})
except Exception as e:
return jsonify({'success': False, 'message': f'Fehler: {str(e)}'})
@app.route('/api/export_config')
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_export_config():
"""Konfiguration exportieren"""
try:
config = project_manager.load_config()
response = app.response_class(
response=json.dumps(config, indent=2, ensure_ascii=False),
status=200,
mimetype='application/json',
headers={'Content-Disposition': 'attachment; filename=app_installer_config.json'}
)
return response
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/import_config', methods=['POST'])
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_import_config():
"""Konfiguration importieren"""
try:
config = request.get_json()
project_manager.save_config(config)
return jsonify({'success': True, 'message': 'Konfiguration importiert'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/reset_config', methods=['POST'])
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_reset_config():
"""Konfiguration zurücksetzen"""
try:
default_config = {
'project_list_url': '',
'auto_refresh_minutes': 30,
'docker_registry': '',
'projects': []
}
project_manager.save_config(default_config)
return jsonify({'success': True, 'message': 'Konfiguration zurückgesetzt'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@app.route('/docker_status')
@permission_required(Permission.SYSTEM_DOCKER)
def docker_status():
"""Docker Status und Diagnose Seite"""
try:
# Lade Standard-Konfiguration für Template-Konsistenz
config = project_manager.load_config()
return render_template('docker_status.html', config=config)
except Exception as e:
print(f"Docker Status Route Fehler: {e}")
# Fallback bei Fehlern
fallback_config = {
'project_list_url': '',
'auto_refresh_minutes': 30,
'docker_registry': '',
'projects': []
}
return render_template('docker_status.html', config=fallback_config)
@app.route('/api/docker_diagnose')
@api_permission_required(Permission.SYSTEM_DOCKER)
def api_docker_diagnose():
"""Docker Diagnose API"""
try:
results = {
'timestamp': datetime.now().isoformat(),
'checks': {},
'recommendations': []
}
# Docker Version
try:
result = subprocess.run(['docker', '--version'], capture_output=True, text=True, timeout=10)
if result.returncode == 0:
results['checks']['docker_version'] = {
'success': True,
'output': result.stdout.strip(),
'details': 'Docker ist installiert'
}
else:
results['checks']['docker_version'] = {
'success': False,
'output': result.stderr.strip() if result.stderr else 'Fehler ohne Ausgabe',
'details': 'Docker Kommando fehlgeschlagen'
}
results['recommendations'].append('Docker Desktop installieren')
except FileNotFoundError:
results['checks']['docker_version'] = {
'success': False,
'output': 'Docker nicht gefunden',
'details': 'Docker ist nicht installiert'
}
results['recommendations'].append('Docker Desktop von https://docker.com herunterladen')
except Exception as e:
results['checks']['docker_version'] = {
'success': False,
'output': f'Unerwarteter Fehler: {str(e)}',
'details': 'Docker Version Check fehlgeschlagen'
}
# Docker Daemon
if project_manager.docker_available and project_manager.docker_client:
try:
project_manager.docker_client.ping()
results['checks']['docker_daemon'] = {
'success': True,
'output': 'Docker Daemon läuft',
'details': 'Verbindung erfolgreich'
}
except Exception as e:
results['checks']['docker_daemon'] = {
'success': False,
'output': 'Daemon nicht erreichbar',
'details': str(e)
}
results['recommendations'].append('Docker Desktop starten')
else:
results['checks']['docker_daemon'] = {
'success': False,
'output': 'Daemon nicht verfügbar',
'details': 'Docker Client nicht initialisiert'
}
results['recommendations'].append('Docker Desktop starten')
# Container Status
if project_manager.docker_available and project_manager.docker_client:
try:
containers = project_manager.docker_client.containers.list(all=True)
container_data = []
for c in containers:
try:
container_data.append({
'name': getattr(c, 'name', 'unknown'),
'status': getattr(c, 'status', 'unknown'),
'State': getattr(c, 'status', 'unknown')
})
except Exception:
container_data.append({
'name': 'unknown',
'status': 'unknown',
'State': 'unknown'
})
results['checks']['containers'] = {
'success': True,
'output': f'{len(containers)} Container',
'details': f'Gefunden: {len(containers)} Container',
'containers': container_data
}
except Exception as e:
results['checks']['containers'] = {
'success': False,
'output': 'Container-Check fehlgeschlagen',
'details': str(e),
'containers': []
}
else:
results['checks']['containers'] = {
'success': False,
'output': 'Docker nicht verfügbar',
'details': 'Docker Client nicht initialisiert',
'containers': []
}
# Images
if project_manager.docker_available and project_manager.docker_client:
try:
images = project_manager.docker_client.images.list()
results['checks']['images'] = {
'success': True,
'output': f'{len(images)} Images',
'details': f'Verfügbare Images: {len(images)}'
}
except Exception as e:
results['checks']['images'] = {
'success': False,
'output': 'Image-Check fehlgeschlagen',
'details': str(e)
}
else:
results['checks']['images'] = {
'success': False,
'output': 'Docker nicht verfügbar',
'details': 'Docker Client nicht initialisiert'
}
return jsonify(results)
except Exception as e:
print(f"Docker Diagnose API Fehler: {e}")
return jsonify({
'error': str(e),
'timestamp': datetime.now().isoformat(),
'checks': {},
'recommendations': ['Systemfehler - bitte versuchen Sie es erneut']
}), 500
@app.route('/api/check_port/<int:port>')
@api_permission_required(Permission.SYSTEM_MONITORING)
def api_check_port(port):
"""Prüfe ob ein Port verfügbar ist"""
try:
available = project_manager.check_port_availability(port)
return jsonify({
'success': True,
'port': port,
'available': available,
'message': f'Port {port} ist {"verfügbar" if available else "belegt"}'
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
})
@app.route('/api/find_available_port')
@api_permission_required(Permission.SYSTEM_MONITORING)
def api_find_available_port():
"""Finde einen verfügbaren Port"""
try:
start_port = request.args.get('start', 8080, type=int)
available_port = project_manager.find_available_port(start_port)
if available_port:
return jsonify({
'success': True,
'port': available_port,
'message': f'Port {available_port} ist verfügbar'
})
else:
return jsonify({
'success': False,
'error': f'Kein freier Port ab {start_port} gefunden'
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
})
@app.route('/api/start_project/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_START)
def api_start_project(project_name):
"""API Endpoint zum Starten eines Projekts"""
try:
data = request.get_json() or {}
port = data.get('port') or request.args.get('port')
# Wenn kein Port angegeben, finde einen freien
if not port:
available_port = project_manager.find_available_port()
if available_port:
port = available_port
else:
return jsonify({
'success': False,
'error': 'Kein freier Port zwischen 8080-8130 verfügbar'
})
success, message = project_manager.start_project(project_name, port)
# Konsistente API-Antwort
response = {
'success': success,
'project_name': project_name
}
if success:
response['message'] = message
response['port'] = port
else:
response['error'] = message
# Bei Port-Konflikten, versuche Alternative zu finden
if 'bereits belegt' in message or 'port is already allocated' in message:
# Extrahiere Port aus Nachricht und finde Alternative
import re
port_match = re.search(r'(\d+)', message)
if port_match:
blocked_port = int(port_match.group(1))
alternative_port = project_manager.find_available_port(blocked_port + 1)
if alternative_port:
response['alternative_port'] = alternative_port
response['error'] = f"Port {blocked_port} ist belegt. Alternativer Port: {alternative_port}"
return jsonify(response)
except Exception as e:
return jsonify({
'success': False,
'error': f'Unerwarteter Fehler: {str(e)}'
})
@app.route('/api/start_native/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_START)
def api_start_native(project_name):
"""Starte Projekt nativ (ohne Docker)"""
try:
data = request.get_json() or {}
mode = data.get('mode', 'custom')
command = data.get('command', '')
project_path = os.path.join(PROJECTS_DIR, project_name)
if not os.path.exists(project_path):
return jsonify({'success': False, 'error': 'Projekt nicht gefunden'})
# Bestimme den Befehl basierend auf dem Modus
if mode == 'batch':
if os.path.exists(os.path.join(project_path, 'start.bat')):
command = 'start.bat'
else:
return jsonify({'success': False, 'error': 'start.bat nicht gefunden'})
elif mode == 'shell':
if os.path.exists(os.path.join(project_path, 'start.sh')):
command = './start.sh'
else:
return jsonify({'success': False, 'error': 'start.sh nicht gefunden'})
elif mode == 'nodejs':
if not command:
command = 'npm start'
if not os.path.exists(os.path.join(project_path, 'package.json')):
return jsonify({'success': False, 'error': 'package.json nicht gefunden'})
elif mode == 'python':
if not command:
# Suche nach Python-Dateien
for py_file in ['app.py', 'main.py', 'server.py']:
if os.path.exists(os.path.join(project_path, py_file)):
command = f'python {py_file}'
break
if not command:
command = 'python app.py'
if not command:
return jsonify({'success': False, 'error': 'Kein Start-Befehl definiert'})
# Starte Prozess im Hintergrund
try:
subprocess.Popen(
command.split(),
cwd=project_path,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True if os.name == 'nt' else False
)
return jsonify({
'success': True,
'message': f'Projekt {project_name} nativ gestartet mit: {command}'
})
except Exception as e:
return jsonify({'success': False, 'error': f'Fehler beim Start: {str(e)}'})
except Exception as e:
return jsonify({'success': False, 'error': f'API-Fehler: {str(e)}'})
@app.route('/api/open_terminal/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_CONSOLE)
def api_open_terminal(project_name):
"""Öffne Terminal im Projektordner"""
try:
project_path = os.path.join(PROJECTS_DIR, project_name)
if not os.path.exists(project_path):
return jsonify({'success': False, 'error': 'Projekt nicht gefunden'})
# Öffne Terminal je nach Betriebssystem
if os.name == 'nt': # Windows
subprocess.Popen(['cmd', '/c', 'start', 'cmd', '/k', f'cd /d "{project_path}"'])
else: # Linux/macOS
subprocess.Popen(['gnome-terminal', '--working-directory', project_path])
return jsonify({'success': True, 'message': 'Terminal geöffnet'})
except Exception as e:
return jsonify({'success': False, 'error': f'Fehler beim Öffnen des Terminals: {str(e)}'})
# Test Icons Template Route
@app.route('/templates/test_icons.html')
@permission_required(Permission.SYSTEM_CONFIG)
def serve_test_icons():
"""Serve Icon Test Template"""
with open('test_icons.html', 'r', encoding='utf-8') as f:
return f.read(), 200, {'Content-Type': 'text/html; charset=utf-8'}
@app.route('/update_project', methods=['POST'])
@permission_required(Permission.PROJECT_UPDATE)
def update_project():
"""Update ein Projekt"""
project_name = request.form.get('project_name')
if not project_name:
return jsonify({'success': False, 'message': 'Projektname erforderlich'})
# Update Projekt
success, message = project_manager.update_project(project_name)
return jsonify({'success': success, 'message': message})
@app.route('/update_project_get/<project_name>')
@permission_required(Permission.PROJECT_VIEW)
def update_project_get(project_name):
"""Update Projekt via GET (Weiterleitung)"""
success, message = project_manager.update_project(project_name)
flash(message, 'success' if success else 'error')
return redirect(url_for('index'))
@app.route('/api/update_project/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_UPDATE)
def api_update_project(project_name):
"""API Endpoint zum Updaten eines Projekts"""
try:
success, message = project_manager.update_project(project_name)
return jsonify({
'success': success,
'message': message
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/installed_projects')
@api_permission_required(Permission.PROJECT_VIEW)
def api_installed_projects():
"""API Endpoint für installierte Projekte mit Versionsinfo"""
try:
installed_projects = []
for project_name in os.listdir(PROJECTS_DIR):
project_path = os.path.join(PROJECTS_DIR, project_name)
if os.path.isdir(project_path):
project_info = {
'name': project_name,
'status': project_manager.get_container_status(project_name),
'version': project_manager.get_installed_version(project_name) or '1.0.0'
}
installed_projects.append(project_info)
return jsonify({
'success': True,
'projects': installed_projects,
'count': len(installed_projects)
})
except Exception as e:
print(f"Fehler bei API installed_projects: {e}")
return jsonify({
'success': False,
'message': str(e),
'projects': [],
'count': 0
})
@app.route('/api/projects_list.json')
@api_permission_required(Permission.PROJECT_VIEW)
def api_projects_list():
"""API Endpoint für die lokale Projektliste (für Tests)"""
try:
with open('projects_list.json', 'r', encoding='utf-8') as f:
projects = json.load(f)
return jsonify(projects)
except Exception as e:
return jsonify([]), 500
@app.route('/api/refresh_projects', methods=['POST'])
@api_permission_required(Permission.PROJECT_VIEW)
def api_refresh_projects():
"""API Endpoint zum Aktualisieren der Projektliste via AJAX"""
try:
config = project_manager.load_config()
if not config.get('project_list_url'):
return jsonify({
'success': False,
'error': 'Keine Projekt-URL konfiguriert'
})
# Lade aktuelle Projekte von Remote-URL
projects = project_manager.fetch_project_list(config['project_list_url'])
if projects:
# Aktualisiere lokale Konfiguration
config['projects'] = projects
project_manager.save_config(config)
return jsonify({
'success': True,
'message': f'{len(projects)} Projekte erfolgreich aktualisiert',
'projects': projects,
'count': len(projects)
})
else:
return jsonify({
'success': False,
'error': 'Keine Projekte von Remote-URL empfangen'
})
except Exception as e:
print(f"Fehler bei API refresh_projects: {e}")
return jsonify({
'success': False,
'error': f'Fehler beim Aktualisieren: {str(e)}'
})
@app.route('/custom_install/<project_name>')
@permission_required(Permission.PROJECT_VIEW)
def custom_install(project_name):
"""Custom Installation Seite für ein Projekt"""
config = project_manager.load_config()
# Finde das Projekt in der Konfiguration
project = None
for p in config.get('projects', []):
if p.get('name') == project_name:
project = p
break
if not project:
flash('Projekt nicht gefunden', 'error')
return redirect(url_for('available_projects'))
# Ermittle verfügbare Installationsmethoden
available_methods = []
preferred_method = None
if 'installation' in project and 'methods' in project['installation']:
for method_type, method_info in project['installation']['methods'].items():
if method_info.get('available', True):
available_methods.append((method_type, method_info))
# Prüfe ob dies die bevorzugte Methode ist
if project['installation'].get('preferred') == method_type:
preferred_method = (method_type, method_info)
# Falls keine bevorzugte Methode gesetzt, nimm die erste verfügbare
if not preferred_method and available_methods:
preferred_method = available_methods[0]
else:
# Fallback für alte Projekte ohne neues Schema
if 'url' in project:
method_info = {
'url': project['url'],
'description': 'Quellcode klonen und bauen',
'available': True
}
available_methods = [('clone', method_info)]
preferred_method = ('clone', method_info)
return render_template('custom_install.html',
project=project,
available_methods=available_methods,
preferred_method=preferred_method)
def check_port_available(port):
"""Prüfe ob ein Port verfügbar ist"""
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(1)
result = sock.connect_ex(('localhost', int(port)))
return result != 0 # Port ist frei wenn Verbindung fehlschlägt
except:
return False
# ===== FILE MANAGER ROUTES =====
@app.route('/file_manager')
@login_required
@permission_required(Permission.FILE_ACCESS)
def file_manager_view():
"""File Manager Hauptseite"""
return render_template('file_manager_new.html')
@app.route('/api/files/list')
@api_permission_required(Permission.FILE_ACCESS)
def api_file_list():
"""API: Liste Dateien und Ordner"""
path = request.args.get('path', '')
result = file_manager.list_directory(path)
return jsonify(result)
@app.route('/api/files/read')
@api_permission_required(Permission.FILE_ACCESS)
def api_file_read():
"""API: Lese Dateiinhalt"""
path = request.args.get('path', '')
if not path:
return jsonify({'error': 'Kein Pfad angegeben'})
result = file_manager.read_file(path)
return jsonify(result)
@app.route('/api/files/write', methods=['POST'])
@api_permission_required(Permission.FILE_WRITE)
def api_file_write():
"""API: Schreibe Dateiinhalt"""
data = request.get_json()
if not data or 'path' not in data or 'content' not in data:
return jsonify({'error': 'Pfad und Inhalt erforderlich'})
result = file_manager.write_file(data['path'], data['content'])
return jsonify(result)
@app.route('/api/files/delete', methods=['POST'])
@api_permission_required(Permission.FILE_WRITE)
def api_file_delete():
"""API: Lösche Datei oder Verzeichnis"""
data = request.get_json()
if not data or 'path' not in data:
return jsonify({'error': 'Pfad erforderlich'})
result = file_manager.delete_item(data['path'])
return jsonify(result)
@app.route('/api/files/create_directory', methods=['POST'])
@api_permission_required(Permission.FILE_WRITE)
def api_create_directory():
"""API: Erstelle neues Verzeichnis"""
data = request.get_json()
if not data or 'path' not in data:
return jsonify({'error': 'Pfad erforderlich'})
result = file_manager.create_directory(data['path'])
return jsonify(result)
@app.route('/api/files/rename', methods=['POST'])
@api_permission_required(Permission.FILE_WRITE)
def api_file_rename():
"""API: Benenne Datei oder Verzeichnis um"""
data = request.get_json()
if not data or 'path' not in data or 'new_name' not in data:
return jsonify({'error': 'Pfad und neuer Name erforderlich'})
result = file_manager.rename_item(data['path'], data['new_name'])
return jsonify(result)
@app.route('/api/files/upload', methods=['POST'])
@api_permission_required(Permission.FILE_WRITE)
def api_file_upload():
"""API: Datei hochladen"""
try:
if 'file' not in request.files:
return jsonify({'error': 'Keine Datei ausgewählt'})
file = request.files['file']
path = request.form.get('path', '')
if file.filename == '':
return jsonify({'error': 'Keine Datei ausgewählt'})
# Sicherer Dateiname
filename = file.filename
if path:
upload_path = os.path.join(path, filename)
else:
upload_path = filename
# Verwende FileManager für sicheren Pfad
safe_path = file_manager.get_safe_path(upload_path)
# Erstelle Verzeichnis falls nicht vorhanden
os.makedirs(os.path.dirname(safe_path), exist_ok=True)
# Speichere Datei
file.save(safe_path)
return jsonify({
'success': True,
'message': f'Datei {filename} erfolgreich hochgeladen'
})
except Exception as e:
return jsonify({'error': str(e)})
@app.route('/api/files/download')
@api_permission_required(Permission.FILE_ACCESS)
def api_file_download():
"""API: Datei herunterladen"""
try:
path = request.args.get('path', '')
if not path:
return jsonify({'error': 'Kein Pfad angegeben'})
safe_path = file_manager.get_safe_path(path)
if not os.path.exists(safe_path):
return jsonify({'error': 'Datei existiert nicht'})
if os.path.isdir(safe_path):
return jsonify({'error': 'Pfad ist ein Verzeichnis'})
# Sende Datei zum Download
directory = os.path.dirname(safe_path)
filename = os.path.basename(safe_path)
return send_from_directory(directory, filename, as_attachment=True)
except Exception as e:
return jsonify({'error': str(e)})
if __name__ == '__main__':
# Finde verfügbaren Port
port = 5000
while not check_port_available(port) and port < 5010:
port += 1
if port >= 5010:
print("❌ Keine verfügbaren Ports gefunden (5000-5009)")
exit(1)
# Zeige Startup-Informationen
print(f"📱 Flask App läuft auf: http://localhost:{port}")
print(f"🐳 Docker Status: {'✅ Verfügbar' if project_manager.docker_available else '❌ Nicht verfügbar'}")
print(f"📁 Projekte Ordner: {os.path.abspath(PROJECTS_DIR)}")
print(f"⚙️ Apps Ordner: {os.path.abspath(APPS_DIR)}")
print("=" * 60)
try:
app.run(
host='0.0.0.0',
port=port,
debug=True,
use_reloader=False # Verhindert doppelte Initialisierung
)
except KeyboardInterrupt:
print("\n" + "=" * 60)
print("🛑 App Installer wurde beendet")
print("=" * 60)
except Exception as e:
print(f"\n❌ Fehler beim Starten der App: {e}")
print("=" * 60)