Compare commits

...

2 Commits

2 changed files with 77 additions and 44 deletions
+72 -39
View File
@@ -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/<filename>')
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())
+5 -5
View File
@@ -319,15 +319,15 @@ THUMBNAIL_FOLDER = _get(_conf, ['upload', 'thumbnail_folder'], DEFAULTS['upload'
PREVIEW_FOLDER = _get(_conf, ['upload', 'preview_folder'], DEFAULTS['upload']['preview_folder'])
QR_CODE_FOLDER = _get(_conf, ['upload', 'qrcode_folder'], DEFAULTS['upload']['qrcode_folder'])
# Normalize to absolute paths to avoid cwd issues
# This guarantees exact matching with Docker volume mounts
if not os.path.isabs(UPLOAD_FOLDER):
UPLOAD_FOLDER = os.path.join(BASE_DIR, os.path.relpath(UPLOAD_FOLDER, BASE_DIR))
UPLOAD_FOLDER = os.path.abspath(os.path.join(BASE_DIR, "uploads"))
if not os.path.isabs(THUMBNAIL_FOLDER):
THUMBNAIL_FOLDER = os.path.join(BASE_DIR, os.path.relpath(THUMBNAIL_FOLDER, BASE_DIR))
THUMBNAIL_FOLDER = os.path.abspath(os.path.join(BASE_DIR, "thumbnails"))
if not os.path.isabs(PREVIEW_FOLDER):
PREVIEW_FOLDER = os.path.join(BASE_DIR, os.path.relpath(PREVIEW_FOLDER, BASE_DIR))
PREVIEW_FOLDER = os.path.abspath(os.path.join(BASE_DIR, "previews"))
if not os.path.isabs(QR_CODE_FOLDER):
QR_CODE_FOLDER = os.path.join(BASE_DIR, os.path.relpath(QR_CODE_FOLDER, BASE_DIR))
QR_CODE_FOLDER = os.path.abspath(os.path.join(BASE_DIR, "QRCodes"))
MAX_UPLOAD_MB = _get(_conf, ['upload', 'max_size_mb'], DEFAULTS['upload']['max_size_mb'])
IMAGE_MAX_UPLOAD_MB = _get(_conf, ['upload', 'image_max_size_mb'], DEFAULTS['upload']['image_max_size_mb'])
VIDEO_MAX_UPLOAD_MB = _get(_conf, ['upload', 'video_max_size_mb'], DEFAULTS['upload']['video_max_size_mb'])