Django 实现文件上传下载API
by:授客 QQ:1033553122 欢迎加入全国软件测试交流QQ群7156436
开发环境
Win 10
Python 3.5.4
Django-2.0.13.tar.gz
官方下载地址:
https://www.djangoproject.com/download/2.0.13/tarball/
vue 2.5.2
djangorestframework-3.9.4
下载地址:
https://github.com/encode/django-rest-framework
附件表设计
from django.db import models # Create your models here. # 上传文件表 class Attachment(models.Model): id = models.AutoField(primary_key=True, verbose_name='自增id') name = models.CharField(max_length=200, verbose_name='附件名称') file_path = models.CharField(max_length=200, verbose_name='附件相对路径') create_time = models.DateTimeField(verbose_name='上传时间') classMeta: db_table = 'tb_attachment' verbose_name = '附件表' verbose_name_plural = verbose_name
项目urls.py配置
修改项目根目录下的urls.py,添加以下带背景色部分的代码内容
#!/usr/bin/env python # -*- coding:utf-8 -*- __author__ = '授客' from django.contrib import admin from django.urls import path from django.conf.urls import include urlpatterns = [ path('admin/', admin.site.urls), path('', include('mywebsite.urls')) #添加API路由配置(这里根据项目实际情况配置) ]
项目settings.py配置
在文件末尾添加以下配置,用于存放附件
MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace('\\', '/')
应用view视图编写
例中直接在views.py视图编写视图,代码如下
#!/usr/bin/env python # -*- coding:utf-8 -*- __author__ = '授客' from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from .models import Attachment from django.http import FileResponse from django.utils import timezone from django.conf import settings import os import uuid import logging logger = logging.getLogger('mylogger') # 批量创建目录 def mkdirs_in_batch(path): try: path = os.path.normpath(path) # 去掉路径最右侧的 \\ 、/ path = path.replace('\\', '/') # 将所有的\\转为/,避免出现转义字符串 head, tail = os.path.split(path) if not os.path.isdir(path) and os.path.isfile(path): # 如果path指向的是文件,则分解文件所在目录 head, tail = os.path.split(head) if tail == '': # head为根目录,形如 / 、D: return True new_dir_path = '' # 存放反转后的目录路径 root = '' # 存放根目录 while tail: new_dir_path = new_dir_path + tail + '/' head, tail = os.path.split(head) root = head else: new_dir_path = root + new_dir_path # 批量创建目录 new_dir_path = os.path.normpath(new_dir_path) head, tail = os.path.split(new_dir_path) temp = '' while tail: temp = temp + '/' + tail dir_path = root + temp if not os.path.isdir(dir_path): os.mkdir(dir_path) head, tail = os.path.split(head) return True except Exception as e: logger.error('批量创建目录出错:%s' % e) return False class AttachmentAPIView(APIView): # 上传附件 def post(self, request, format=None): result = {} try: files = request.FILES file = files.get('file') if not file: result['msg'] = '上传失败,未获取到文件' result['success'] = False return Response(result, status.HTTP_400_BAD_REQUEST) # data = request.POST #获取前端发送的,file之外的其它参数 # extra = data.get('extra') file_name = file.name attachment_name = file_name creater = request.user.username create_time = timezone.now() time_str = create_time.strftime('%Y%m%d') name, suffix = os.path.splitext(file_name) file_name = str(uuid.uuid1()).replace('-', '') + time_str + suffix file_relative_path = '/myapp/attachments/'+ time_str file_absolute_path = settings.MEDIA_ROOT + file_relative_path if not os.path.exists(file_absolute_path):# 路径不存在 if not utils.mkdirs_in_batch(file_absolute_path): result['msg'] = '批量创建路径(%s)对应的目录失败' % file_absolute_path result['success'] = False return Response(result, status.HTTP_500_INTERNAL_SERVER_ERROR) file_relative_path += '/' + file_name data['file_path'] = file_relative_path file_absolute_path = file_absolute_path + '/' + file_name file_handler = open(file_absolute_path, 'wb') # 打开特定的文件进行二进制的写操作 try: for chunk in file.chunks(): # 分块写入文件 file_handler.write(chunk) finally: file_handler.close() # 记录到数据库 try: obj = Attachment(file_path=file_path, name=attachment_name, create_time=create_time, creater=creater) obj.save() except Exception as e: result['msg'] = '上传失败:%s' % e result['success'] = False return Response(result, status.HTTP_400_BAD_REQUEST) result['msg'] = '上传成功' result['success'] = True result['data'] = result_data return Response(result, status.HTTP_200_OK) except Exception as e: result['msg'] = '%s' % e result['success'] = False return Response(result, status.HTTP_500_INTERNAL_SERVER_ERROR)
注意:这里采用UploadedFile.chunks()分块写入,而不是直接使用UploadedFile.read()一次性读取整个文件,是因为如果文件比较大,一次性读取过多内容,会占用系统过多的内存,进而让系统变得更低效。默认的chunks分块默认值为2.5M
file = files.get('file')# 注意:这里的字典key'file'要和前端提交form表单请求时,文件对象对应的表单key保持一致,前端代码如下
letform = newFormData();
form.append("file", file);
# 删除附件 def delete(self, request, format=None): result = {} try: data = request.data attachment_id = data.get('attachment_id') obj = Attachment.objects.filter(id=attachment_id).first() if obj: file_absoulte_path = settings.MEDIA_ROOT + '/'+ obj.file_path if os.path.exists(file_absoulte_path) and os.path.isfile(file_absoulte_path): os.remove(file_absoulte_path) obj.delete() result['msg'] = '删除成功' result['success'] = True return Response(result, status.HTTP_200_OK) except Exception as e: result['msg'] = '%s' % e result['success'] = False return Response(result, status.HTTP_500_INTERNAL_SERVER_ERROR)
# 下载附件 def get(self, request, format=None): result = {} try: data = request.GET attachment_id = data.get('attachmentId') obj = Attachment.objects.filter(id=attachment_id).first() if obj: file_absoulte_path = settings.MEDIA_ROOT+ obj.file_path if os.path.exists(file_absoulte_path) and os.path.isfile(file_absoulte_path): file = open(file_absoulte_path, 'rb') file_response = FileResponse(file) file_response['Content-Type']='application/octet-stream' file_response["Access-Control-Expose-Headers"] = 'Content-Disposition' # 设置可以作为响应的一部分暴露给外部的请求头,如果缺少这行代码,会导致前端请求响应中看不到该请求头 file_response['Content-Disposition']='attachment;filename={}'.format(urlquote(obj.name)) # 这里使用urlquote函数主要为针对文件名为中文时,对文件名进行编码,编码后,前端获取的文件名称形如“%E5%AF%BC%E5%87%BA%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B” return file_response else: result['msg'] = '请求失败,资源不存在' result['success'] = False else: result['msg'] = '请求失败,资源不存在' result['success'] = False return Response(result, status.HTTP_200_OK) except Exception as e: result['msg'] = '%s' % e result['success'] = False return Response(result, status.HTTP_500_INTERNAL_SERVER_ERROR)
说明:
file_response = FileResponse(file),可以在引入StreamingHttpResponse之后(from django.http import StreamingHttpResponse),替换为
file_response = StreamingHttpResponse(file)
前端获取响应头中文件名方法如下:
let disposition = res.headers["content-disposition"];
let filename = decodeURI(disposition.replace("attachment;filename=", "") );
# do something,比如下载:
link.setAttribute("download", filename);
应用urls.py配置
新建urls.py,文件内容如下:
#!/usr/bin/env python # -*- coding:utf-8 -*- __author__ = '授客' from django.urls import re_path from .views import AttachmentAPIView urlpatterns = [ #...略 re_path('^api/v1/testcase/\d+/attachment$', testcase_attachment_views.TestcaseAttachmentAPIView.as_view()), # 给测试用例添加附件 re_path('^api/v1/testcase/\d+/attachment/\d+$', testcase_attachment_views.TestcaseAttachmentAPIView.as_view()), # 删除、下载测试用例关联的附件
前端实现
参考文档“ElementUI Upload上传(利用http-request自定义上传)&下载&删除附件”
参考链接
https://docs.djangoproject.com/zh-hans/2.1/topics/http/file-uploads/
https://docs.djangoproject.com/zh-hans/2.0/ref/files/uploads/