Dashboard Add top-right user dropdown using Syncfusion DropDownButton: shows username + role; menu entries “Profil” and “Abmelden”. Replace custom dropdown logic with Syncfusion component; position at header’s right edge. Update /logout page to call backend logout and redirect to /login (reliable user switching). Build/Config Add @syncfusion/ej2-react-splitbuttons and @syncfusion/ej2-splitbuttons dependencies. Update Vite optimizeDeps.include to pre-bundle splitbuttons and avoid import-analysis errors. Docs README: Rework Architecture Overview with clearer data flow: Listener consumes MQTT (discovery/heartbeats) and updates API. Scheduler reads from API and publishes events via MQTT to clients. Clients send via MQTT and receive via MQTT. Worker receives commands directly from API and reports results back (no MQTT). Explicit note: MariaDB is accessed exclusively by the API Server; Dashboard never talks to DB directly. README: Add SplitButtons to “Syncfusion Components Used”; add troubleshooting steps for @syncfusion/ej2-react-splitbuttons import issues (optimizeDeps + volume reset). Copilot instructions: Document header user menu and splitbuttons technical notes (deps, optimizeDeps, dev-container node_modules volume). Program info Bump to 2025.1.0-alpha.10 with changelog: UI: Header user menu (DropDownButton with username/role; Profil/Abmelden). Frontend: Syncfusion SplitButtons integration + Vite pre-bundling config. Fix: Added README guidance for splitbuttons import errors. No breaking changes.
266 lines
9.5 KiB
Python
266 lines
9.5 KiB
Python
from re import A
|
|
from flask import Blueprint, request, jsonify, send_from_directory
|
|
from server.permissions import editor_or_higher
|
|
from server.database import Session
|
|
from models.models import EventMedia, MediaType, Conversion, ConversionStatus
|
|
from server.task_queue import get_queue
|
|
from server.worker import convert_event_media_to_pdf
|
|
import hashlib
|
|
import os
|
|
|
|
eventmedia_bp = Blueprint('eventmedia', __name__, url_prefix='/api/eventmedia')
|
|
|
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
|
|
|
|
|
def get_param(key, default=None):
|
|
# Reihenfolge: form > json > args
|
|
if request.form and key in request.form:
|
|
return request.form.get(key, default)
|
|
if request.is_json and request.json and key in request.json:
|
|
return request.json.get(key, default)
|
|
return request.args.get(key, default)
|
|
|
|
# --- FileManager: List, Create Folder, Rename, Delete, Move ---
|
|
|
|
|
|
@eventmedia_bp.route('/filemanager/operations', methods=['GET', 'POST'])
|
|
@editor_or_higher
|
|
def filemanager_operations():
|
|
action = get_param('action')
|
|
path = get_param('path', '/')
|
|
name = get_param('name')
|
|
new_name = get_param('newName')
|
|
target_path = get_param('targetPath')
|
|
|
|
full_path = os.path.join(MEDIA_ROOT, path.lstrip('/'))
|
|
|
|
print(action, path, name, new_name, target_path, full_path) # Debug-Ausgabe
|
|
|
|
if action == 'read':
|
|
# List files and folders
|
|
items = []
|
|
session = Session()
|
|
for entry in os.scandir(full_path):
|
|
item = {
|
|
'name': entry.name,
|
|
'isFile': entry.is_file(),
|
|
'size': entry.stat().st_size,
|
|
'type': os.path.splitext(entry.name)[1][1:] if entry.is_file() else '',
|
|
'hasChild': entry.is_dir()
|
|
}
|
|
# Wenn Datei, versuche Upload-Datum aus DB zu holen
|
|
if entry.is_file():
|
|
media = session.query(EventMedia).filter_by(
|
|
url=entry.name).first()
|
|
if media and media.uploaded_at:
|
|
# FileManager erwartet UNIX-Timestamp (Sekunden)
|
|
item['dateModified'] = int(media.uploaded_at.timestamp())
|
|
else:
|
|
item['dateModified'] = entry.stat().st_mtime
|
|
else:
|
|
item['dateModified'] = entry.stat().st_mtime
|
|
items.append(item)
|
|
session.close()
|
|
return jsonify({'files': items, 'cwd': {'name': os.path.basename(full_path), 'path': path}})
|
|
|
|
elif action == 'details':
|
|
# Details für eine oder mehrere Dateien zurückgeben
|
|
names = request.form.getlist('names[]') or (request.json.get(
|
|
'names') if request.is_json and request.json else [])
|
|
path = get_param('path', '/')
|
|
details = []
|
|
session = Session()
|
|
for name in names:
|
|
file_path = os.path.join(MEDIA_ROOT, path.lstrip('/'), name)
|
|
media = session.query(EventMedia).filter_by(url=name).first()
|
|
if os.path.isfile(file_path):
|
|
detail = {
|
|
'name': name,
|
|
'size': os.path.getsize(file_path),
|
|
'dateModified': int(media.uploaded_at.timestamp()) if media and media.uploaded_at else int(os.path.getmtime(file_path)),
|
|
'type': os.path.splitext(name)[1][1:],
|
|
'hasChild': False,
|
|
'isFile': True,
|
|
'description': media.message_content if media else '',
|
|
# weitere Felder nach Bedarf
|
|
}
|
|
details.append(detail)
|
|
session.close()
|
|
return jsonify({'details': details})
|
|
elif action == 'delete':
|
|
for item in request.form.getlist('names[]'):
|
|
item_path = os.path.join(full_path, item)
|
|
if os.path.isdir(item_path):
|
|
os.rmdir(item_path)
|
|
else:
|
|
os.remove(item_path)
|
|
return jsonify({'success': True})
|
|
elif action == 'rename':
|
|
src = os.path.join(full_path, name)
|
|
dst = os.path.join(full_path, new_name)
|
|
os.rename(src, dst)
|
|
return jsonify({'success': True})
|
|
elif action == 'move':
|
|
src = os.path.join(full_path, name)
|
|
dst = os.path.join(MEDIA_ROOT, target_path.lstrip('/'), name)
|
|
os.rename(src, dst)
|
|
return jsonify({'success': True})
|
|
elif action == 'create':
|
|
os.makedirs(os.path.join(full_path, name), exist_ok=True)
|
|
return jsonify({'success': True})
|
|
else:
|
|
return jsonify({'error': 'Unknown action'}), 400
|
|
|
|
# --- FileManager: Upload ---
|
|
|
|
|
|
@eventmedia_bp.route('/filemanager/upload', methods=['POST'])
|
|
@editor_or_higher
|
|
def filemanager_upload():
|
|
session = Session()
|
|
# Korrigiert: Erst aus request.form, dann aus request.args lesen
|
|
path = request.form.get('path') or request.args.get('path', '/')
|
|
upload_path = os.path.join(MEDIA_ROOT, path.lstrip('/'))
|
|
os.makedirs(upload_path, exist_ok=True)
|
|
for file in request.files.getlist('uploadFiles'):
|
|
file_path = os.path.join(upload_path, file.filename)
|
|
file.save(file_path)
|
|
ext = os.path.splitext(file.filename)[1][1:].lower()
|
|
try:
|
|
media_type = MediaType(ext)
|
|
except ValueError:
|
|
media_type = MediaType.other
|
|
from datetime import datetime, timezone
|
|
media = EventMedia(
|
|
media_type=media_type,
|
|
url=file.filename,
|
|
file_path=os.path.relpath(file_path, MEDIA_ROOT),
|
|
uploaded_at=datetime.now(timezone.utc)
|
|
)
|
|
session.add(media)
|
|
session.commit()
|
|
|
|
# Enqueue conversion for office presentation types
|
|
if media_type in {MediaType.ppt, MediaType.pptx, MediaType.odp}:
|
|
# compute file hash
|
|
h = hashlib.sha256()
|
|
with open(file_path, 'rb') as f:
|
|
for chunk in iter(lambda: f.read(8192), b""):
|
|
h.update(chunk)
|
|
file_hash = h.hexdigest()
|
|
|
|
# upsert Conversion row
|
|
conv = (
|
|
session.query(Conversion)
|
|
.filter_by(
|
|
source_event_media_id=media.id,
|
|
target_format='pdf',
|
|
file_hash=file_hash,
|
|
)
|
|
.one_or_none()
|
|
)
|
|
if not conv:
|
|
conv = Conversion(
|
|
source_event_media_id=media.id,
|
|
target_format='pdf',
|
|
status=ConversionStatus.pending,
|
|
file_hash=file_hash,
|
|
)
|
|
session.add(conv)
|
|
session.commit()
|
|
|
|
if conv.status in {ConversionStatus.pending, ConversionStatus.failed}:
|
|
q = get_queue()
|
|
q.enqueue(convert_event_media_to_pdf, conv.id)
|
|
|
|
session.commit()
|
|
return jsonify({'success': True})
|
|
|
|
# --- FileManager: Download ---
|
|
|
|
|
|
@eventmedia_bp.route('/filemanager/download', methods=['GET'])
|
|
def filemanager_download():
|
|
path = request.args.get('path', '/')
|
|
names = request.args.getlist('names[]')
|
|
# Nur Einzel-Download für Beispiel
|
|
if names:
|
|
file_path = os.path.join(MEDIA_ROOT, path.lstrip('/'), names[0])
|
|
return send_from_directory(os.path.dirname(file_path), os.path.basename(file_path), as_attachment=True)
|
|
return jsonify({'error': 'No file specified'}), 400
|
|
|
|
# --- FileManager: Get Image (optional, für Thumbnails) ---
|
|
|
|
|
|
@eventmedia_bp.route('/filemanager/get-image', methods=['GET'])
|
|
def filemanager_get_image():
|
|
path = request.args.get('path', '/')
|
|
file_path = os.path.join(MEDIA_ROOT, path.lstrip('/'))
|
|
return send_from_directory(os.path.dirname(file_path), os.path.basename(file_path))
|
|
|
|
# --- EventMedia-API: Metadaten-Liste (wie gehabt) ---
|
|
|
|
|
|
@eventmedia_bp.route('', methods=['GET'])
|
|
def list_media():
|
|
session = Session()
|
|
media = session.query(EventMedia).all()
|
|
return jsonify([m.to_dict() for m in media])
|
|
|
|
# --- EventMedia-API: Metadaten-Update ---
|
|
|
|
|
|
@eventmedia_bp.route('/<int:media_id>', methods=['PUT'])
|
|
@editor_or_higher
|
|
def update_media(media_id):
|
|
session = Session()
|
|
media = session.query(EventMedia).get(media_id)
|
|
if not media:
|
|
return jsonify({'error': 'Not found'}), 404
|
|
data = request.json
|
|
media.url = data.get('title', media.url)
|
|
media.message_content = data.get('description', media.message_content)
|
|
# Event-Zuordnung ggf. ergänzen
|
|
session.commit()
|
|
return jsonify(media.to_dict())
|
|
|
|
|
|
@eventmedia_bp.route('/find_by_filename', methods=['GET'])
|
|
def find_by_filename():
|
|
filename = request.args.get('filename')
|
|
if not filename:
|
|
return jsonify({'error': 'Missing filename'}), 400
|
|
session = Session()
|
|
# Suche nach exaktem Dateinamen in url oder file_path
|
|
media = session.query(EventMedia).filter(
|
|
(EventMedia.url == filename) | (
|
|
EventMedia.file_path.like(f"%{filename}"))
|
|
).first()
|
|
if not media:
|
|
return jsonify({'error': 'Not found'}), 404
|
|
return jsonify({
|
|
'id': media.id,
|
|
'file_path': media.file_path,
|
|
'url': media.url
|
|
})
|
|
|
|
|
|
@eventmedia_bp.route('/<int:media_id>', methods=['GET'])
|
|
def get_media_by_id(media_id):
|
|
session = Session()
|
|
media = session.query(EventMedia).get(media_id)
|
|
if not media:
|
|
session.close()
|
|
return jsonify({'error': 'Not found'}), 404
|
|
result = {
|
|
'id': media.id,
|
|
'file_path': media.file_path,
|
|
'url': media.url,
|
|
'name': media.url, # oder ein anderes Feld für den Namen
|
|
'media_type': media.media_type.name if media.media_type else None
|
|
}
|
|
session.close()
|
|
return jsonify(result)
|