"feat: Implement File Management System documentation and related updates"
This commit is contained in:
@@ -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.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 utils.file_manager import file_manager, save_job_file, save_guest_file, save_avatar_file, delete_file as delete_file_safe
|
||||
|
||||
# Blueprints importieren
|
||||
from blueprints.guest import guest_blueprint
|
||||
@@ -6149,3 +6150,287 @@ 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
|
||||
|
||||
# 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
|
||||
|
1
backend/app/docs/FILE_MANAGEMENT_SYSTEM.md
Normal file
1
backend/app/docs/FILE_MANAGEMENT_SYSTEM.md
Normal file
@@ -0,0 +1 @@
|
||||
|
662
backend/app/static/js/job-manager.js
Normal file
662
backend/app/static/js/job-manager.js
Normal 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');
|
||||
})();
|
@@ -577,6 +577,7 @@
|
||||
|
||||
<!-- JavaScript -->
|
||||
<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/optimization-features.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
|
||||
*/
|
||||
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
|
||||
if (typeof MYPApp !== 'undefined') {
|
||||
window.mypApp = new MYPApp();
|
||||
|
@@ -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)
|
Reference in New Issue
Block a user