小易说IT 小易说IT

Django + DRF 文件上传 + 下载完整示例

要点前置:

  1. 文件上传必须用 multipart/form-data,不能用 application/json

  2. 开发环境通过 MEDIA_ROOT / MEDIA_URL 托管上传文件;生产环境交给 Nginx 代理 media,不要让 Django 直接提供静态文件下载

  3. DRF 解析文件依赖 MultiPartParser

  4. 下载使用 Django 原生 FileResponse,as_attachment=True 触发浏览器下载弹窗Django

1. settings.py 配置(新增媒体文件配置)

import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'fileserver', # 我们本次的app
]

# ========= 文件上传核心配置 =========
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")

# 可选:限制上传大小
DATA_UPLOAD_MAX_MEMORY_SIZE = 52428800  # 50MB
FILE_UPLOAD_MAX_MEMORY_SIZE = 10485760   # 10MB,超过会写到临时磁盘

在项目根目录手动新建 media 文件夹

2. 创建 app + 模型 models.py

python manage.py startapp fileserver

fileserver/models.py

from django.db import models

class UploadFile(models.Model):
    name = models.CharField(max_length=255, verbose_name="文件原名")
    file = models.FileField(upload_to="uploads/%Y/%m/%d/", verbose_name="文件")
    size = models.BigIntegerField(default=0, verbose_name="文件大小byte")
    mime_type = models.CharField(max_length=128, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name
  • upload_to="uploads/%Y/%m/%d/":自动按年月日分目录存放文件,避免单目录文件太多

  • 数据库只存文件相对路径,二进制文件保存在磁盘 media 目录

执行迁移:

python manage.py makemigrations
python manage.py migrate

3. 序列化器 serializers.py(新建)

fileserver/serializers.py

from rest_framework import serializers
from .models import UploadFile

class UploadFileSerializer(serializers.ModelSerializer):
    file_url = serializers.SerializerMethodField(read_only=True)
    class Meta:
        model = UploadFile
        fields = ["id", "name", "file", "file_url", "size", "mime_type", "created_at"]
        read_only_fields = ["id", "size", "mime_type", "created_at"]

    def get_file_url(self, obj):
        # 返回文件完整访问链接
        request = self.context.get("request")
        if obj.file and request:
            return request.build_absolute_uri(obj.file.url)
        return None

    def create(self, validated_data):
        file_obj = validated_data["file"]
        validated_data["size"] = file_obj.size
        validated_data["mime_type"] = file_obj.content_type
        return super().create(validated_data)

4. 视图 views.py(上传接口 + 单独下载接口)

fileserver/views.py

from rest_framework import viewsets, parsers, status
from rest_framework.response import Response
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django.http import FileResponse, Http404
import os
from .models import UploadFile
from .serializers import UploadFileSerializer

class UploadFileViewSet(viewsets.ModelViewSet):
    queryset = UploadFile.objects.all().order_by("-created_at")
    serializer_class = UploadFileSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]
    # 必须开启MultiPartParser,才能接收文件
    parser_classes = [parsers.MultiPartParser, parsers.FormParser]

    @action(detail=True, methods=["get"], url_path="download")
    def download(self, request, pk=None):
        """ 下载接口:GET /api/files/{id}/download/ """
        obj = self.get_object()
        file_path = obj.file.path
        if not os.path.exists(file_path):
            raise Http404("文件不存在")
        # as_attachment=True 弹出下载;False则浏览器在线预览(图片/pdf)
        return FileResponse(
            open(file_path, "rb"),
            as_attachment=True,
            filename=obj.name
        )

说明:

  • 普通 list/create/delete 由 ModelViewSet 自带(上传就是 POST)

  • 自定义 action download 单独处理文件下载

5. 路由 urls.py

fileserver/urls.py

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import UploadFileViewSet

router = DefaultRouter()
router.register(r'files', UploadFileViewSet)

urlpatterns = [
    path("", include(router.urls)),
]

项目根 urls.py

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('fileserver.urls')),
    path('api-auth/', include('rest_framework.urls')),
]
# 开发环境才启用media路由!生产注释掉,交给Nginx
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

6. 启动 & curl 测试

python manage.py runserver

✅ 上传文件(POST,multipart/form-data)

curl -X POST http://127.0.0.1:8000/api/files/ \
-F "name=测试文档.pdf" \
-F "file=@/Users/xxx/test.pdf"

✅ 获取文件列表

curl http://127.0.0.1:8000/api/files/

✅ 下载文件(GET)

curl http://127.0.0.1:8000/api/files/1/download/ -O

✅ 删除文件

curl -X DELETE http://127.0.0.1:8000/api/files/1/

7. 前端 Vue/axios 上传注意点

const formData = new FormData()
formData.append("name", file.name)
formData.append("file", file.raw)
// headers不要手动设置Content-Type!axios会自动生成boundary
axios.post("/api/files/", formData)

8. 生产环境重要注意事项

  1. 关闭 DEBUG,删掉 static (settings.MEDIA...),Django 不能直接对外提供大文件

  2. Nginx 配置 media 代理示例:

location /media/ {
    alias /path/to/your/project/media/;
    expires 7d;
}
  1. 安全风险:不要直接信任上传文件后缀;增加后缀白名单校验(防止上传 php/exe 等恶意文件)

  2. 大文件:上面是一次性读入;超大文件建议前端分片上传 + 后端分片合并

  3. 存储扩展:如果文件量大,可以替换为对象存储(MinIO、OSS),重写存储后端