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//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//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/') @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/') @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/') @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/') @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/') @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/', 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/') @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/') @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/') @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/', 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/') @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/', 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/', 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/', 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/') @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/', 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/') @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)