From 482e04723df79eb1cac8198f232a3427e9fe1fee Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Thu, 29 May 2025 22:35:00 +0200 Subject: [PATCH] "feat: Integrate printer monitor for enhanced logging in backend" --- backend/app/app.py | 534 ++++++++++++++++++++++++++- backend/app/utils/printer_monitor.py | 30 +- 2 files changed, 556 insertions(+), 8 deletions(-) diff --git a/backend/app/app.py b/backend/app/app.py index a3b68561..a98d9d3d 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -4344,4 +4344,536 @@ if __name__ == "__main__": stop_queue_manager() except: pass - sys.exit(1) \ No newline at end of file + sys.exit(1) + +# ===== FILE-UPLOAD-ROUTEN ===== + +@app.route('/api/upload/job', methods=['POST']) +@login_required +def upload_job_file(): + """ + Lädt eine Datei für einen Druckjob hoch + + Form Data: + file: Die hochzuladende Datei + job_name: Name des Jobs (optional) + """ + try: + if 'file' not in request.files: + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + file = request.files['file'] + job_name = request.form.get('job_name', '') + + if file.filename == '': + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + # Metadaten für die Datei + metadata = { + 'uploader_id': current_user.id, + 'uploader_name': current_user.username, + 'job_name': job_name + } + + # Datei speichern + result = save_job_file(file, current_user.id, metadata) + + if result: + relative_path, absolute_path, file_metadata = result + + app_logger.info(f"Job-Datei hochgeladen: {file_metadata['original_filename']} von User {current_user.id}") + + return jsonify({ + 'success': True, + 'message': 'Datei erfolgreich hochgeladen', + 'file_path': relative_path, + 'filename': file_metadata['original_filename'], + 'unique_filename': file_metadata['unique_filename'], + 'file_size': file_metadata['file_size'], + 'metadata': file_metadata + }) + else: + return jsonify({'error': 'Fehler beim Speichern der Datei'}), 500 + + except Exception as e: + app_logger.error(f"Fehler beim Hochladen der Job-Datei: {str(e)}") + return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500 + +@app.route('/api/upload/guest', methods=['POST']) +def upload_guest_file(): + """ + Lädt eine Datei für einen Gastauftrag hoch + + Form Data: + file: Die hochzuladende Datei + guest_name: Name des Gasts (optional) + guest_email: E-Mail des Gasts (optional) + """ + try: + if 'file' not in request.files: + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + file = request.files['file'] + guest_name = request.form.get('guest_name', '') + guest_email = request.form.get('guest_email', '') + + if file.filename == '': + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + # Metadaten für die Datei + metadata = { + 'guest_name': guest_name, + 'guest_email': guest_email + } + + # Datei speichern + result = save_guest_file(file, metadata) + + if result: + relative_path, absolute_path, file_metadata = result + + app_logger.info(f"Gast-Datei hochgeladen: {file_metadata['original_filename']} für {guest_name or 'Unbekannt'}") + + return jsonify({ + 'success': True, + 'message': 'Datei erfolgreich hochgeladen', + 'file_path': relative_path, + 'filename': file_metadata['original_filename'], + 'unique_filename': file_metadata['unique_filename'], + 'file_size': file_metadata['file_size'], + 'metadata': file_metadata + }) + else: + return jsonify({'error': 'Fehler beim Speichern der Datei'}), 500 + + except Exception as e: + app_logger.error(f"Fehler beim Hochladen der Gast-Datei: {str(e)}") + return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500 + +@app.route('/api/upload/avatar', methods=['POST']) +@login_required +def upload_avatar(): + """ + Lädt ein Avatar-Bild für den aktuellen Benutzer hoch + + Form Data: + file: Das Avatar-Bild + """ + try: + if 'file' not in request.files: + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + file = request.files['file'] + + if file.filename == '': + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + # Nur Bilder erlauben + allowed_extensions = {'png', 'jpg', 'jpeg', 'gif', 'webp'} + if not file.filename or '.' not in file.filename: + return jsonify({'error': 'Ungültiger Dateityp'}), 400 + + file_ext = file.filename.rsplit('.', 1)[1].lower() + if file_ext not in allowed_extensions: + return jsonify({'error': 'Nur Bilddateien sind erlaubt (PNG, JPG, JPEG, GIF, WebP)'}), 400 + + # Alte Avatar-Datei löschen falls vorhanden + db_session = get_db_session() + user = db_session.query(User).get(current_user.id) + if user and user.avatar_path: + delete_file_safe(user.avatar_path) + + # Neue Avatar-Datei speichern + result = save_avatar_file(file, current_user.id) + + if result: + relative_path, absolute_path, file_metadata = result + + # Avatar-Pfad in der Datenbank aktualisieren + user.avatar_path = relative_path + db_session.commit() + db_session.close() + + app_logger.info(f"Avatar hochgeladen für User {current_user.id}") + + return jsonify({ + 'success': True, + 'message': 'Avatar erfolgreich hochgeladen', + 'file_path': relative_path, + 'filename': file_metadata['original_filename'], + 'unique_filename': file_metadata['unique_filename'], + 'file_size': file_metadata['file_size'] + }) + else: + db_session.close() + return jsonify({'error': 'Fehler beim Speichern des Avatars'}), 500 + + except Exception as e: + app_logger.error(f"Fehler beim Hochladen des Avatars: {str(e)}") + return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500 + +@app.route('/api/upload/asset', methods=['POST']) +@login_required +@admin_required +def upload_asset(): + """ + Lädt ein statisches Asset hoch (nur für Administratoren) + + Form Data: + file: Die Asset-Datei + asset_name: Name des Assets (optional) + """ + try: + if 'file' not in request.files: + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + file = request.files['file'] + asset_name = request.form.get('asset_name', '') + + if file.filename == '': + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + # Metadaten für die Datei + metadata = { + 'uploader_id': current_user.id, + 'uploader_name': current_user.username, + 'asset_name': asset_name + } + + # Datei speichern + result = file_manager.save_file(file, 'assets', current_user.id, 'asset', metadata) + + if result: + relative_path, absolute_path, file_metadata = result + + app_logger.info(f"Asset hochgeladen: {file_metadata['original_filename']} von Admin {current_user.id}") + + return jsonify({ + 'success': True, + 'message': 'Asset erfolgreich hochgeladen', + 'file_path': relative_path, + 'filename': file_metadata['original_filename'], + 'unique_filename': file_metadata['unique_filename'], + 'file_size': file_metadata['file_size'], + 'metadata': file_metadata + }) + else: + return jsonify({'error': 'Fehler beim Speichern des Assets'}), 500 + + except Exception as e: + app_logger.error(f"Fehler beim Hochladen des Assets: {str(e)}") + return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500 + +@app.route('/api/upload/log', methods=['POST']) +@login_required +@admin_required +def upload_log(): + """ + Lädt eine Log-Datei hoch (nur für Administratoren) + + Form Data: + file: Die Log-Datei + log_type: Typ des Logs (optional) + """ + try: + if 'file' not in request.files: + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + file = request.files['file'] + log_type = request.form.get('log_type', 'allgemein') + + if file.filename == '': + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + # Metadaten für die Datei + metadata = { + 'uploader_id': current_user.id, + 'uploader_name': current_user.username, + 'log_type': log_type + } + + # Datei speichern + result = file_manager.save_file(file, 'logs', current_user.id, 'log', metadata) + + if result: + relative_path, absolute_path, file_metadata = result + + app_logger.info(f"Log-Datei hochgeladen: {file_metadata['original_filename']} von Admin {current_user.id}") + + return jsonify({ + 'success': True, + 'message': 'Log-Datei erfolgreich hochgeladen', + 'file_path': relative_path, + 'filename': file_metadata['original_filename'], + 'unique_filename': file_metadata['unique_filename'], + 'file_size': file_metadata['file_size'], + 'metadata': file_metadata + }) + else: + return jsonify({'error': 'Fehler beim Speichern der Log-Datei'}), 500 + + except Exception as e: + app_logger.error(f"Fehler beim Hochladen der Log-Datei: {str(e)}") + return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500 + +@app.route('/api/upload/backup', methods=['POST']) +@login_required +@admin_required +def upload_backup(): + """ + Lädt eine Backup-Datei hoch (nur für Administratoren) + + Form Data: + file: Die Backup-Datei + backup_type: Typ des Backups (optional) + """ + try: + if 'file' not in request.files: + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + file = request.files['file'] + backup_type = request.form.get('backup_type', 'allgemein') + + if file.filename == '': + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + # Metadaten für die Datei + metadata = { + 'uploader_id': current_user.id, + 'uploader_name': current_user.username, + 'backup_type': backup_type + } + + # Datei speichern + result = file_manager.save_file(file, 'backups', current_user.id, 'backup', metadata) + + if result: + relative_path, absolute_path, file_metadata = result + + app_logger.info(f"Backup-Datei hochgeladen: {file_metadata['original_filename']} von Admin {current_user.id}") + + return jsonify({ + 'success': True, + 'message': 'Backup-Datei erfolgreich hochgeladen', + 'file_path': relative_path, + 'filename': file_metadata['original_filename'], + 'unique_filename': file_metadata['unique_filename'], + 'file_size': file_metadata['file_size'], + 'metadata': file_metadata + }) + else: + return jsonify({'error': 'Fehler beim Speichern der Backup-Datei'}), 500 + + except Exception as e: + app_logger.error(f"Fehler beim Hochladen der Backup-Datei: {str(e)}") + return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500 + +@app.route('/api/upload/temp', methods=['POST']) +@login_required +def upload_temp_file(): + """ + Lädt eine temporäre Datei hoch + + Form Data: + file: Die temporäre Datei + purpose: Verwendungszweck (optional) + """ + try: + if 'file' not in request.files: + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + file = request.files['file'] + purpose = request.form.get('purpose', '') + + if file.filename == '': + return jsonify({'error': 'Keine Datei ausgewählt'}), 400 + + # Metadaten für die Datei + metadata = { + 'uploader_id': current_user.id, + 'uploader_name': current_user.username, + 'purpose': purpose + } + + # Datei speichern + result = file_manager.save_file(file, 'temp', current_user.id, 'temp', metadata) + + if result: + relative_path, absolute_path, file_metadata = result + + app_logger.info(f"Temporäre Datei hochgeladen: {file_metadata['original_filename']} von User {current_user.id}") + + return jsonify({ + 'success': True, + 'message': 'Temporäre Datei erfolgreich hochgeladen', + 'file_path': relative_path, + 'filename': file_metadata['original_filename'], + 'unique_filename': file_metadata['unique_filename'], + 'file_size': file_metadata['file_size'], + 'metadata': file_metadata + }) + else: + return jsonify({'error': 'Fehler beim Speichern der temporären Datei'}), 500 + + except Exception as e: + app_logger.error(f"Fehler beim Hochladen der temporären Datei: {str(e)}") + return jsonify({'error': f'Fehler beim Hochladen: {str(e)}'}), 500 + +@app.route('/api/files/', methods=['GET']) +@login_required +def serve_uploaded_file(file_path): + """ + Stellt hochgeladene Dateien bereit (mit Zugriffskontrolle) + """ + try: + # Datei-Info abrufen + file_info = file_manager.get_file_info(file_path) + + if not file_info: + return jsonify({'error': 'Datei nicht gefunden'}), 404 + + # Zugriffskontrolle basierend auf Dateikategorie + if file_path.startswith('jobs/'): + # Job-Dateien: Nur Besitzer und Admins + if not current_user.is_admin: + # Prüfen ob Benutzer der Besitzer ist + if f"user_{current_user.id}" not in file_path: + return jsonify({'error': 'Zugriff verweigert'}), 403 + + elif file_path.startswith('guests/'): + # Gast-Dateien: Nur Admins + if not current_user.is_admin: + return jsonify({'error': 'Zugriff verweigert'}), 403 + + elif file_path.startswith('avatars/'): + # Avatar-Dateien: Öffentlich zugänglich für angemeldete Benutzer + pass + + elif file_path.startswith('temp/'): + # Temporäre Dateien: Nur Besitzer und Admins + if not current_user.is_admin: + # Prüfen ob Benutzer der Besitzer ist + if f"user_{current_user.id}" not in file_path: + return jsonify({'error': 'Zugriff verweigert'}), 403 + + else: + # Andere Dateien (assets, logs, backups): Nur Admins + if not current_user.is_admin: + return jsonify({'error': 'Zugriff verweigert'}), 403 + + # Datei bereitstellen + return send_file(file_info['absolute_path'], as_attachment=False) + + except Exception as e: + app_logger.error(f"Fehler beim Bereitstellen der Datei {file_path}: {str(e)}") + return jsonify({'error': 'Fehler beim Laden der Datei'}), 500 + +@app.route('/api/files/', methods=['DELETE']) +@login_required +def delete_uploaded_file(file_path): + """ + Löscht eine hochgeladene Datei (mit Zugriffskontrolle) + """ + try: + # Datei-Info abrufen + file_info = file_manager.get_file_info(file_path) + + if not file_info: + return jsonify({'error': 'Datei nicht gefunden'}), 404 + + # Zugriffskontrolle basierend auf Dateikategorie + if file_path.startswith('jobs/'): + # Job-Dateien: Nur Besitzer und Admins + if not current_user.is_admin: + # Prüfen ob Benutzer der Besitzer ist + if f"user_{current_user.id}" not in file_path: + return jsonify({'error': 'Zugriff verweigert'}), 403 + + elif file_path.startswith('guests/'): + # Gast-Dateien: Nur Admins + if not current_user.is_admin: + return jsonify({'error': 'Zugriff verweigert'}), 403 + + elif file_path.startswith('avatars/'): + # Avatar-Dateien: Nur Besitzer und Admins + if not current_user.is_admin: + # Prüfen ob Benutzer der Besitzer ist + if f"user_{current_user.id}" not in file_path: + return jsonify({'error': 'Zugriff verweigert'}), 403 + + elif file_path.startswith('temp/'): + # Temporäre Dateien: Nur Besitzer und Admins + if not current_user.is_admin: + # Prüfen ob Benutzer der Besitzer ist + if f"user_{current_user.id}" not in file_path: + return jsonify({'error': 'Zugriff verweigert'}), 403 + + else: + # Andere Dateien (assets, logs, backups): Nur Admins + if not current_user.is_admin: + return jsonify({'error': 'Zugriff verweigert'}), 403 + + # Datei löschen + if delete_file_safe(file_path): + app_logger.info(f"Datei gelöscht: {file_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: + app_logger.error(f"Fehler beim Löschen der Datei {file_path}: {str(e)}") + return jsonify({'error': f'Fehler beim Löschen der Datei: {str(e)}'}), 500 + +@app.route('/api/admin/files/stats', methods=['GET']) +@login_required +@admin_required +def get_file_stats(): + """ + Gibt Statistiken zu allen Dateien zurück (nur für Administratoren) + """ + try: + stats = file_manager.get_category_stats() + + # Gesamtstatistiken berechnen + total_files = sum(category.get('file_count', 0) for category in stats.values()) + total_size = sum(category.get('total_size', 0) for category in stats.values()) + + return jsonify({ + 'success': True, + 'categories': stats, + 'totals': { + 'file_count': total_files, + 'total_size': total_size, + 'total_size_mb': round(total_size / (1024 * 1024), 2) + } + }) + + except Exception as e: + app_logger.error(f"Fehler beim Abrufen der Datei-Statistiken: {str(e)}") + return jsonify({'error': f'Fehler beim Abrufen der Statistiken: {str(e)}'}), 500 + +@app.route('/api/admin/files/cleanup', methods=['POST']) +@login_required +@admin_required +def cleanup_temp_files(): + """ + Räumt temporäre Dateien auf (nur für Administratoren) + """ + try: + data = request.get_json() or {} + max_age_hours = data.get('max_age_hours', 24) + + # Temporäre Dateien aufräumen + deleted_count = file_manager.cleanup_temp_files(max_age_hours) + + app_logger.info(f"Temporäre Dateien aufgeräumt: {deleted_count} Dateien gelöscht") + + return jsonify({ + 'success': True, + 'message': f'{deleted_count} temporäre Dateien erfolgreich gelöscht', + 'deleted_count': deleted_count + }) + + except Exception as e: + app_logger.error(f"Fehler beim Aufräumen temporärer Dateien: {str(e)}") + return jsonify({'error': f'Fehler beim Aufräumen: {str(e)}'}), 500 \ No newline at end of file diff --git a/backend/app/utils/printer_monitor.py b/backend/app/utils/printer_monitor.py index 690f6593..0a7b68f3 100644 --- a/backend/app/utils/printer_monitor.py +++ b/backend/app/utils/printer_monitor.py @@ -138,8 +138,15 @@ class PrinterMonitor: monitor_logger.error("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Steckdose nicht schalten") return False + # Fallback zu globalen Anmeldedaten wenn keine lokalen vorhanden + if not username or not password: + username = TAPO_USERNAME + password = TAPO_PASSWORD + monitor_logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip_address}") + try: # TP-Link Tapo P100 Verbindung herstellen (P100 statt P110 verwenden) + from PyP100 import PyP100 p100 = PyP100.P100(ip_address, username, password) p100.handshake() # Authentifizierung p100.login() # Login @@ -342,7 +349,7 @@ class PrinterMonitor: def _ping_address(self, ip_address: str, timeout: int = 3) -> bool: """ Führt einen Konnektivitätstest zu einer IP-Adresse durch. - Verwendet TCP-Verbindung statt Ping, um Encoding-Probleme zu vermeiden. + Verwendet ausschließlich TCP-Verbindung statt Ping, um Encoding-Probleme zu vermeiden. Args: ip_address: Zu testende IP-Adresse @@ -355,10 +362,18 @@ class PrinterMonitor: # IP-Adresse validieren ipaddress.ip_address(ip_address.strip()) - # TCP-Verbindung zu Port 80 (HTTP) oder 443 (HTTPS) versuchen import socket - # Erst Port 80 versuchen (HTTP) + # Erst Port 9999 versuchen (Tapo-Standard) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + result = sock.connect_ex((ip_address.strip(), 9999)) + sock.close() + + if result == 0: + return True + + # Falls Port 9999 nicht erfolgreich, Port 80 versuchen (HTTP) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) result = sock.connect_ex((ip_address.strip(), 80)) @@ -481,15 +496,16 @@ class PrinterMonitor: for ip in DEFAULT_TAPO_IPS: try: - # Ping-Test für Grundkonnektivität - ping_success = self._ping_address(ip, timeout=2) + # TCP-Verbindungstest für Grundkonnektivität + ping_success = self._ping_address(ip, timeout=3) if ping_success: - monitor_logger.info(f"✅ Steckdose mit IP {ip} ist pingbar") + monitor_logger.info(f"✅ Steckdose mit IP {ip} ist erreichbar") # Tapo-Verbindung testen if TAPO_AVAILABLE: try: + from PyP100 import PyP100 p100 = PyP100.P100(ip, TAPO_USERNAME, TAPO_PASSWORD) p100.handshake() p100.login() @@ -506,7 +522,7 @@ class PrinterMonitor: self._ensure_tapo_in_database(ip, nickname) except Exception as e: - monitor_logger.debug(f"❌ IP {ip} ist pingbar, aber keine Tapo-Steckdose: {str(e)}") + monitor_logger.debug(f"❌ IP {ip} ist erreichbar, aber keine Tapo-Steckdose: {str(e)}") results[ip] = False else: monitor_logger.warning("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Verbindung nicht testen")