""" Upload-Blueprint für das 3D-Druck-Management-System Dieses Modul enthält alle Routen und Funktionen für Datei-Uploads. """ from flask import Blueprint, jsonify, request, send_file, abort from flask_login import login_required, current_user from werkzeug.utils import secure_filename from functools import wraps import os from typing import Dict, Any from datetime import datetime from models import get_db_session, SystemLog from utils.logging_config import get_logger from utils.file_manager import file_manager, save_job_file, save_guest_file, save_avatar_file, save_asset_file, save_log_file, save_backup_file, save_temp_file, delete_file as delete_file_safe from utils.settings import UPLOAD_FOLDER, ALLOWED_EXTENSIONS # Blueprint erstellen uploads_blueprint = Blueprint('uploads', __name__, url_prefix='/api') # Logger initialisieren uploads_logger = get_logger("uploads") def admin_required(f): """Decorator für Admin-geschützte Routen""" @wraps(f) @login_required def decorated_function(*args, **kwargs): if not current_user.is_admin: abort(403) return f(*args, **kwargs) return decorated_function def allowed_file(filename: str) -> bool: """Prüft ob die Dateiendung erlaubt ist""" return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @uploads_blueprint.route('/upload/job', methods=['POST']) @login_required def upload_job_file(): """Lädt eine Job-Datei hoch""" try: if 'file' not in request.files: return jsonify({'error': 'Keine Datei hochgeladen'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'Keine Datei ausgewählt'}), 400 if not allowed_file(file.filename): return jsonify({'error': 'Dateityp nicht erlaubt'}), 400 # Datei speichern result = save_job_file(file, user_id=current_user.id) if result['success']: # SystemLog erstellen with get_db_session() as db_session: SystemLog.log_system_event( level="INFO", message=f"Job-Datei hochgeladen: {result['filename']}", module="uploads", user_id=current_user.id ) db_session.commit() uploads_logger.info(f"Job-Datei erfolgreich hochgeladen: {result['filename']} von User {current_user.id}") return jsonify({ 'success': True, 'filename': result['filename'], 'path': result['path'], 'url': result['url'], 'size': result['size'] }) else: return jsonify({'error': result.get('error', 'Upload fehlgeschlagen')}), 500 except Exception as e: uploads_logger.error(f"Fehler beim Job-Upload: {str(e)}") return jsonify({'error': 'Fehler beim Hochladen der Datei'}), 500 @uploads_blueprint.route('/upload/guest', methods=['POST']) def upload_guest_file(): """Lädt eine Gast-Datei hoch (ohne Login)""" try: if 'file' not in request.files: return jsonify({'error': 'Keine Datei hochgeladen'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'Keine Datei ausgewählt'}), 400 if not allowed_file(file.filename): return jsonify({'error': 'Dateityp nicht erlaubt'}), 400 # Gast-ID aus Request holen guest_id = request.form.get('guest_id', 'anonymous') # Datei speichern result = save_guest_file(file, guest_id=guest_id) if result['success']: uploads_logger.info(f"Gast-Datei erfolgreich hochgeladen: {result['filename']} für Gast {guest_id}") return jsonify({ 'success': True, 'filename': result['filename'], 'path': result['path'], 'url': result['url'], 'size': result['size'] }) else: return jsonify({'error': result.get('error', 'Upload fehlgeschlagen')}), 500 except Exception as e: uploads_logger.error(f"Fehler beim Gast-Upload: {str(e)}") return jsonify({'error': 'Fehler beim Hochladen der Datei'}), 500 @uploads_blueprint.route('/upload/avatar', methods=['POST']) @login_required def upload_avatar(): """Lädt ein Avatar-Bild hoch""" try: if 'file' not in request.files: return jsonify({'error': 'Keine Datei hochgeladen'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'Keine Datei ausgewählt'}), 400 # Prüfe Dateityp (nur Bilder) allowed_image_extensions = {'png', 'jpg', 'jpeg', 'gif', 'webp'} if not ('.' in file.filename and file.filename.rsplit('.', 1)[1].lower() in allowed_image_extensions): return jsonify({'error': 'Nur Bilddateien erlaubt'}), 400 # Datei speichern result = save_avatar_file(file, user_id=current_user.id) if result['success']: # Altes Avatar löschen (optional) # TODO: Implementiere Avatar-Verwaltung in User-Model uploads_logger.info(f"Avatar erfolgreich hochgeladen: {result['filename']} für User {current_user.id}") return jsonify({ 'success': True, 'filename': result['filename'], 'path': result['path'], 'url': result['url'], 'size': result['size'] }) else: return jsonify({'error': result.get('error', 'Upload fehlgeschlagen')}), 500 except Exception as e: uploads_logger.error(f"Fehler beim Avatar-Upload: {str(e)}") return jsonify({'error': 'Fehler beim Hochladen des Avatars'}), 500 @uploads_blueprint.route('/upload/asset', methods=['POST']) @login_required @admin_required def upload_asset(): """Lädt ein Asset hoch (nur für Admins)""" try: if 'file' not in request.files: return jsonify({'error': 'Keine Datei hochgeladen'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'Keine Datei ausgewählt'}), 400 # Asset-Typ aus Request asset_type = request.form.get('type', 'general') # Datei speichern result = save_asset_file(file, asset_type=asset_type, user_id=current_user.id) if result['success']: # SystemLog erstellen with get_db_session() as db_session: SystemLog.log_system_event( level="INFO", message=f"Asset hochgeladen: {result['filename']} (Typ: {asset_type})", module="uploads", user_id=current_user.id ) db_session.commit() uploads_logger.info(f"Asset erfolgreich hochgeladen: {result['filename']} (Typ: {asset_type})") return jsonify({ 'success': True, 'filename': result['filename'], 'path': result['path'], 'url': result['url'], 'size': result['size'], 'type': asset_type }) else: return jsonify({'error': result.get('error', 'Upload fehlgeschlagen')}), 500 except Exception as e: uploads_logger.error(f"Fehler beim Asset-Upload: {str(e)}") return jsonify({'error': 'Fehler beim Hochladen des Assets'}), 500 @uploads_blueprint.route('/upload/log', methods=['POST']) @login_required @admin_required def upload_log(): """Lädt eine Log-Datei hoch (nur für Admins)""" try: if 'file' not in request.files: return jsonify({'error': 'Keine Datei hochgeladen'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'Keine Datei ausgewählt'}), 400 # Log-Typ aus Request log_type = request.form.get('type', 'general') # Datei speichern result = save_log_file(file, log_type=log_type, user_id=current_user.id) if result['success']: uploads_logger.info(f"Log-Datei erfolgreich hochgeladen: {result['filename']} (Typ: {log_type})") return jsonify({ 'success': True, 'filename': result['filename'], 'path': result['path'], 'url': result['url'], 'size': result['size'], 'type': log_type }) else: return jsonify({'error': result.get('error', 'Upload fehlgeschlagen')}), 500 except Exception as e: uploads_logger.error(f"Fehler beim Log-Upload: {str(e)}") return jsonify({'error': 'Fehler beim Hochladen der Log-Datei'}), 500 @uploads_blueprint.route('/upload/backup', methods=['POST']) @login_required @admin_required def upload_backup(): """Lädt eine Backup-Datei hoch (nur für Admins)""" try: if 'file' not in request.files: return jsonify({'error': 'Keine Datei hochgeladen'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'Keine Datei ausgewählt'}), 400 # Backup-Typ aus Request backup_type = request.form.get('type', 'database') # Datei speichern result = save_backup_file(file, backup_type=backup_type, user_id=current_user.id) if result['success']: # SystemLog erstellen with get_db_session() as db_session: SystemLog.log_system_event( level="INFO", message=f"Backup hochgeladen: {result['filename']} (Typ: {backup_type})", module="uploads", user_id=current_user.id ) db_session.commit() uploads_logger.info(f"Backup erfolgreich hochgeladen: {result['filename']} (Typ: {backup_type})") return jsonify({ 'success': True, 'filename': result['filename'], 'path': result['path'], 'url': result['url'], 'size': result['size'], 'type': backup_type }) else: return jsonify({'error': result.get('error', 'Upload fehlgeschlagen')}), 500 except Exception as e: uploads_logger.error(f"Fehler beim Backup-Upload: {str(e)}") return jsonify({'error': 'Fehler beim Hochladen des Backups'}), 500 @uploads_blueprint.route('/upload/temp', methods=['POST']) @login_required def upload_temp_file(): """Lädt eine temporäre Datei hoch""" try: if 'file' not in request.files: return jsonify({'error': 'Keine Datei hochgeladen'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'Keine Datei ausgewählt'}), 400 # Datei speichern result = save_temp_file(file, user_id=current_user.id) if result['success']: uploads_logger.info(f"Temporäre Datei erfolgreich hochgeladen: {result['filename']}") return jsonify({ 'success': True, 'filename': result['filename'], 'path': result['path'], 'url': result['url'], 'size': result['size'], 'ttl': 3600 # 1 Stunde TTL für temporäre Dateien }) else: return jsonify({'error': result.get('error', 'Upload fehlgeschlagen')}), 500 except Exception as e: uploads_logger.error(f"Fehler beim Temp-Upload: {str(e)}") return jsonify({'error': 'Fehler beim Hochladen der temporären Datei'}), 500 @uploads_blueprint.route('/files/', methods=['GET']) @login_required def serve_uploaded_file(file_path): """Gibt eine hochgeladene Datei zurück""" try: # Sicherheitsprüfung: Pfad-Traversal verhindern safe_path = os.path.normpath(file_path) if '..' in safe_path or safe_path.startswith('/'): abort(403) # Vollständiger Pfad full_path = os.path.join(UPLOAD_FOLDER, safe_path) # Prüfe ob Datei existiert if not os.path.exists(full_path) or not os.path.isfile(full_path): abort(404) # Prüfe Zugriffsrechte (je nach Dateityp) # TODO: Implementiere detaillierte Zugriffskontrolle # Datei zurückgeben return send_file(full_path, as_attachment=True) except Exception as e: uploads_logger.error(f"Fehler beim Abrufen der Datei {file_path}: {str(e)}") abort(500) @uploads_blueprint.route('/files/', methods=['DELETE']) @login_required def delete_uploaded_file(file_path): """Löscht eine hochgeladene Datei""" try: # Sicherheitsprüfung: Pfad-Traversal verhindern safe_path = os.path.normpath(file_path) if '..' in safe_path or safe_path.startswith('/'): return jsonify({'error': 'Ungültiger Dateipfad'}), 403 # Vollständiger Pfad full_path = os.path.join(UPLOAD_FOLDER, safe_path) # Prüfe ob Datei existiert if not os.path.exists(full_path) or not os.path.isfile(full_path): return jsonify({'error': 'Datei nicht gefunden'}), 404 # Prüfe Zugriffsrechte # TODO: Implementiere detaillierte Zugriffskontrolle # Aktuell: Nur eigene Dateien oder Admin # Datei löschen if delete_file_safe(full_path): # SystemLog erstellen with get_db_session() as db_session: SystemLog.log_system_event( level="INFO", message=f"Datei gelöscht: {safe_path}", module="uploads", user_id=current_user.id ) db_session.commit() uploads_logger.info(f"Datei erfolgreich gelöscht: {safe_path} von User {current_user.id}") return jsonify({ 'success': True, 'message': 'Datei erfolgreich gelöscht' }) else: return jsonify({'error': 'Fehler beim Löschen der Datei'}), 500 except Exception as e: uploads_logger.error(f"Fehler beim Löschen der Datei {file_path}: {str(e)}") return jsonify({'error': 'Fehler beim Löschen der Datei'}), 500 @uploads_blueprint.route('/admin/files/stats', methods=['GET']) @login_required @admin_required def get_file_stats(): """Gibt Statistiken über hochgeladene Dateien zurück""" try: stats = file_manager.get_storage_stats() return jsonify({ 'success': True, 'stats': { 'total_files': stats.get('total_files', 0), 'total_size': stats.get('total_size', 0), 'by_type': stats.get('by_type', {}), 'by_user': stats.get('by_user', {}), 'storage_path': UPLOAD_FOLDER } }) except Exception as e: uploads_logger.error(f"Fehler beim Abrufen der Datei-Statistiken: {str(e)}") return jsonify({'error': 'Fehler beim Abrufen der Statistiken'}), 500 @uploads_blueprint.route('/admin/files/cleanup', methods=['POST']) @login_required @admin_required def cleanup_temp_files(): """Bereinigt temporäre Dateien""" try: # Cleanup-Optionen aus Request data = request.get_json() or {} older_than_hours = data.get('older_than_hours', 24) dry_run = data.get('dry_run', False) # Cleanup durchführen result = file_manager.cleanup_temp_files( older_than_hours=older_than_hours, dry_run=dry_run ) if not dry_run: # SystemLog erstellen with get_db_session() as db_session: SystemLog.log_system_event( level="INFO", message=f"Temporäre Dateien bereinigt: {result['deleted_count']} Dateien, {result['freed_space']} Bytes", module="uploads", user_id=current_user.id ) db_session.commit() uploads_logger.info(f"Cleanup durchgeführt: {result['deleted_count']} Dateien gelöscht") return jsonify({ 'success': True, 'deleted_count': result['deleted_count'], 'freed_space': result['freed_space'], 'dry_run': dry_run }) except Exception as e: uploads_logger.error(f"Fehler beim Cleanup: {str(e)}") return jsonify({'error': 'Fehler beim Bereinigen der temporären Dateien'}), 500