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>
90 lines
2.9 KiB
Python
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})
|