"feat: Integrate printer monitor for enhanced logging in backend"

This commit is contained in:
Till Tomczak 2025-05-29 22:35:00 +02:00
parent 81c2b2ab88
commit 482e04723d
2 changed files with 556 additions and 8 deletions

View File

@ -4345,3 +4345,535 @@ if __name__ == "__main__":
except:
pass
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/<path:file_path>', 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/<path:file_path>', 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

View File

@ -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")