文件上传

当 Django 处理文件上传时,文件数据最终会被放置在 request.FILES (关于 request 对象的更多信息,请参见 request 和 response 对象 的文档)。本文档解释了文件如何存储在磁盘和内存中,以及如何自定义默认行为。

警告

如果接收不受信任的用户的上传会有安全隐患, 请阅读 用户上传内容 获取详情。

简单文件上传

考虑一个包含 FileField 的表单:

forms.py
from django import forms


class UploadFileForm(forms.Form):
    title = forms.CharField(max_length=50)
    file = forms.FileField()

处理这个表单的视图将在 request.FILES 中接收文件数据,它是一个字典,包含表单中每个 FileField (或 ImageField,或其他 FileField 子类)的键。所以上述表单中的数据将以 request.FILES['file'] 的形式被访问。

注意 request.FILES 只有当请求方法是 POST,至少有一个文件字段被实际发布,并且发布请求的 <form>enctype="multipart/form-data" 属性时,才会包含数据。否则 request.FILES 将为空。

大多数情况下,你需要像 将上传的文件绑定到表单中 里描述的那样将文件数据从 request 传递给表单。示例如下:

views.py
from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import UploadFileForm

# Imaginary function to handle an uploaded file.
from somewhere import handle_uploaded_file


def upload_file(request):
    if request.method == "POST":
        form = UploadFileForm(request.POST, request.FILES)
        if form.is_valid():
            handle_uploaded_file(request.FILES["file"])
            return HttpResponseRedirect("/success/url/")
    else:
        form = UploadFileForm()
    return render(request, "upload.html", {"form": form})

注意我们必须将 request.FILES 传入到表单的 构造方法中,只有这样文件数据才能绑定到表单中。

我们通常可能像这样处理上传文件:

def handle_uploaded_file(f):
    with open("some/file/name.txt", "wb+") as destination:
        for chunk in f.chunks():
            destination.write(chunk)

使用 UploadedFile.chunks() 而不是 read() 是为了确保即使是大文件又不会将我们系统的内存占满。

There are a few other methods and attributes available on UploadedFile objects; see UploadedFile for a complete reference.

通过模型来处理上传的文件

如果想要在 FileField 上的 Model 保存文件,使用 ModelForm 会让这一过程变得简单。当调用 form.save() 时,文件对象将会被保存在对相应 FileFieldupload_to 参数所指定的地方:

from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import ModelFormWithFileField


def upload_file(request):
    if request.method == "POST":
        form = ModelFormWithFileField(request.POST, request.FILES)
        if form.is_valid():
            # file is saved
            form.save()
            return HttpResponseRedirect("/success/url/")
    else:
        form = ModelFormWithFileField()
    return render(request, "upload.html", {"form": form})

如果你准备手工构建对象,你可以指定来自 request.FILES 的文件对象到模型里的文件对象:

from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import UploadFileForm
from .models import ModelWithFileField


def upload_file(request):
    if request.method == "POST":
        form = UploadFileForm(request.POST, request.FILES)
        if form.is_valid():
            instance = ModelWithFileField(file_field=request.FILES["file"])
            instance.save()
            return HttpResponseRedirect("/success/url/")
    else:
        form = UploadFileForm()
    return render(request, "upload.html", {"form": form})

If you are constructing an object manually outside of a request, you can assign a File like object to the FileField:

from django.core.management.base import BaseCommand
from django.core.files.base import ContentFile


class MyCommand(BaseCommand):
    def handle(self, *args, **options):
        content_file = ContentFile(b"Hello world!", name="hello-world.txt")
        instance = ModelWithFileField(file_field=content_file)
        instance.save()

上传多个文件

If you want to upload multiple files using one form field, create a subclass of the field's widget and set the allow_multiple_selected attribute on it to True.

In order for such files to be all validated by your form (and have the value of the field include them all), you will also have to subclass FileField. See below for an example.

Multiple file field

Django is likely to have a proper multiple file field support at some point in the future.

forms.py
from django import forms


class MultipleFileInput(forms.ClearableFileInput):
    allow_multiple_selected = True


class MultipleFileField(forms.FileField):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault("widget", MultipleFileInput())
        super().__init__(*args, **kwargs)

    def clean(self, data, initial=None):
        single_file_clean = super().clean
        if isinstance(data, (list, tuple)):
            result = [single_file_clean(d, initial) for d in data]
        else:
            result = single_file_clean(data, initial)
        return result


class FileFieldForm(forms.Form):
    file_field = MultipleFileField()

然后覆盖 FormView 子类的 post 方法来控制多个文件上传:

views.py
from django.views.generic.edit import FormView
from .forms import FileFieldForm


