"""通用媒体上传:图片与文件统一入口,按 category 分子目录存储。""" import re import uuid from pathlib import Path from django.conf import settings from rest_framework import permissions, status from rest_framework.parsers import FormParser, MultiPartParser from rest_framework.response import Response from rest_framework.views import APIView ALLOWED_IMAGE_EXT = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"} ALLOWED_FILE_EXT = ALLOWED_IMAGE_EXT | { ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".zip", ".rar", ".7z", ".txt", ".csv", } MAX_IMAGE_BYTES = 8 * 1024 * 1024 MAX_FILE_BYTES = 50 * 1024 * 1024 # 仅允许字母数字、下划线、连字符,防止路径穿越 CATEGORY_RE = re.compile(r"^[a-zA-Z0-9_-]{1,32}$") class MediaUploadView(APIView): """POST /uploads/ 表单字段: file, category(可选), kind=image|file(可选)""" permission_classes = [permissions.IsAdminUser] parser_classes = [MultiPartParser, FormParser] def post(self, request): file = request.FILES.get("file") if not file: return Response({"detail": "请选择文件"}, status=status.HTTP_400_BAD_REQUEST) category = (request.data.get("category") or "misc").strip() if not CATEGORY_RE.match(category): return Response( {"detail": "category 仅允许字母、数字、下划线与连字符"}, status=status.HTTP_400_BAD_REQUEST, ) kind = (request.data.get("kind") or "").strip().lower() ext = Path(file.name).suffix.lower() if not kind: kind = "image" if ext in ALLOWED_IMAGE_EXT else "file" if kind == "image": allowed = ALLOWED_IMAGE_EXT max_bytes = MAX_IMAGE_BYTES size_label = "8MB" else: allowed = ALLOWED_FILE_EXT max_bytes = MAX_FILE_BYTES size_label = "50MB" if file.size > max_bytes: return Response( {"detail": f"文件不能超过 {size_label}"}, status=status.HTTP_400_BAD_REQUEST, ) if ext not in allowed: return Response( { "detail": f"不支持的格式,请上传 {', '.join(sorted(allowed))}", }, status=status.HTTP_400_BAD_REQUEST, ) filename = f"{uuid.uuid4().hex}{ext}" rel_dir = Path(category) abs_dir = Path(settings.MEDIA_ROOT) / rel_dir abs_dir.mkdir(parents=True, exist_ok=True) abs_path = abs_dir / filename with open(abs_path, "wb") as out: for chunk in file.chunks(): out.write(chunk) url = f"/{settings.MEDIA_URL.strip('/')}/{rel_dir.as_posix()}/{filename}" return Response({"url": url, "name": file.name, "kind": kind})