diff --git a/Web/app.py b/Web/app.py index 9f66cae..fda4f2a 100755 --- a/Web/app.py +++ b/Web/app.py @@ -26,6 +26,7 @@ from jinja2 import TemplateNotFound import os import sys from bs4 import BeautifulSoup +from gridfs import GridFS # Ensure imports work regardless of whether gunicorn starts in /app or /app/Web. _CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -395,6 +396,12 @@ SENSITIVE_AUDIT_FIELDS = ["email", "username", "full_name", "phone", "borrower", APP_VERSION = __version__ RELEASE_STATE_FILE = os.path.join(os.path.dirname(BASE_DIR), '.release-version') +def get_gridfs(): + """Return a GridFS instance connected to the current tenant's database context.""" + client = cfg.MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) + db = client[cfg.MONGODB_DB] # Intercepted by your multi-tenant proxy automatically + return GridFS(db) + """ -----------------------------------------------------------------------Before Request Handlers---------------------------------------------------------------------------- """ def _get_csrf_token(): @@ -797,7 +804,7 @@ def _get_school_info_for_export(): def _save_school_logo_upload(upload_file, tenant_id=None, tenant_db=None): - """Save an uploaded school logo to the shared upload folder with a tenant-specific filename.""" + """Save an uploaded school logo directly into MongoDB GridFS.""" if not upload_file or not getattr(upload_file, 'filename', ''): return None @@ -813,33 +820,44 @@ def _save_school_logo_upload(upload_file, tenant_id=None, tenant_db=None): raise ValueError('Das Logo benötigt eine Dateiendung.') logo_filename = f'school-logo-{safe_tenant}{extension}' - logo_path = os.path.join(app.config['UPLOAD_FOLDER'], logo_filename) + thumb_filename = f'school-logo-{safe_tenant}-thumb.png' - os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) - upload_file.save(logo_path) + fs = get_gridfs() - # Generate a thumbnail for consistent navbar sizing + # Read file data into memory + upload_file.seek(0) + file_bytes = upload_file.read() + + # Save main logo to GridFS + fs.put( + io.BytesIO(file_bytes), + filename=logo_filename, + content_type=upload_file.content_type or 'image/png' + ) + + # Generate thumbnail in-memory and save to GridFS try: - thumb_filename = f'school-logo-{safe_tenant}-thumb.png' - thumb_path = os.path.join(app.config['UPLOAD_FOLDER'], thumb_filename) - - with Image.open(logo_path) as img: - # Ensure RGBA for transparency and convert if needed + with Image.open(io.BytesIO(file_bytes)) as img: if img.mode not in ('RGBA', 'RGB'): img = img.convert('RGBA') - # Target max size: width up to 520px, height up to 114px (matches CSS constraints) max_thumb_size = (520, 114) img.thumbnail(max_thumb_size, Image.LANCZOS) - # Save as PNG for predictable rendering in web UI - img.save(thumb_path, format='PNG', optimize=True) + thumb_io = io.BytesIO() + img.save(thumb_io, format='PNG', optimize=True) + thumb_io.seek(0) + + fs.put( + thumb_io, + filename=thumb_filename, + content_type='image/png' + ) except Exception as e: - app.logger.warning(f"Thumbnail generation failed for {logo_path}: {e}") + app.logger.warning(f"Thumbnail generation failed for logo: {e}") thumb_filename = None return (logo_filename, thumb_filename) - """---------------------------------------------Invoice Generation----------------------------------------------------------------------------- """ def _parse_money_value(value): @@ -2534,14 +2552,15 @@ def upload_student_cards_excel(): """-------------------------------------------------------------File Serving-----------------------------------------------------------------------------""" + @app.route('/uploads/') def uploaded_file(filename): """ - Serve uploaded files from the uploads directory. - + Serve uploaded files from MongoDB GridFS with local path fallbacks. + Args: filename (str): Name of the file to serve - + Returns: flask.Response: The requested file or placeholder image if not found """ @@ -2550,24 +2569,37 @@ def uploaded_file(filename): if denied: return denied - # Check production path first (deployed environment) + # 1. Try serving from MongoDB GridFS first + try: + fs = get_gridfs() + grid_out = fs.find_one({'filename': filename}) + if grid_out: + return send_file( + io.BytesIO(grid_out.read()), + mimetype=grid_out.content_type or 'application/octet-stream', + download_name=filename + ) + except Exception as grid_err: + app.logger.warning(f"GridFS lookup failed for {filename}: {grid_err}") + + # 2. Fallback to production path for legacy local files prod_path = "/opt/Inventarsystem/Web/uploads" dev_path = app.config['UPLOAD_FOLDER'] if os.path.exists(os.path.join(prod_path, filename)): return send_from_directory(prod_path, filename) - # Then check development path + # 3. Fallback to development path if os.path.exists(os.path.join(dev_path, filename)): return send_from_directory(dev_path, filename) - - # Use a placeholder image if file not found - first try SVG, then PNG + + # 4. Use a placeholder image if file not found - first try SVG, then PNG svg_placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.svg') png_placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.png') - + if os.path.exists(svg_placeholder_path): return send_from_directory(app.static_folder, 'img/no-image.svg') elif os.path.exists(png_placeholder_path): return send_from_directory(app.static_folder, 'img/no-image.png') - + # Default placeholder from static folder return send_from_directory(app.static_folder, 'favicon.ico') except Exception as e: @@ -10811,7 +10843,7 @@ def admin_school_settings(): flash('Bitte melden Sie sich mit einem administrativen Konto an.', 'error') return redirect(url_for('login')) current_permissions = us.get_effective_permissions(session['username']) - + if not current_permissions['actions'].get('can_manage_settings', False): flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') return redirect(url_for('home_admin')) @@ -10855,21 +10887,22 @@ def admin_school_settings(): school_info['logo_path'] = saved_logo_filename school_info['logo_thumb'] = saved_thumb_filename or '' - # remove previous files if different + fs = get_gridfs() + # Remove previous logo files from GridFS if different if previous_logo_path and previous_logo_path != saved_logo_filename: - previous_logo_file = os.path.join(app.config['UPLOAD_FOLDER'], previous_logo_path) - if os.path.exists(previous_logo_file): - try: - os.remove(previous_logo_file) - except Exception: - pass + try: + old_logo_file = fs.find_one({'filename': previous_logo_path}) + if old_logo_file: + fs.delete(old_logo_file._id) + except Exception: + pass if previous_thumb_path and previous_thumb_path != saved_thumb_filename: - previous_thumb_file = os.path.join(app.config['UPLOAD_FOLDER'], previous_thumb_path) - if os.path.exists(previous_thumb_file): - try: - os.remove(previous_thumb_file) - except Exception: - pass + try: + old_thumb_file = fs.find_one({'filename': previous_thumb_path}) + if old_thumb_file: + fs.delete(old_thumb_file._id) + except Exception: + pass except Exception as exc: flash(f'Logo konnte nicht hochgeladen werden: {exc}', 'error') return redirect(url_for('admin_school_settings')) @@ -10895,7 +10928,7 @@ def admin_school_settings(): try: updated_school = cfg.update_school_info(school_info) current_school = updated_school - + # Also process filter names filter_name_1 = request.form.get('filter_name_1') if filter_name_1: it.set_filter_name(1, filter_name_1.strip())