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
2964 lines
120 KiB
Python
2964 lines
120 KiB
Python
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)
|