Django REST framework 实现文件上传的两种方式

一、基于 Django models

Django 框架的数据模型(models 类)中定义了 ImageFieldFileField 等类型的字段,可以用来存储图片或者文件对象。
ImageFieldFileField 针对文件对象的属性和行为封装了易于使用的 API,配合 Django REST framework 提供的一系列组件,可以在编写很少量代码的情况下完成初步的文件上传功能。

各组件代码

Models

from django.db import models

class FileModel(models.Model):
    name = models.CharField(max_length=50)
    file = models.FileField(upload_to='upload')

Serializers

from .models import FileModel
from rest_framework import serializers


class FileSerializer(serializers.ModelSerializer):
    class Meta:
        model = FileModel
        fields = '__all__'

Views

from rest_framework import viewsets
from .models import FileModel
from .serializers import FileSerializer

class FileViewSet(viewsets.ModelViewSet):
    queryset = FileModel.objects.all()
    serializer_class = FileSerializer

Urls

from django.urls import path, include
from <appname>.views import FileViewSet
from rest_framework import routers

router = routers.DefaultRouter()
router.register(r'upload', FileViewSet)

urlpatterns = [
    path('', include(router.urls)),
]
功能测试

使用 HTTPie 工具利用 POST 方法以 Form 表单的形式(-f)提交上传的文件:

$ http -f POST http://127.0.0.1:8000/upload/ name="test" file@test.txt
HTTP/1.1 201 Created

{
    "file": "http://127.0.0.1:8000/upload/upload/test.txt",
    "id": 4,
    "name": "test"
}

同时也可以直接访问 http://127.0.0.1:8000/upload/ ,通过 Django REST framework 提供的前端界面手动上传文件。

Django REST framework

或者也可以自定义前端界面,HTML 上传页面示例代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>
    <form action="http://xx.xx.xx.xx:8000/upload/" method="post" enctype="multipart/form-data">
        <input type="text" name="name" placeholder="name"><br>
        <input type="file" name="file"><br>
        <input type="submit" value="submit">
    </form>
</body>

</html>

二、FileField

对于 FileField 类型的文件字段,后台的视图代码可以通过 requests.FILES 获取上传的文件数据。如 requests.FILES['file']
只有当请求方法为 POST 且前端的 <form> 带有属性 enctype="multipart/form-data" 时,request.FILES 才能接收到数据,否则为空。

FileField 字段的 upload_to 属性用于指定上传文件的保存位置,以 settings.py 中定义的 MEDIA_ROOT 为路径前缀。
upload_to 属性可以接收包含 strftime() 格式的日期字符串(/%Y/%m/%d),用来定义类似 /year/month/day 格式的路径。如:

# 文件上传至类似 MEDIA_ROOT/uploads/2019/12/20 的路径下
upload = models.FileField(upload_to='uploads/%Y/%m/%d/')

可以使用 models 提供的查询接口获取已上传文件的相关信息:

$ python manage.py shell
(InteractiveConsole)
>>> from <appname>.models import FileModel
>>> test = FileModel.objects.get(name='test')  # 获取某一条数据记录
>>> test.file  # 数据记录关联的文件对象
<FieldFile: upload/test.txt>
>>> test.file.name  # 文件名
'upload/test.txt'
>>> test.file.url  # 文件 URL
'upload/test.txt'
>>> test.file.size  # 文件大小
22
>>> test.file.path  # 文件路径
'/home/starky/program/python/web/django/filestorage/media/upload/test.txt'

通过数据模型检索到的文件(test.file)为 FieldFile 对象。FieldFile 类封装了一些便捷的 API 可以用来操作关联的底层文件,以下是一些简单的示例。

读取文件内容

>>> test = FileModel.objects.get(name='test')
>>> test.file
<FieldFile: upload/test.txt>
>>> with test.file.open('rb') as f:
...     f.read()
...
b'sdfsdfsdfweofssdnvdvs\n'

写入新的内容

>>> with test.file.open('wb') as f:
...     f.write(b'Hello, World')
...
12
>>> test.file.open().read()
b'Hello, World'

删除关联的底层文件

>>> test.file.delete()
>>> test.file
<FieldFile: None>

新建文件
语法格式为 FieldFile.save(name, content, save=True)
其中 content 参数必须接收 django.core.files.File 类或者其子类的实例,比如 ContentFile,不能直接使用 Python 内置的 file 对象。

不管是删除(FieldFile.delete())还是新建(FieldFile.save())文件,save 参数默认都为 True。即自动调用模型实例的 save() 方法提交对数据库的改动。

>>> testnew = FileModel(name='testnew', file=None)
>>> testnew.file
<FieldFile: None>
>>> from django.core.files.base import ContentFile
>>> content = ContentFile('Hello, Django!')
>>> testnew.file.save('testnew.txt', content)
>>> testnew.file
<FieldFile: upload/testnew.txt>
>>> testnew.file.open().read()
b'Hello, Django!'

三、FileUploadParser

Django REST framework 提供了 parsers.FileUploadParser 类,可以用来处理原始格式的文件内容的上传。后端获取到的 request.data 为字典结构,其中包含的 'file' 键对应的值即为上传的文件对象。

如果 FileUploadParser 类被包含 filename 参数的 URL 调用,则该参数会作为文件保存到服务端后的文件名。若 URL 中不包含 filename 参数,则客户端发起的请求必须包含 Content-Disposition 请求头及 filename 参数。如 Content-Disposition: attachment; filename=upload.jpg

示例代码
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.parsers import FileUploadParser


class FileUploadView(APIView):
    parser_classes = [FileUploadParser, ]

    def put(self, request, filename, format=None):
        file_obj = request.data['file']
        with open(filename, 'wb') as f:
            for chunk in file_obj.chunks():
                f.write(chunk)

        return Response(f'{filename} uploaded',status=204)
# urls.py
from django.urls import re_path

urlpatterns = [
    re_path(r'^files/(?P<filename>[^/]+)$', views.FileUploadView.as_view()),
]

上传接口的 URL 为 http://xx.xx.xx.xx/files/<filename> ,其中 <filenmae> 用于指定上传成功后在服务器端的文件名。客户端使用 PUT 请求上传文件。

使用 postman 测试文件上传,截图如下:

postman

前端上传代码示例如下(使用 jQuery,有可能出现跨域问题,可参考网上资料解决):

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>文件上传</title>
</head>

<body>
    <form>
        <p>上传文件: <input type="file" name="files" id='files' /></p>
        <input type="button" value="上传" onclick="doUpload()" />
    </form>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
    <script type="text/javascript">
        function doUpload() {
            $.ajax({
                url: 'http://xx.xx.xx.xx:8000/files/test.jpg',
                type: 'PUT',
                data: $('#files')[0].files[0],
                cache: false,
                processData: false,
                contentType: false,
                async: false
            }).done(function (res) {
                alert("上传成功")
            }).fail(function (res) {
                alert("上传失败:" + res)
            });
        }
    </script>
</body>

</html>

参考资料

Managing files
models.FileField
parsers

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容