feat: Initial commit - Task Reporter incident response system
Complete implementation of the production line incident response system (生產線異常即時反應系統) including: Backend (FastAPI): - User authentication with AD integration and session management - Chat room management (create, list, update, members, roles) - Real-time messaging via WebSocket (typing indicators, reactions) - File storage with MinIO (upload, download, image preview) Frontend (React + Vite): - Authentication flow with token management - Room list with filtering, search, and pagination - Real-time chat interface with WebSocket - File upload with drag-and-drop and image preview - Member management and room settings - Breadcrumb navigation - 53 unit tests (Vitest) Specifications: - authentication: AD auth, sessions, JWT tokens - chat-room: rooms, members, templates - realtime-messaging: WebSocket, messages, reactions - file-storage: MinIO integration, file management - frontend-core: React SPA structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
158
app/modules/file_storage/validators.py
Normal file
158
app/modules/file_storage/validators.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""File validation utilities"""
|
||||
import magic
|
||||
from fastapi import UploadFile, HTTPException
|
||||
from typing import Set
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# MIME type whitelists
|
||||
IMAGE_TYPES: Set[str] = {
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif"
|
||||
}
|
||||
|
||||
DOCUMENT_TYPES: Set[str] = {
|
||||
"application/pdf"
|
||||
}
|
||||
|
||||
LOG_TYPES: Set[str] = {
|
||||
"text/plain",
|
||||
"text/csv"
|
||||
}
|
||||
|
||||
# File size limits (bytes)
|
||||
IMAGE_MAX_SIZE = 10 * 1024 * 1024 # 10MB
|
||||
DOCUMENT_MAX_SIZE = 20 * 1024 * 1024 # 20MB
|
||||
LOG_MAX_SIZE = 5 * 1024 * 1024 # 5MB
|
||||
|
||||
|
||||
def detect_mime_type(file_data: bytes) -> str:
|
||||
"""
|
||||
Detect MIME type from file content using python-magic
|
||||
|
||||
Args:
|
||||
file_data: First chunk of file data
|
||||
|
||||
Returns:
|
||||
MIME type string
|
||||
"""
|
||||
try:
|
||||
mime = magic.Magic(mime=True)
|
||||
return mime.from_buffer(file_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to detect MIME type: {e}")
|
||||
return "application/octet-stream"
|
||||
|
||||
|
||||
def validate_file_type(file: UploadFile, allowed_types: Set[str]) -> str:
|
||||
"""
|
||||
Validate file MIME type using actual file content
|
||||
|
||||
Args:
|
||||
file: FastAPI UploadFile object
|
||||
allowed_types: Set of allowed MIME types
|
||||
|
||||
Returns:
|
||||
Detected MIME type
|
||||
|
||||
Raises:
|
||||
HTTPException if file type is not allowed
|
||||
"""
|
||||
# Read first 2048 bytes to detect MIME type
|
||||
file.file.seek(0)
|
||||
header = file.file.read(2048)
|
||||
file.file.seek(0)
|
||||
|
||||
# Detect actual MIME type from content
|
||||
detected_mime = detect_mime_type(header)
|
||||
|
||||
if detected_mime not in allowed_types:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"File type not allowed: {detected_mime}. Allowed types: {', '.join(allowed_types)}"
|
||||
)
|
||||
|
||||
return detected_mime
|
||||
|
||||
|
||||
def validate_file_size(file: UploadFile, max_size: int):
|
||||
"""
|
||||
Validate file size
|
||||
|
||||
Args:
|
||||
file: FastAPI UploadFile object
|
||||
max_size: Maximum allowed size in bytes
|
||||
|
||||
Raises:
|
||||
HTTPException if file exceeds max size
|
||||
"""
|
||||
# Seek to end to get file size
|
||||
file.file.seek(0, 2) # 2 = SEEK_END
|
||||
file_size = file.file.tell()
|
||||
file.file.seek(0) # Reset to beginning
|
||||
|
||||
if file_size > max_size:
|
||||
max_mb = max_size / (1024 * 1024)
|
||||
actual_mb = file_size / (1024 * 1024)
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"File size exceeds limit: {actual_mb:.2f}MB > {max_mb:.2f}MB"
|
||||
)
|
||||
|
||||
return file_size
|
||||
|
||||
|
||||
def get_file_type_and_limits(mime_type: str) -> tuple[str, int]:
|
||||
"""
|
||||
Determine file type category and size limit from MIME type
|
||||
|
||||
Args:
|
||||
mime_type: MIME type string
|
||||
|
||||
Returns:
|
||||
Tuple of (file_type, max_size)
|
||||
|
||||
Raises:
|
||||
HTTPException if MIME type not recognized
|
||||
"""
|
||||
if mime_type in IMAGE_TYPES:
|
||||
return ("image", IMAGE_MAX_SIZE)
|
||||
elif mime_type in DOCUMENT_TYPES:
|
||||
return ("document", DOCUMENT_MAX_SIZE)
|
||||
elif mime_type in LOG_TYPES:
|
||||
return ("log", LOG_MAX_SIZE)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Unsupported file type: {mime_type}"
|
||||
)
|
||||
|
||||
|
||||
def validate_upload_file(file: UploadFile) -> tuple[str, str, int]:
|
||||
"""
|
||||
Validate uploaded file (type and size)
|
||||
|
||||
Args:
|
||||
file: FastAPI UploadFile object
|
||||
|
||||
Returns:
|
||||
Tuple of (file_type, mime_type, file_size)
|
||||
|
||||
Raises:
|
||||
HTTPException if validation fails
|
||||
"""
|
||||
# Combine all allowed types
|
||||
all_allowed_types = IMAGE_TYPES | DOCUMENT_TYPES | LOG_TYPES
|
||||
|
||||
# Validate MIME type
|
||||
mime_type = validate_file_type(file, all_allowed_types)
|
||||
|
||||
# Get file type category and max size
|
||||
file_type, max_size = get_file_type_and_limits(mime_type)
|
||||
|
||||
# Validate file size
|
||||
file_size = validate_file_size(file, max_size)
|
||||
|
||||
return (file_type, mime_type, file_size)
|
||||
Reference in New Issue
Block a user