2015年1月

Django实现文件上传功能的笔记

给网站增加了上传文件的功能,以便发布内容的时候可以插入图片和附件。翻了一下Django的文档,把Django对文件上传的处理流程做个笔记总结一下。(这里的文件指的是用户上传的文件,和CSS, JS这些静态文件有所不同)

向网站上传文件的流程其实是很简单的,无非就是用户打开一个网页,里面有一个表单,表单里有一个(或多个)文件上传控件,通过上传控件选择了要上传的文件后提交,然后浏览器会向服务器发送POST请求,把要上传的文件和表单的其它内容一起发送给服务器,然后在服务器端进行处理。

用Django做的网站也是这套流程,关键在于服务器对接收到的文件的处理这块上,不同的网站程序有不同的处理方法。Django也有自己的处理流程,我把它总结成两大块:一是预处理(Upload Handlers),二是在View中处理

Upload process of Django

这个网站的文件上传功能设计是这样:文件上传这块是独立的,不和文章有固定的联系。如果发布文章时要上传文件或者图片,那就到文件上传的页面上传文件,然后会得到上传文件的URL,一次上传可以多次使用,只要到已上传文件的列表中复制一个URL即可。

实现步骤如下:

1.定义Model

Django中的信息都是用Model来表示的,文件也不例外,这里把每个附件也用一个model来表示:
# -*- coding: utf-8 -*-
# forum/models.py
# ...

class Attachment(models.Model):
# 上传文件的用户
user = models.ForeignKey('User')
# 被上传的文件
file = models.FileField(upload_to='uploads')
#...


在Model中,文件是用FileFieldImageField来表示的,ImageField是对FileField的扩展,在FileField的基础上增加了一些属性(图像的长度和宽度等)。

2.设定在View中的处理流程

上传的文件是通过表单提交一起提交的,事实上,对它们的处理也和其它表单内容的处理基本一样,可以通过Form, ModelForm等处理后保存为Model的实例。

区别在于:表单中的一般内容是通过request.POST来访问的,而上传的文件要通过request.FILES来访问,这个request.FILES是一个Python的字典对象。 一个表单一次可能上传多个文件,每个文件都是request.FILES这个字典中的元素,我们可以通过request.FILES['key']操作它们, 这里的键名key是指表单中上传控件的名称(name属性值)。1

文档中的示例:

# -*- coding: utf-8 -*-
# forum/views.py

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})

# ...

3.上传文件的存储

文件在View中的处理逻辑也写好了,那上传的文件到底存在哪里呢?——当然是想存在哪里就存在哪里。

可以通过settings.py中的MEDIA_ROOT属性指定文件在服务器上的存储路径绝对路径。例如:

MEDIA_ROOT = "/var/www/example.com/upload/"
或者不要写得这么绝对,比如想把上传的文件放在Django项目目录下的upload/文件夹中,可以写成:
# project/settings.py

# ...

BASE_DIR = os.path.dirname(os.path.dirname(file))

# ...

MEDIA_ROOT = os.path.join(BASE_DIR, 'upload')

如果想把文件动态地放在upload文件夹下不同的子目录中也很容易做到,请参阅官方文档

上面讲的这些都是文件最终的储存位置,那在文件上传到服务器后,在View函数中被处理完成之前它们又是放在哪里呢?之前讲到上传的文件在服务器端的处理有两步,一个是预处理,二是View中的处理。在settings.py中有一个FILE_UPLOAD_HANDLERS属性指定了预处理的流程,它有两个默认值,当然也可以自己写处理流程。FILE_UPLOAD_HANDLERS的默认处理流程会把较小的文件暂时放在内存中,较大的文件放在某个临时文件夹中等待进一步处理,设置中的FILE_UPLOAD_TEMP_DIR指定了临时文件夹的位置,一般不用特别设置,会使用系统默认的/tmp/。2

4.访问已上传的文件

上传完成后就是对文件进行操作和使用了。对文件的操作可以分成两类情况:一种是在Python程序内部使用,另外一种是在WEB页面中访问

先说如何在程序内部操作文件。

Internally, Django uses a django.core.files.File instance any time it needs to represent a file. This object is a thin wrapper around Python’s built-in file object with some Django-specific additions.3

When you use a FileField or ImageField, Django provides a set of APIs you can use to deal with that file.4


一个文件在Django中始终是以django.core.files.File或者它的子类的一个实例的形态的存在的,而File又是对Python中的文件对象(File Objects)的一个简单封装。换句话说,你可以像操作Python原生文件对象一样操作Django中的文件。例如,在request.FILES中,文件的存在形式是UploadedFile的实例(UploadFile是File的一个子类)5,当通过Model的FileField访问文件时,你会得到一个FieldFile的实例(显然,FieldFile也是File的子类)6

再说一下在WEB页面或者是模板中对文件的访问。

在WEB页面中访问文件或者图片就更简单了,只要一个URL就可以了,前面讲到当通过Model的FileField访问文件时,你会得到一个FieldFile的实例,这个FieldFile扩展了默认的File类,提供了一些新的属性和方法,比如FieldFile.url——用来得到文件的URL。

这里问题又来了,通过FieldFile.url这样的属性得到的URL是怎样的呢?是lcfcn.com/aaa/file.exe还是lcf.cn/bbb/file.exe还是aaa.lcfcn.com/file.exe呢?settings.py中的MEDIA_URL就是解决这个问题的,MEDIA_URL指定了MEDIA_ROOT的URL。例如当MEDIA_URL = "/upload/"(MEDIA_URL值需要以/结尾),那生成的文件URL会类似lcfcn.com/upload/filename.ext

到这一步还没有结束,MEIDA_URL只是确定了附件的URL而以,如果想让URL生效,还需要在urls.py中或者WEB服务器上进行对应的设置。到这一步又分成两种情况:一是开发过程中,另一种是生产环境中。如果在是开发过程中,只需要在urls.py中增加对应的设置就好,Django提供了方便的函数,具体照抄文档中的这个例子即可,基本没有需要改的地方:

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = patterns('',
# ... the rest of your URLconf goes here ...
) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

# 文档中说明了static()只有在DEBUG模式中才能生效

如果是在生产环境中,一般来说不会把静态文件交给Django来处理,WEB服务器处理这种情况更拿手。根据不同的WEB服务器,设置也略有不同,不过思路还大同小异的,例如以下的情况:

MEDIA_ROOT = "/home/user/www/site/upload/"

MEDIA_URL = "/upload/"

那在WEB服务器中只要把/upload/指向/home/user/www/site/upload/就好了。


最后再总结一下在Django中实现简单的文件上传需要的一些流程(不分先后顺序):

  • settings.py中设置好MEDIA_ROOTMEDIA_URL
  • urls.py中的设置(开发过程中)
  • WEB服务器(nginx, apache)中的设置(服务器上部署)
  • 在Model中使用FileField或者ImageField
  • 在View中像处理普通表单一样处理上传的文件(记得使用request.FILES)
  • 给表单设定enctype="multipart/form-data属性7