"""DMS gemeinsame Helper & Konstanten — BUG-018 God-Object-Split.""" from __future__ import annotations import os import uuid from fastapi import HTTPException OFFICE_EXTENSIONS = { ".docx": "docx", ".xlsx": "xlsx", ".pptx": "pptx", } # Max file size: 100 MB MAX_FILE_SIZE = 100 * 1024 * 1024 def _parse_uuid(val: str, field: str) -> uuid.UUID: try: return uuid.UUID(val) except (ValueError, TypeError): raise HTTPException( 400, detail={"detail": f"Invalid {field}", "code": "invalid_id"} ) from None def _file_storage_path(tenant_id: uuid.UUID, file_id: uuid.UUID) -> str: """Build relative storage path for a file (relative to storage base).""" return f"{tenant_id}/{file_id}" def _get_file_extension(filename: str) -> str: """Extract lowercase extension including dot.""" return os.path.splitext(filename)[1].lower() def _sanitize_filename(filename: str) -> str: """Sanitize a filename for safe use in Content-Disposition headers.""" import re # Extract basename only (strip any path components) safe = os.path.basename(filename.replace('\\', '/')) # Remove dangerous characters (keep alnum, dot, dash, underscore, space, unicode) safe = re.sub(r'[^a-zA-Z0-9.\-_\u00c0-\u017f\u4e00-\u9fff ]', '_', safe) # Collapse consecutive dots (path traversal prevention) safe = re.sub(r'\.{2,}', '_', safe) # Collapse multiple spaces safe = re.sub(r' {2,}', ' ', safe) # Strip leading dots and whitespace safe = safe.lstrip('.').strip() # Limit length if len(safe) > 200: name, ext = safe.rsplit('.', 1) if '.' in safe[:200] else (safe[:200], '') safe = name[:200] + ('.' + ext if ext else '') return safe or 'file' # Blocked file extensions for security BLOCKED_EXTENSIONS = { ".exe", ".bat", ".cmd", ".sh", ".jar", ".com", ".scr", ".msi", ".dll", ".vbs", ".ps1", ".app", ".bin", ".reg", ".inf", ".php", ".py", ".pl", ".asp", ".aspx", ".jsp", ".svg", ".htaccess", ".phtml", ".pht", ".cgi", ".cfm", ".erb", } # Allowed MIME types for upload validation ALLOWED_MIME_PREFIXES = { "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument", "application/vnd.oasis.opendocument", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/zip", "application/gzip", "application/x-tar", "application/json", "application/xml", "application/rtf", "application/x-7z-compressed", "application/x-rar-compressed", "text/plain", "text/csv", "text/html", "text/markdown", "image/png", "image/jpeg", "image/gif", "image/webp", "image/bmp", "image/tiff", "image/x-icon", "audio/", "video/", "application/octet-stream", } def _is_blocked_filetype(filename: str) -> bool: """Check if a file has a blocked (dangerous) extension.""" ext = os.path.splitext(filename)[1].lower() return ext in BLOCKED_EXTENSIONS chunk_size = 1024 * 1024 # 1MB chunks for streaming uploads # ─── Folders ───