aifeilong 13b1ca2b9b Namespace API routes by app and flatten package layout.
Move Django apps out of apps/ and mount them under auth/, common/, content/, and cms/ so endpoint paths map directly to the owning app.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 08:42:37 +08:00

90 lines
2.9 KiB
Python

"""通用媒体上传:图片与文件统一入口,按 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})