文件上传的本质就是把本地的文件复制到服务端,那么客户端和服务端要怎样操作才能完成这个复制过程呢,这一篇我们就一起来看看。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。
基本上传文件
客户端配置
页面的表单利用<input type="file" name="">
来上传文件,不过需要注意的是,要想让服务端能成功接收到文件,需要给表单加一个额外的配置就是enctype="multipart/form-data"
。
同时,如果要允许一次性上传多个文件,可以给input
加上multiple
属性。
而要限制上传的文件MIME类型,可以给input
加上accept
属性。
创建一个h5页面upload_file.html
如下备用,允许上传多个文件,同时限制了文件类型MIME
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Upload File</title>
</head>
<body>
<form action="{% url 'four:upload_file' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<label for="upload">File:</label><input type="file" id="upload" name="upload" multiple accept="image/jpeg,text/plain"><br>
<input type="submit">
</form>
</body>
</html>
这里的{% csrf_token %}
是为了防止csrf嵌入的标志,不加这个标志无法使用post方法。
UploadedFile类
在view函数中通过HttpRequest.FILES
来获取上传的所有文件,类似HttpRequest.POST
,也是通过字典的方式来进行访问,字典的key是h5页面中<input type="file">
的name
名,而字典的value是一个UploadedFile类的实例。
根据官方文档,有下面几种方法获取上传文件的内容
-
UploadedFile.read()
一次读取文件的所有内容,一般不使用这个方法因为一旦上传的文件过大,可能会使得内存溢出 -
UploadedFile.multiple_chunks(chunk_size=None)
如果上传的文件过大以至于需要分多个trunk来读取,这个方法会返回True,这个大小的临界点默认是2.5MB,可配 -
UploadedFile.chunks(chunk_size=None)
一个包含文件所有trunk的生成器,当上面的UploadedFile.multiple_chunks()==True
的时候,通过在一个循环中读取每个trunk来获取文件而不是通过上面的UploadFile.read()
。根据官方文档,推荐在任何情况下使用本方法来读取文件内容,经过我自己的测试,即使上面的UploadedFile.multiple_chunks()==False
的时候也是没问题的
同时还有下面几种属性可以调用
-
UploadedFile.name
上传文件的文件名,包含后缀 -
UploadedFile.size
单位为bytes的文件大小 -
UploadedFile.content_type
文件的MIME类型
服务端操作
在static/upload/
下创建两个目录images
和text
来存储上传的两类文件,然后创建下面的路由和view函数用来接收文件
path('upload_file/', views.upload_file, name='upload_file'),
def upload_file(request):
if request.method == 'GET':
return render(request, 'upload_file.html')
elif request.method == 'POST':
uploaded_list = request.FILES.getlist('upload')
for uploaded in uploaded_list:
if uploaded.content_type.startswith('image/'):
file_type = 'images'
elif uploaded.content_type.startswith('text/'):
file_type = 'text'
with open('/home/fuhx/python_projects/django/DjangoModel/static/upload/' + file_type + '/' + uploaded.name,
'wb') as f:
for chunk in uploaded.chunks():
f.write(chunk)
f.flush()
return HttpResponse('upload successfully')
这里的逻辑如下:
- 内聚函数,根据请求的方法来看是返回html还是响应API
- 通过
request.FILES.getlist()
来获取包含所有文件的一个list,这里不能用get()
,不然只能返回QueryDict的最后一个元素 - 通过文件类型来决定存储路径的子目录,值得一提的是,这里也可以用正则表达式来完成,例如
if re.match(r'^image/',uploaded.content_type): pass
- 通过文件描述符,以二进制方式将每个trunk写入,每次写入都flush一下清空缓存
- 所有文件存储完毕,返回一个成功页面
效果演示
最后的效果如下:
-
打开
http://127.0.0.1:8000/four/upload_file/
如下
-
选择文件,将符合MIME的三个文件都选上
-
点击
submit
按钮上传
-
成功上传
-
在服务端看到不同类型文件被存储到了不同目录
利用数据模型简化操作
上面这种存储方式可以说是放之四海而皆准,不管是Django还是Flask都可以用,虽然正确但不简洁。Django为了方便存储上传的文件,在数据模型中特意创建了一个FileField用来实现这一目的。下面我们就来看看怎么使用FileField来更简洁地存储文件。当然还有一个ImageField,它继承了FileField所有特征,只是会判断上传的文件是不是图片而已。
在models.py
中定义2个模型如下,分别用来存储图片和文件
class Upload_Image(models.Model):
u_name = models.CharField(max_length=16)
u_path = models.ImageField(upload_to='images')
class Upload_Text(models.Model):
u_name = models.CharField(max_length=16)
u_path = models.FileField(upload_to='text')
然后进行迁移,如果是第一次使用ImageField
,迁移的时候会有下面的报错
Cannot use ImageField because Pillow is not installed.
直接用pip安装下pillow即可,或者在Pycharm中安装也可以。
需要额外注意的就是这里的upload_to
配置项,下面的Django文件存储系统会详细讲。 这里只需要知道这是一个相对路径。
然后类似上面的方法创建h5页面filefield.html
如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Image Field</title>
</head>
<body>
<form action="{% url 'four:filefield' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<label for="name">Name:</label><input type="text" id="name" name="name"><br>
<label for="upload">Upload:</label><input type="file" id="upload" name="upload" multiple accept="image/jpeg,text/plain"><br>
<input type="submit">
</form>
</body>
</html>
创建路由和view函数
path('filefield/', views.filefield, name='filefield'),
def filefield(request):
if request.method == 'GET':
return render(request, 'filefield.html')
elif request.method == 'POST':
name = request.POST.get('name')
upload_list = request.FILES.getlist('upload')
for upload in upload_list:
if upload.content_type.startswith('image/'):
upload_image = Upload_Image()
upload_image.u_name = name
upload_image.u_path = upload
upload_image.save()
elif upload.content_type.startswith('text/'):
upload_text = Upload_Text()
upload_text.u_name = name
upload_text.u_path = upload
upload_text.save()
return HttpResponse('Upload Successfully')
基本逻辑和上面利用trunk来存储类似,不过这里是通过实例化模型类,然后将上传的UploadedFile直接赋值给FileField或者ImageField,剩下的事情都由Django帮我们完成了。
Django的文件存储系统
上面在建立模型的时候说到upload_to
参数是一个相对路径,那么是相对哪一个目录的路径呢?
答案是根据settings.py
中配置的MEDIA_ROOT
目录
In your settings file, you’ll need to define MEDIA_ROOT as the full path to a directory where you’d like Django to store uploaded files.
我这里配置的是MEDIA_ROOT = os.path.join(BASE_DIR, 'static/upload')
效果演示
效果演示如下:
-
访问
http://127.0.0.1:8000/four/filefield/
如下
-
同样选择刚才的三个文件并上传
-
可以看到ImageField中存储了图片文件的存储路径
-
而FileField中存储了文本文件的存储路径
这样就基本达到了存储上传文件的目的。
重复文件处理
那么如果用户将内容不同但名字相同的,能不能不要覆盖旧文件呢?
Django已经帮我们考虑了这个需求。
如果重复一遍上面的步骤结果如下
可以看到同样名字的文件被加入了后缀再存储,这样以后想获取同一名字的最新文件,用model.objects.last()
即可。
按时间动态创建路径
Django帮我们考虑的还不仅这些,大家也许知道Linux有一个bug,就是当一个目录内的文件达到65535个的时候,该目录将无法打开。
所以在生产环境中对文件的分类存储就显得尤为重要,最简单有效的就是按照日期来创建目录归档。
上面FileField
或者ImageField
的upload_to
参数除了支持固定路径,还支持strftime()
对象,也就是说可以用%Y
,%m
,%d
来做为路径名
If you specify a string value, it may contain strftime() formatting, which will be replaced by the date/time of the file upload (so that uploaded files don’t fill up the given directory).
修改上面的模型定义如下
class Upload_Image(models.Model):
u_name = models.CharField(max_length=16)
u_path = models.ImageField(upload_to='images/%Y/%m/%d')
class Upload_Text(models.Model):
u_name = models.CharField(max_length=16)
u_path = models.FileField(upload_to='text/%Y/%m/%d')
重新迁移到数据库,再重复上面的上传操作,结果如下
可以看到自动按照当前日期创建了子目录,这样只要每天上传的文件不超过65535就不用担心了。
获取服务器文件
上传到服务器的文件也可以再次被获取,这里就通过获取最新上传的图片为例来演示一下。
创建路由和view函数如下
path('show_image/', views.show_image, name='show_image'),
def show_image(request):
name = request.GET.get('name')
upload_image = Upload_Image.objects.filter(u_name=name).last()
context = {
'name': name,
'url': '/static/upload/'+upload_image.u_path.url
}
return render(request, 'show_image.html', context=context)
这里的upload_image.u_path
有url
和path
两个路径可以返回:
url
是相对于MEDIA_ROOT
的路径path
是相对于根目录/
的路径
这里使用upload_image.u_path.url
前面再加上MEDIA_ROOT
定义中的后半部分,即可形成h5页面可使用的url了。
关于h5页面中的url问题,可以参考我的另一篇博客《HTML通过href实现页面跳转的几种不同路径写法》
其中的h5页面show_image.html
如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Show Image</title>
</head>
<body>
<h2>{{ name }}</h2>
<img src="{{ url }}" alt="image">
</body>
</html>
此时访问http://127.0.0.1:8000/four/show_image/?name=xiaofu
的效果如下
总结
文件的上传,存储和使用我们都了解的差不多了,那么可以说MTV模型剩余的一点边角料也都了解到了。下一篇开始我们就要进入Django的进阶学习了。