class FileFieldFormView(FormView):
    form_class = FileFieldForm
    template_name = "upload.html"  # Replace with your template.
    success_url = "..."  # Replace with your URL or reverse().

    def post(self, request, *args, **kwargs):
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form):
        files = form.cleaned_data["file_field"]
        for f in files:
            ...  # Do something with each file.
        return super().form_valid()

警告

This will allow you to handle multiple files at the form level only. Be aware that you cannot use it to put multiple files on a single model instance (in a single field), for example, even if the custom widget is used with a form field related to a model FileField.

Changed in Django 3.2.19:

In previous versions, there was no support for the allow_multiple_selected class attribute, and users were advised to create the widget with the HTML attribute multiple set through the attrs argument. However, this caused validation of the form field to be applied only to the last file submitted, which could have adverse security implications.

上传 Handlers

当一个用户上传文件时,Django 会把文件数据传递给 upload handler —— 这是一个很小的类,它用来在上传时处理文件数据。上传处理模块最初定义在 FILE_UPLOAD_HANDLERS 里,默认为:

[
    "django.core.files.uploadhandler.MemoryFileUploadHandler",
    "django.core.files.uploadhandler.TemporaryFileUploadHandler",
]

MemoryFileUploadHandlerTemporaryFileUploadHandler 提供 Django 默认文件上传行为,小文件读入内存,大文件存在磁盘上。

你可以编写自定义的 handlers 来自定义 Django 如何处理文件。比如,你可以使用自定义的 handlers 来强制处理用户层面的配额,动态压缩数据,渲染进度条,甚至可以将数据发送到其他存储地址而不是本地。查看 编写自定义上传处理程序 来了解你如何自定义或者完全替换上传行为。

上传数据的存储

在保存上传的文件之前,数据需要保存到某处。

默认情况下,如果上传的文件小于2.5兆,Django 将把文件的所有内容保存到内存里。这意味着保存文件只涉及从内存中读取和写入磁盘,因此这很快。

但如果上传的文件很大,Django 会把文件写入系统临时目录的临时文件里存储。在类 Unix 平台里这意味着 Django 会生成一个类似名为 /tmp/tmpzfp6I6.upload 的文件。如果上传的文件非常大,你可以查看这个文件的大小增长,因为 Django 将数据流式传输到磁盘上。

2.5 megabytes; /tmp; 等这些细节只是合理的默认值,在下一节要介绍它们如何被自定义。

改变上传处理行为

这里有一些控制 Django 文件上传行为的配置,请查看 File Upload Settings

动态修改上传处理程序

有时候某些视图需要不同的上传行为。在这些例子里,你可以基于每个请求覆盖上传处理程序。默认情况下,这个列表将包含由 FILE_UPLOAD_HANDLERS 设置的上传处理程序,但你可以像修改其他列表一样修改这个列表。

比如,假设你正在编写 ProgressBarUploadHandler ,来提供在上传过程中的反馈给 Ajax widget。你需要添加这个处理程序到你的上传处理模块:

request.upload_handlers.insert(0, ProgressBarUploadHandler(request))

你或许想在这里使用 list.insert() (而不是 append() ),因为进度条处理程序需要在其他处理程序之前使用。记住,上传处理程序是按顺序处理的。

如果你想完全替换上传处理程序,你需要指定新列表:

request.upload_handlers = [ProgressBarUploadHandler(request)]

备注

你只能在访问 request.POSTrequest.FILES 之前修改上传处理程序,开始上传处理后修改上传处理程序没有意义。如果你从读取 request.POSTrequest.FILES 之后尝试修改 request.upload_handlers ,Django 会报错。

因此,你要尽早在视图里修改上传处理程序。

而且, request.POSTCsrfViewMiddleware 访问,默认情况下已开启。这意味着你需要在视图上使用 csrf_exempt() 来允许你改变上传处理程序。然后你需要在实际处理请求的函数上使用 csrf_protect() 。注意这可能会让处理程序在 CSRF 检测完成之前开始接受文件上传。示例:

from django.views.decorators.csrf import csrf_exempt, csrf_protect


@csrf_exempt
def upload_file_view(request):
    request.upload_handlers.insert(0, ProgressBarUploadHandler(request))
    return _upload_file_view(request)


@csrf_protect
def _upload_file_view(request):
    ...  # Process request

如果你使用的是基于类的视图,你需要在其 csrf_exempt() 方法上使用 dispatch(),并在实际处理请求的方法上使用 csrf_protect()。示例代码:

from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt, csrf_protect


@method_decorator(csrf_exempt, name="dispatch")
class UploadFileView(View):
    def setup(self, request, *args, **kwargs):
        request.upload_handlers.insert(0, ProgressBarUploadHandler(request))
        super().setup(request, *args, **kwargs)

    @method_decorator(csrf_protect)
    def post(self, request, *args, **kwargs):
        ...  # Process request