DB/model Add Conversion model + ConversionStatus enum (pending, processing, ready, failed) Alembic migrations: create conversions table, indexes, unique (source_event_media_id, target_format, file_hash), and NOT NULL on file_hash API Enqueue on upload (ppt|pptx|odp) in routes/eventmedia.py: compute sha256, upsert Conversion, enqueue job New routes: POST /api/conversions/<media_id>/pdf — ensure/enqueue conversion GET /api/conversions/<media_id>/status — latest status/details GET /api/files/converted/<path> — serve converted PDFs Register conversions blueprint in wsgi Worker server/worker.py: convert_event_media_to_pdf Calls Gotenberg /forms/libreoffice/convert, writes to server/media/converted/ Updates Conversion status, timestamps, error messages Fix media root resolution to /server/media Prefer function enqueue over string path; expose server.worker in package init for RQ string compatibility Queue/infra server/task_queue.py: RQ queue helper (REDIS_URL, default redis://redis:6379/0) docker-compose: Add redis and gotenberg services Add worker service (rq worker conversions) Pass REDIS_URL and GOTENBERG_URL to server/worker Mount shared media volume in prod for API/worker parity docker-compose.override: Add dev redis/gotenberg/worker services Ensure PYTHONPATH + working_dir allow importing server.worker Use rq CLI instead of python -m rq for worker Dashboard dev: run as appropriate user/root and pre-create/chown caches to avoid EACCES Dashboard dev UX Vite: set cacheDir .vite to avoid EACCES in node_modules Disable Node inspector by default to avoid port conflicts Docs Update copilot-instructions.md with conversion system: flow, services, env vars, endpoints, storage paths, and data model
262 lines
9.4 KiB
Python
262 lines
9.4 KiB
Python
from re import A
|
|
from flask import Blueprint, request, jsonify, send_from_directory
|
|
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'])
|
|
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'])
|
|
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'])
|
|
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)
|