From 89d1748100b94daee534a1a81b4ebf8e85a93b32 Mon Sep 17 00:00:00 2001 From: olaf Date: Wed, 17 Sep 2025 06:36:37 +0000 Subject: [PATCH] add nginx.dev.conf for development environment add functionality of scheduler to send right event data to the clients added route for file download --- models/models.py | 8 +++-- nginx.dev.conf | 47 +++++++++++++++++++++++++++ scheduler/db_utils.py | 74 +++++++++++++++++++++++++++++++++++------- scheduler/scheduler.py | 13 +++----- server/routes/files.py | 57 ++++++++++++++++++++++++++++++++ server/wsgi.py | 2 ++ 6 files changed, 178 insertions(+), 23 deletions(-) create mode 100644 nginx.dev.conf create mode 100644 server/routes/files.py diff --git a/models/models.py b/models/models.py index 053307b..55011ae 100644 --- a/models/models.py +++ b/models/models.py @@ -1,7 +1,7 @@ from sqlalchemy import ( Column, Integer, String, Enum, TIMESTAMP, func, Boolean, ForeignKey, Float, Text, Index, DateTime ) -from sqlalchemy.orm import declarative_base +from sqlalchemy.orm import declarative_base, relationship import enum from datetime import datetime, timezone @@ -122,6 +122,9 @@ class Event(Base): updated_by = Column(Integer, ForeignKey('users.id'), nullable=True) is_active = Column(Boolean, default=True, nullable=False) + # Add relationship to EventMedia + event_media = relationship("EventMedia", foreign_keys=[event_media_id]) + class EventMedia(Base): __tablename__ = 'event_media' @@ -130,7 +133,8 @@ class EventMedia(Base): url = Column(String(255), nullable=False) file_path = Column(String(255), nullable=True) message_content = Column(Text, nullable=True) - uploaded_at = Column(TIMESTAMP, nullable=False, default=lambda: datetime.now(timezone.utc)) + uploaded_at = Column(TIMESTAMP, nullable=False, + default=lambda: datetime.now(timezone.utc)) def to_dict(self): return { diff --git a/nginx.dev.conf b/nginx.dev.conf new file mode 100644 index 0000000..34a74c4 --- /dev/null +++ b/nginx.dev.conf @@ -0,0 +1,47 @@ +events {} +http { + upstream dashboard { + # Vite dev server inside the dashboard container + server infoscreen-dashboard:5173; + } + upstream infoscreen_api { + server infoscreen-api:8000; + } + + server { + listen 80; + server_name _; + + # Proxy /api and /screenshots to the Flask API + location /api/ { + proxy_pass http://infoscreen_api/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /screenshots/ { + proxy_pass http://infoscreen_api/screenshots/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Everything else to the Vite dev server + location / { + proxy_pass http://dashboard; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # WebSocket upgrade for Vite HMR + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + } +} diff --git a/scheduler/db_utils.py b/scheduler/db_utils.py index 283d55b..8d2f3d5 100644 --- a/scheduler/db_utils.py +++ b/scheduler/db_utils.py @@ -2,11 +2,9 @@ from dotenv import load_dotenv import os from datetime import datetime -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import sessionmaker, joinedload from sqlalchemy import create_engine -from models.models import Event -# import sys -# sys.path.append('/workspace/server') +from models.models import Event, EventMedia load_dotenv('/workspace/.env') @@ -15,14 +13,66 @@ DB_CONN = os.environ.get("DB_CONN", "mysql+pymysql://user:password@db/dbname") engine = create_engine(DB_CONN) Session = sessionmaker(bind=engine) +# Base URL from .env for file URLs +API_BASE_URL = os.environ.get("API_BASE_URL", "http://server:8000") + def get_active_events(start: datetime, end: datetime, group_id: int = None): session = Session() - query = session.query(Event).filter(Event.is_active == True) - if start and end: - query = query.filter(Event.start < end, Event.end > start) - if group_id: - query = query.filter(Event.group_id == group_id) - events = query.all() - session.close() - return events + try: + # Now this will work with the relationship defined + query = session.query(Event).options( + joinedload(Event.event_media) + ).filter(Event.is_active == True) + + if start and end: + query = query.filter(Event.start < end, Event.end > start) + if group_id: + query = query.filter(Event.group_id == group_id) + + events = query.all() + + formatted_events = [] + for event in events: + formatted_event = format_event_with_media(event) + formatted_events.append(formatted_event) + + return formatted_events + finally: + session.close() + + +def format_event_with_media(event): + """Transform Event + EventMedia into client-expected format""" + event_dict = { + "id": event.id, + "title": event.title, + "start": str(event.start), + "end": str(event.end), + "group_id": event.group_id, + } + + # Now you can directly access event.event_media + if event.event_media: + media = event.event_media + + if event.event_type.value == "presentation": + event_dict["presentation"] = { + "type": "slideshow", + "files": [], + "slide_interval": event.slideshow_interval or 5000, + "auto_advance": True + } + + if media.file_path: + filename = os.path.basename(media.file_path) + event_dict["presentation"]["files"].append({ + "name": filename, + "url": f"{API_BASE_URL}/api/files/{media.id}/{filename}", + "checksum": None, + "size": None + }) + + # Add other event types... + + return event_dict diff --git a/scheduler/scheduler.py b/scheduler/scheduler.py index 302bb11..2c83317 100644 --- a/scheduler/scheduler.py +++ b/scheduler/scheduler.py @@ -55,22 +55,17 @@ def main(): while True: now = datetime.datetime.now(datetime.timezone.utc) - # Hole alle aktiven Events (Vergleich mit UTC) + # Hole alle aktiven Events (bereits formatierte Dictionaries) events = get_active_events(now, now) # Gruppiere Events nach group_id groups = {} for event in events: - gid = getattr(event, "group_id", None) + gid = event.get("group_id") if gid not in groups: groups[gid] = [] - groups[gid].append({ - "id": event.id, - "title": getattr(event, "title", ""), - "start": str(getattr(event, "start", "")), - "end": str(getattr(event, "end", "")), - "group_id": gid, - }) + # Event ist bereits ein Dictionary im gewünschten Format + groups[gid].append(event) # Sende pro Gruppe die Eventliste als retained Message, nur bei Änderung for gid, event_list in groups.items(): diff --git a/server/routes/files.py b/server/routes/files.py new file mode 100644 index 0000000..675adac --- /dev/null +++ b/server/routes/files.py @@ -0,0 +1,57 @@ +from flask import Blueprint, jsonify, send_from_directory +from server.database import Session +from models.models import EventMedia +import os + +# Blueprint for direct file downloads by media ID +files_bp = Blueprint("files", __name__, url_prefix="/api/files") + +# Reuse the same media root convention as eventmedia.py +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MEDIA_ROOT = os.path.join(BASE_DIR, "media") + + +@files_bp.route("//", methods=["GET"]) +def download_media_file(media_id: int, filename: str): + """ + Download the stored media file for a given EventMedia ID. + + URL format example: + /api/files/26/LPUV4I_Folien_Nowitzki_Bewertungskriterien.pptx + + Behavior: + - Looks up EventMedia by ID + - Validates requested filename against stored metadata (best-effort) + - Serves the file from server/media using the stored relative file_path + """ + session = Session() + media = session.query(EventMedia).get(media_id) + if not media: + session.close() + return jsonify({"error": "Not found"}), 404 + + # Prefer the stored relative file_path; fall back to the URL/filename + rel_path = media.file_path or media.url + + # Basic filename consistency check to avoid leaking other files + # Only enforce if media.url is present + if media.url and os.path.basename(filename) != os.path.basename(media.url): + session.close() + return jsonify({ + "error": "Filename mismatch", + "expected": os.path.basename(media.url), + "got": os.path.basename(filename), + }), 400 + + abs_path = os.path.join(MEDIA_ROOT, rel_path) + + # Ensure file exists + if not os.path.isfile(abs_path): + session.close() + return jsonify({"error": "File not found on server"}), 404 + + # Serve as attachment (download) + directory = os.path.dirname(abs_path) + served_name = os.path.basename(abs_path) + session.close() + return send_from_directory(directory, served_name, as_attachment=True) diff --git a/server/wsgi.py b/server/wsgi.py index 7a018e8..4fc7fc5 100644 --- a/server/wsgi.py +++ b/server/wsgi.py @@ -1,5 +1,6 @@ # server/wsgi.py from server.routes.eventmedia import eventmedia_bp +from server.routes.files import files_bp from server.routes.events import events_bp from server.routes.groups import groups_bp from server.routes.clients import clients_bp @@ -18,6 +19,7 @@ app.register_blueprint(clients_bp) app.register_blueprint(groups_bp) app.register_blueprint(events_bp) app.register_blueprint(eventmedia_bp) +app.register_blueprint(files_bp) @app.route("/health")