"feat: Implement File Management System documentation and related updates"

This commit is contained in:
2025-05-29 20:15:47 +02:00
parent 4a46e278f2
commit b078eefb4d
5 changed files with 1347 additions and 8 deletions

View File

@@ -38,6 +38,7 @@ from utils.logging_config import setup_logging, get_logger, measure_execution_ti
from utils.job_scheduler import JobScheduler, get_job_scheduler from utils.job_scheduler import JobScheduler, get_job_scheduler
from utils.queue_manager import start_queue_manager, stop_queue_manager, get_queue_manager from utils.queue_manager import start_queue_manager, stop_queue_manager, get_queue_manager
from config.settings import SECRET_KEY, UPLOAD_FOLDER, ALLOWED_EXTENSIONS, ENVIRONMENT, SESSION_LIFETIME, SCHEDULER_ENABLED, SCHEDULER_INTERVAL from config.settings import SECRET_KEY, UPLOAD_FOLDER, ALLOWED_EXTENSIONS, ENVIRONMENT, SESSION_LIFETIME, SCHEDULER_ENABLED, SCHEDULER_INTERVAL
from utils.file_manager import file_manager, save_job_file, save_guest_file, save_avatar_file, delete_file as delete_file_safe
# Blueprints importieren # Blueprints importieren
from blueprints.guest import guest_blueprint from blueprints.guest import guest_blueprint
@@ -6149,3 +6150,287 @@ if __name__ == "__main__":
except: except:
pass pass
sys.exit(1) 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
# Benutzer-Avatar-Pfad in 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}: {file_metadata['original_filename']}")
return jsonify({
'success': True,
'message': 'Avatar erfolgreich hochgeladen',
'avatar_path': relative_path,
'filename': file_metadata['original_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/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
else:
# Andere Dateien: 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:
# Zugriffskontrolle
if file_path.startswith('jobs/'):
if not current_user.is_admin and f"user_{current_user.id}" not in file_path:
return jsonify({'error': 'Zugriff verweigert'}), 403
elif not current_user.is_admin:
return jsonify({'error': 'Zugriff verweigert'}), 403
# Datei löschen
success = delete_file_safe(file_path)
if success:
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': 'Datei konnte nicht gelöscht werden'}), 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: {str(e)}'}), 500
@app.route('/api/admin/files/stats', methods=['GET'])
@admin_required
def get_file_statistics():
"""
Gibt Statistiken über alle hochgeladenen Dateien zurück (nur für Admins)
"""
try:
stats = file_manager.get_category_stats()
# Gesamtstatistiken berechnen
total_files = sum(cat_stats['file_count'] for cat_stats in stats.values())
total_size = sum(cat_stats['total_size'] for cat_stats in stats.values())
total_size_mb = round(total_size / (1024 * 1024), 2)
return jsonify({
'success': True,
'categories': stats,
'totals': {
'file_count': total_files,
'total_size': total_size,
'total_size_mb': total_size_mb
}
})
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'])
@admin_required
def cleanup_temp_files():
"""
Räumt temporäre Dateien auf (nur für Admins)
"""
try:
max_age_hours = request.json.get('max_age_hours', 24)
deleted_count = file_manager.cleanup_temp_files(max_age_hours)
app_logger.info(f"Temporäre Dateien aufgeräumt: {deleted_count} Dateien gelöscht (älter als {max_age_hours}h)")
return jsonify({
'success': True,
'message': f'{deleted_count} temporäre Dateien 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

@@ -0,0 +1 @@

View File

@@ -0,0 +1,662 @@
/**
* MYP Platform Job Manager
* Verwaltung und Steuerung von 3D-Druckaufträgen
* Version: 1.0.0
*/
(function() {
'use strict';
/**
* Job Manager Klasse für 3D-Druckaufträge
*/
class JobManager {
constructor() {
this.jobs = [];
this.currentPage = 1;
this.totalPages = 1;
this.isLoading = false;
this.refreshInterval = null;
this.autoRefreshEnabled = false;
console.log('🔧 JobManager wird initialisiert...');
}
/**
* JobManager initialisieren
*/
async init() {
try {
console.log('🚀 JobManager-Initialisierung gestartet');
// Event-Listener einrichten
this.setupEventListeners();
// Formular-Handler einrichten
this.setupFormHandlers();
// Anfängliche Jobs laden
await this.loadJobs();
// Auto-Refresh starten falls aktiviert
if (this.autoRefreshEnabled) {
this.startAutoRefresh();
}
console.log('✅ JobManager erfolgreich initialisiert');
} catch (error) {
console.error('❌ Fehler bei JobManager-Initialisierung:', error);
this.showToast('Fehler beim Initialisieren des Job-Managers', 'error');
}
}
/**
* Event-Listener für Job-Aktionen einrichten
*/
setupEventListeners() {
console.log('📡 Event-Listener werden eingerichtet...');
// Job-Aktionen über data-Attribute
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-job-action]');
if (!target) return;
const action = target.getAttribute('data-job-action');
const jobId = target.getAttribute('data-job-id');
if (!jobId) {
console.warn('⚠️ Job-ID fehlt für Aktion:', action);
return;
}
switch (action) {
case 'start':
this.startJob(jobId);
break;
case 'pause':
this.pauseJob(jobId);
break;
case 'resume':
this.resumeJob(jobId);
break;
case 'stop':
this.stopJob(jobId);
break;
case 'delete':
this.deleteJob(jobId);
break;
case 'details':
this.openJobDetails(jobId);
break;
default:
console.warn('⚠️ Unbekannte Job-Aktion:', action);
}
});
// Refresh-Button
const refreshBtn = document.getElementById('refresh-jobs');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => this.loadJobs());
}
// Auto-Refresh Toggle
const autoRefreshToggle = document.getElementById('auto-refresh-toggle');
if (autoRefreshToggle) {
autoRefreshToggle.addEventListener('change', (e) => {
this.autoRefreshEnabled = e.target.checked;
if (this.autoRefreshEnabled) {
this.startAutoRefresh();
} else {
this.stopAutoRefresh();
}
});
}
console.log('✅ Event-Listener erfolgreich eingerichtet');
}
/**
* Formular-Handler für Job-Erstellung einrichten
*/
setupFormHandlers() {
console.log('📝 Formular-Handler werden eingerichtet...');
const newJobForm = document.getElementById('new-job-form');
if (newJobForm) {
newJobForm.addEventListener('submit', async (e) => {
e.preventDefault();
await this.createNewJob(new FormData(newJobForm));
});
}
const editJobForm = document.getElementById('edit-job-form');
if (editJobForm) {
editJobForm.addEventListener('submit', async (e) => {
e.preventDefault();
const jobId = editJobForm.getAttribute('data-job-id');
await this.updateJob(jobId, new FormData(editJobForm));
});
}
console.log('✅ Formular-Handler erfolgreich eingerichtet');
}
/**
* Jobs von Server laden
*/
async loadJobs(page = 1) {
if (this.isLoading) {
console.log('⚠️ Jobs werden bereits geladen...');
return;
}
this.isLoading = true;
this.showLoadingState(true);
try {
console.log(`📥 Lade Jobs (Seite ${page})...`);
const response = await fetch(`/api/jobs?page=${page}`, {
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
this.jobs = data.jobs || [];
this.currentPage = data.current_page || 1;
this.totalPages = data.total_pages || 1;
this.renderJobs();
this.updatePagination();
console.log(`${this.jobs.length} Jobs erfolgreich geladen`);
} catch (error) {
console.error('❌ Fehler beim Laden der Jobs:', error);
this.showToast('Fehler beim Laden der Jobs', 'error');
// Fallback: Leere Jobs-Liste anzeigen
this.jobs = [];
this.renderJobs();
} finally {
this.isLoading = false;
this.showLoadingState(false);
}
}
/**
* Job starten
*/
async startJob(jobId) {
try {
console.log(`▶️ Starte Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}/start`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich gestartet', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Starten des Jobs:', error);
this.showToast('Fehler beim Starten des Jobs', 'error');
}
}
/**
* Job pausieren
*/
async pauseJob(jobId) {
try {
console.log(`⏸️ Pausiere Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}/pause`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich pausiert', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Pausieren des Jobs:', error);
this.showToast('Fehler beim Pausieren des Jobs', 'error');
}
}
/**
* Job fortsetzen
*/
async resumeJob(jobId) {
try {
console.log(`▶️ Setze Job ${jobId} fort...`);
const response = await fetch(`/api/jobs/${jobId}/resume`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich fortgesetzt', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Fortsetzen des Jobs:', error);
this.showToast('Fehler beim Fortsetzen des Jobs', 'error');
}
}
/**
* Job stoppen
*/
async stopJob(jobId) {
if (!confirm('Möchten Sie diesen Job wirklich stoppen?')) {
return;
}
try {
console.log(`⏹️ Stoppe Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}/stop`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich gestoppt', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Stoppen des Jobs:', error);
this.showToast('Fehler beim Stoppen des Jobs', 'error');
}
}
/**
* Job löschen
*/
async deleteJob(jobId) {
if (!confirm('Möchten Sie diesen Job wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) {
return;
}
try {
console.log(`🗑️ Lösche Job ${jobId}...`);
const response = await fetch(`/api/jobs/${jobId}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.showToast('Job erfolgreich gelöscht', 'success');
await this.loadJobs();
} catch (error) {
console.error('❌ Fehler beim Löschen des Jobs:', error);
this.showToast('Fehler beim Löschen des Jobs', 'error');
}
}
/**
* Job-Details öffnen
*/
openJobDetails(jobId) {
console.log(`📄 Öffne Details für Job ${jobId}...`);
// Modal für Job-Details öffnen oder zu Detail-Seite navigieren
const detailsUrl = `/jobs/${jobId}`;
// Prüfen ob Modal verfügbar ist
const detailsModal = document.getElementById(`job-details-${jobId}`);
if (detailsModal && typeof window.MYP !== 'undefined' && window.MYP.UI && window.MYP.UI.modal) {
window.MYP.UI.modal.open(`job-details-${jobId}`);
} else {
// Fallback: Zur Detail-Seite navigieren
window.location.href = detailsUrl;
}
}
/**
* Jobs in der UI rendern
*/
renderJobs() {
const jobsList = document.getElementById('jobs-list');
if (!jobsList) {
console.warn('⚠️ Jobs-Liste Element nicht gefunden');
return;
}
if (this.jobs.length === 0) {
jobsList.innerHTML = `
<div class="text-center py-12">
<div class="text-gray-400 dark:text-gray-600 text-6xl mb-4">📭</div>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Keine Jobs vorhanden</h3>
<p class="text-gray-500 dark:text-gray-400">Es wurden noch keine Druckaufträge erstellt.</p>
</div>
`;
return;
}
const jobsHTML = this.jobs.map(job => this.renderJobCard(job)).join('');
jobsList.innerHTML = jobsHTML;
console.log(`📋 ${this.jobs.length} Jobs gerendert`);
}
/**
* Einzelne Job-Karte rendern
*/
renderJobCard(job) {
const statusClass = this.getJobStatusClass(job.status);
const statusText = this.getJobStatusText(job.status);
return `
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div class="flex items-start justify-between">
<div class="flex-1">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">${job.name || 'Unbenannter Job'}</h3>
<div class="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400 mb-4">
<span>ID: ${job.id}</span>
<span>•</span>
<span>Drucker: ${job.printer_name || 'Unbekannt'}</span>
<span>•</span>
<span>Erstellt: ${new Date(job.created_at).toLocaleDateString('de-DE')}</span>
</div>
<div class="flex items-center space-x-2 mb-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusClass}">
${statusText}
</span>
${job.progress ? `<span class="text-sm text-gray-500 dark:text-gray-400">${job.progress}%</span>` : ''}
</div>
</div>
<div class="flex flex-col space-y-2 ml-4">
${this.renderJobActions(job)}
</div>
</div>
</div>
`;
}
/**
* Job-Aktionen rendern
*/
renderJobActions(job) {
const actions = [];
// Details-Button immer verfügbar
actions.push(`
<button data-job-action="details" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Details
</button>
`);
// Status-abhängige Aktionen
switch (job.status) {
case 'pending':
case 'ready':
actions.push(`
<button data-job-action="start" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
Starten
</button>
`);
break;
case 'running':
case 'printing':
actions.push(`
<button data-job-action="pause" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500">
Pausieren
</button>
`);
actions.push(`
<button data-job-action="stop" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Stoppen
</button>
`);
break;
case 'paused':
actions.push(`
<button data-job-action="resume" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
Fortsetzen
</button>
`);
actions.push(`
<button data-job-action="stop" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Stoppen
</button>
`);
break;
case 'completed':
case 'failed':
case 'cancelled':
actions.push(`
<button data-job-action="delete" data-job-id="${job.id}"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Löschen
</button>
`);
break;
}
return actions.join('');
}
/**
* CSS-Klasse für Job-Status
*/
getJobStatusClass(status) {
const statusClasses = {
'pending': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
'ready': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
'running': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
'printing': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
'paused': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
'completed': 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-300',
'failed': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
'cancelled': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
};
return statusClasses[status] || statusClasses['pending'];
}
/**
* Anzeigetext für Job-Status
*/
getJobStatusText(status) {
const statusTexts = {
'pending': 'Wartend',
'ready': 'Bereit',
'running': 'Läuft',
'printing': 'Druckt',
'paused': 'Pausiert',
'completed': 'Abgeschlossen',
'failed': 'Fehlgeschlagen',
'cancelled': 'Abgebrochen'
};
return statusTexts[status] || 'Unbekannt';
}
/**
* Loading-Zustand anzeigen/verstecken
*/
showLoadingState(show) {
const loadingEl = document.getElementById('jobs-loading');
const jobsList = document.getElementById('jobs-list');
if (loadingEl) {
loadingEl.style.display = show ? 'block' : 'none';
}
if (jobsList) {
jobsList.style.opacity = show ? '0.5' : '1';
jobsList.style.pointerEvents = show ? 'none' : 'auto';
}
}
/**
* CSRF-Token abrufen
*/
getCSRFToken() {
const token = document.querySelector('meta[name="csrf-token"]');
return token ? token.getAttribute('content') : '';
}
/**
* Toast-Nachricht anzeigen
*/
showToast(message, type = 'info') {
if (typeof window.showToast === 'function') {
window.showToast(message, type);
} else {
console.log(`${type.toUpperCase()}: ${message}`);
}
}
/**
* Auto-Refresh starten
*/
startAutoRefresh() {
this.stopAutoRefresh(); // Vorherigen Refresh stoppen
this.refreshInterval = setInterval(() => {
if (!this.isLoading) {
this.loadJobs(this.currentPage);
}
}, 30000); // Alle 30 Sekunden
console.log('🔄 Auto-Refresh gestartet (30s Intervall)');
}
/**
* Auto-Refresh stoppen
*/
stopAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
console.log('⏹️ Auto-Refresh gestoppt');
}
}
/**
* Paginierung aktualisieren
*/
updatePagination() {
const paginationEl = document.getElementById('jobs-pagination');
if (!paginationEl) return;
if (this.totalPages <= 1) {
paginationEl.style.display = 'none';
return;
}
paginationEl.style.display = 'flex';
let paginationHTML = '';
// Vorherige Seite
if (this.currentPage > 1) {
paginationHTML += `
<button onclick="jobManager.loadJobs(${this.currentPage - 1})"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">
Zurück
</button>
`;
}
// Seitenzahlen
for (let i = 1; i <= this.totalPages; i++) {
const isActive = i === this.currentPage;
paginationHTML += `
<button onclick="jobManager.loadJobs(${i})"
class="px-3 py-2 text-sm font-medium ${isActive
? 'text-blue-600 bg-blue-50 border-blue-300 dark:bg-blue-900 dark:text-blue-300'
: 'text-gray-500 bg-white border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700'
} border">
${i}
</button>
`;
}
// Nächste Seite
if (this.currentPage < this.totalPages) {
paginationHTML += `
<button onclick="jobManager.loadJobs(${this.currentPage + 1})"
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700">
Weiter
</button>
`;
}
paginationEl.innerHTML = paginationHTML;
}
}
// JobManager global verfügbar machen
window.JobManager = JobManager;
// JobManager-Instanz erstellen wenn DOM bereit ist
document.addEventListener('DOMContentLoaded', function() {
if (typeof window.jobManager === 'undefined') {
window.jobManager = new JobManager();
// Nur initialisieren wenn wir uns auf einer Jobs-Seite befinden
if (document.getElementById('jobs-list') || document.querySelector('[data-job-action]')) {
window.jobManager.init();
}
}
});
console.log('✅ JobManager-Modul geladen');
})();

View File

@@ -577,6 +577,7 @@
<!-- JavaScript --> <!-- JavaScript -->
<script src="{{ url_for('static', filename='js/ui-components.js') }}"></script> <script src="{{ url_for('static', filename='js/ui-components.js') }}"></script>
<script src="{{ url_for('static', filename='js/job-manager.js') }}"></script>
<script src="{{ url_for('static', filename='js/dark-mode-fix.js') }}"></script> <script src="{{ url_for('static', filename='js/dark-mode-fix.js') }}"></script>
<script src="{{ url_for('static', filename='js/optimization-features.js') }}"></script> <script src="{{ url_for('static', filename='js/optimization-features.js') }}"></script>
<script src="{{ url_for('static', filename='js/global-refresh-functions.js') }}"></script> <script src="{{ url_for('static', filename='js/global-refresh-functions.js') }}"></script>
@@ -627,13 +628,6 @@
* Initialisierung aller UI-Komponenten nach DOM-Load * Initialisierung aller UI-Komponenten nach DOM-Load
*/ */
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Dark Mode Manager initialisieren
if (typeof MYP !== 'undefined' && MYP.UI) {
window.darkModeManager = new MYP.UI.DarkModeManager();
window.mobileMenuManager = new MYP.UI.MobileMenuManager();
window.userDropdownManager = new MYP.UI.UserDropdownManager();
}
// MYP App für Offline-Funktionalität initialisieren // MYP App für Offline-Funktionalität initialisieren
if (typeof MYPApp !== 'undefined') { if (typeof MYPApp !== 'undefined') {
window.mypApp = new MYPApp(); window.mypApp = new MYPApp();

View File

@@ -1 +1,398 @@
"""
Mercedes-Benz MYP - Datei-Management-System
Organisierte Speicherung von hochgeladenen Dateien mit Verzeichniskonventionen
"""
import os
import shutil
from datetime import datetime
from werkzeug.utils import secure_filename
from typing import Optional, Tuple, Dict, List
from config.settings import UPLOAD_FOLDER, ALLOWED_EXTENSIONS
class FileManager:
"""
Zentrales Datei-Management-System für die MYP-Platform
Organisiert Uploads in strukturierte Unterverzeichnisse
"""
# Verzeichniskonventionen
DIRECTORIES = {
'jobs': 'jobs', # Druckjob-Dateien
'guests': 'guests', # Gastauftrags-Dateien
'avatars': 'avatars', # Benutzer-Avatare
'temp': 'temp', # Temporäre Dateien
'backups': 'backups', # Backup-Dateien
'logs': 'logs', # Exportierte Logs
'assets': 'assets' # Statische Assets
}
def __init__(self, base_upload_folder: str = UPLOAD_FOLDER):
"""
Initialisiert den FileManager
Args:
base_upload_folder: Basis-Upload-Verzeichnis
"""
self.base_folder = base_upload_folder
self.ensure_directories()
def ensure_directories(self) -> None:
"""Erstellt alle erforderlichen Verzeichnisse"""
try:
# Basis-Upload-Ordner erstellen
os.makedirs(self.base_folder, exist_ok=True)
# Alle Unterverzeichnisse erstellen
for category, subdir in self.DIRECTORIES.items():
dir_path = os.path.join(self.base_folder, subdir)
os.makedirs(dir_path, exist_ok=True)
# Jahres-/Monatsverzeichnisse für organisierte Speicherung
current_date = datetime.now()
year_dir = os.path.join(dir_path, str(current_date.year))
month_dir = os.path.join(year_dir, f"{current_date.month:02d}")
os.makedirs(year_dir, exist_ok=True)
os.makedirs(month_dir, exist_ok=True)
except Exception as e:
print(f"Fehler beim Erstellen der Verzeichnisse: {e}")
def allowed_file(self, filename: str) -> bool:
"""
Prüft, ob eine Datei erlaubt ist
Args:
filename: Name der Datei
Returns:
bool: True wenn erlaubt
"""
if '.' not in filename:
return False
extension = filename.rsplit('.', 1)[1].lower()
return extension in ALLOWED_EXTENSIONS
def generate_unique_filename(self, original_filename: str, prefix: str = "") -> str:
"""
Generiert einen eindeutigen Dateinamen
Args:
original_filename: Ursprünglicher Dateiname
prefix: Optionaler Präfix
Returns:
str: Eindeutiger Dateiname
"""
# Dateiname sicher machen
secure_name = secure_filename(original_filename)
# Timestamp hinzufügen für Eindeutigkeit
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# Dateiname und Erweiterung trennen
if '.' in secure_name:
name, ext = secure_name.rsplit('.', 1)
if prefix:
unique_name = f"{prefix}_{name}_{timestamp}.{ext}"
else:
unique_name = f"{name}_{timestamp}.{ext}"
else:
if prefix:
unique_name = f"{prefix}_{secure_name}_{timestamp}"
else:
unique_name = f"{secure_name}_{timestamp}"
return unique_name
def save_file(self, file, category: str, user_id: int = None,
prefix: str = "", metadata: Dict = None) -> Optional[Tuple[str, str, Dict]]:
"""
Speichert eine Datei in der organisierten Struktur
Args:
file: Werkzeug FileStorage Objekt
category: Kategorie (jobs, guests, avatars, etc.)
user_id: Benutzer-ID für Pfad-Organisation
prefix: Dateiname-Präfix
metadata: Zusätzliche Metadaten
Returns:
Tuple[str, str, Dict]: (relativer_pfad, absoluter_pfad, metadaten) oder None bei Fehler
"""
try:
if not file or not file.filename:
return None
if not self.allowed_file(file.filename):
raise ValueError(f"Dateityp nicht erlaubt: {file.filename}")
if category not in self.DIRECTORIES:
raise ValueError(f"Unbekannte Kategorie: {category}")
# Verzeichnisstruktur aufbauen
current_date = datetime.now()
category_dir = self.DIRECTORIES[category]
year_dir = str(current_date.year)
month_dir = f"{current_date.month:02d}"
# Benutzer-spezifischen Unterordner hinzufügen wenn user_id vorhanden
if user_id:
relative_dir = os.path.join(category_dir, year_dir, month_dir, f"user_{user_id}")
else:
relative_dir = os.path.join(category_dir, year_dir, month_dir)
# Vollständigen Pfad erstellen
full_dir = os.path.join(self.base_folder, relative_dir)
os.makedirs(full_dir, exist_ok=True)
# Eindeutigen Dateinamen generieren
unique_filename = self.generate_unique_filename(file.filename, prefix)
# Pfade definieren
relative_path = os.path.join(relative_dir, unique_filename).replace('\\', '/')
absolute_path = os.path.join(full_dir, unique_filename)
# Datei speichern
file.save(absolute_path)
# Metadaten sammeln
file_metadata = {
'original_filename': file.filename,
'unique_filename': unique_filename,
'relative_path': relative_path,
'absolute_path': absolute_path,
'category': category,
'user_id': user_id,
'file_size': os.path.getsize(absolute_path),
'upload_timestamp': current_date.isoformat(),
'mime_type': file.content_type or 'application/octet-stream'
}
# Zusätzliche Metadaten hinzufügen
if metadata:
file_metadata.update(metadata)
return relative_path, absolute_path, file_metadata
except Exception as e:
print(f"Fehler beim Speichern der Datei: {e}")
return None
def delete_file(self, relative_path: str) -> bool:
"""
Löscht eine Datei
Args:
relative_path: Relativer Pfad zur Datei
Returns:
bool: True wenn erfolgreich gelöscht
"""
try:
if not relative_path:
return False
absolute_path = os.path.join(self.base_folder, relative_path)
if os.path.exists(absolute_path) and os.path.isfile(absolute_path):
os.remove(absolute_path)
return True
return False
except Exception as e:
print(f"Fehler beim Löschen der Datei {relative_path}: {e}")
return False
def move_file(self, old_relative_path: str, new_category: str,
new_prefix: str = "") -> Optional[str]:
"""
Verschiebt eine Datei in eine andere Kategorie
Args:
old_relative_path: Alter relativer Pfad
new_category: Neue Kategorie
new_prefix: Neuer Präfix
Returns:
str: Neuer relativer Pfad oder None bei Fehler
"""
try:
old_absolute_path = os.path.join(self.base_folder, old_relative_path)
if not os.path.exists(old_absolute_path):
return None
# Dateiname extrahieren
filename = os.path.basename(old_absolute_path)
# Neuen Pfad generieren
current_date = datetime.now()
new_category_dir = self.DIRECTORIES.get(new_category)
if not new_category_dir:
return None
year_dir = str(current_date.year)
month_dir = f"{current_date.month:02d}"
new_relative_dir = os.path.join(new_category_dir, year_dir, month_dir)
new_full_dir = os.path.join(self.base_folder, new_relative_dir)
os.makedirs(new_full_dir, exist_ok=True)
# Neuen Dateinamen generieren falls Präfix angegeben
if new_prefix:
new_filename = self.generate_unique_filename(filename, new_prefix)
else:
new_filename = filename
new_relative_path = os.path.join(new_relative_dir, new_filename).replace('\\', '/')
new_absolute_path = os.path.join(new_full_dir, new_filename)
# Datei verschieben
shutil.move(old_absolute_path, new_absolute_path)
return new_relative_path
except Exception as e:
print(f"Fehler beim Verschieben der Datei: {e}")
return None
def get_file_info(self, relative_path: str) -> Optional[Dict]:
"""
Gibt Informationen über eine Datei zurück
Args:
relative_path: Relativer Pfad zur Datei
Returns:
Dict: Datei-Informationen oder None
"""
try:
if not relative_path:
return None
absolute_path = os.path.join(self.base_folder, relative_path)
if not os.path.exists(absolute_path):
return None
stat = os.stat(absolute_path)
return {
'filename': os.path.basename(absolute_path),
'relative_path': relative_path,
'absolute_path': absolute_path,
'size': stat.st_size,
'created': datetime.fromtimestamp(stat.st_ctime).isoformat(),
'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
'exists': True
}
except Exception as e:
print(f"Fehler beim Abrufen der Datei-Informationen: {e}")
return None
def cleanup_temp_files(self, max_age_hours: int = 24) -> int:
"""
Räumt temporäre Dateien auf
Args:
max_age_hours: Maximales Alter in Stunden
Returns:
int: Anzahl gelöschte Dateien
"""
try:
temp_dir = os.path.join(self.base_folder, self.DIRECTORIES['temp'])
if not os.path.exists(temp_dir):
return 0
deleted_count = 0
max_age_seconds = max_age_hours * 3600
current_time = datetime.now().timestamp()
for root, dirs, files in os.walk(temp_dir):
for file in files:
file_path = os.path.join(root, file)
try:
file_age = current_time - os.path.getmtime(file_path)
if file_age > max_age_seconds:
os.remove(file_path)
deleted_count += 1
except Exception:
continue
return deleted_count
except Exception as e:
print(f"Fehler beim Aufräumen temporärer Dateien: {e}")
return 0
def get_category_stats(self) -> Dict[str, Dict]:
"""
Gibt Statistiken für alle Kategorien zurück
Returns:
Dict: Statistiken pro Kategorie
"""
stats = {}
try:
for category, subdir in self.DIRECTORIES.items():
category_path = os.path.join(self.base_folder, subdir)
if not os.path.exists(category_path):
stats[category] = {'file_count': 0, 'total_size': 0}
continue
file_count = 0
total_size = 0
for root, dirs, files in os.walk(category_path):
for file in files:
file_path = os.path.join(root, file)
try:
total_size += os.path.getsize(file_path)
file_count += 1
except Exception:
continue
stats[category] = {
'file_count': file_count,
'total_size': total_size,
'total_size_mb': round(total_size / (1024 * 1024), 2)
}
return stats
except Exception as e:
print(f"Fehler beim Abrufen der Kategorie-Statistiken: {e}")
return {}
# Globale FileManager-Instanz
file_manager = FileManager()
# Convenience-Funktionen
def save_job_file(file, user_id: int, metadata: Dict = None) -> Optional[Tuple[str, str, Dict]]:
"""Speichert eine Druckjob-Datei"""
return file_manager.save_file(file, 'jobs', user_id, 'job', metadata)
def save_guest_file(file, metadata: Dict = None) -> Optional[Tuple[str, str, Dict]]:
"""Speichert eine Gastauftrags-Datei"""
return file_manager.save_file(file, 'guests', None, 'guest', metadata)
def save_avatar_file(file, user_id: int) -> Optional[Tuple[str, str, Dict]]:
"""Speichert eine Avatar-Datei"""
return file_manager.save_file(file, 'avatars', user_id, 'avatar')
def delete_file(relative_path: str) -> bool:
"""Löscht eine Datei"""
return file_manager.delete_file(relative_path)
def get_file_info(relative_path: str) -> Optional[Dict]:
"""Gibt Datei-Informationen zurück"""
return file_manager.get_file_info(relative_path)