前后端实现文件下载
2024年7月5日大约 5 分钟
在项目中,遇到一个需求,需要下载任务的执行日志。为了熟悉文件下载的流程,特将重点记录,以便后期翻阅。
Django
在后端,主要是接收请求,根据发送的参数,找到对应的 任务日志,然后生成文件流返回响应对象。
注意: 其实不同的文件,如:docx、zip、png等,都可以按照下面的思路,根据文件对象生成文件流,然后指明不同的content-type
即可。
下载txt文件
直接将任务日志的内容作为txt文件返回
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from django.http.response import HttpResponse
from app01.models import Task
class TaskDownloadViewSet(ViewSet):
@staticmethod
def download_log(request):
pk = request.data.get('pk')
if not pk:
return Response(data={"msg": "缺少必要的任务id", "code": 400})
task_obj = Task.objects.filter(pk=pk).first()
if not task_obj:
return Response(data={"msg": "任务不存在", "code": 404})
content = task_obj.log.encode(encoding='utf-8')
file_response = HttpResponse(content, content_type='application/octet-stream')
file_response["Content-Disposition"] = "attachment;filename=log.txt"
return file_response
下载zip压缩包
实现任务日志的批量下载,为了提高下载效率,将其打包为zip压缩包返回。此处,并没有真正在服务器端生成zip文件,而是借用临时文件实现其功能。
from tempfile import TemporaryFile
from zipfile import ZipFile, ZIP_DEFLATED
from rest_framework.viewsets import ViewSet
from django.http.response import HttpResponse, JsonResponse
from app01.models import Task
class TaskDownloadViewSet(ViewSet):
@staticmethod
def download_log_zip(request):
pk_list = request.data.get('pk_list')
if not pk_list or (pk_list and not isinstance(pk_list, list)):
return Response(data={"msg": "缺少必要的任务id列表", "code": 400})
task_obj_list = Task.objects.filter(pk__in=pk_list)
if not task_obj_list.count():
return Response(data={"msg": "任务不存在", "code": 404})
temp = TemporaryFile() # 创建临时文件
with ZipFile(temp, 'a', ZIP_DEFLATED) as archive: # 在临时文件基础上,创建一个压缩文件对象
for task_obj in task_obj_list:
name = f'{task_obj.name}.txt'
content = task_obj.log.encode(encoding='utf-8')
archive.writestr(name, content) # 将内容写入压缩文件对象中
temp.seek(0) # 将指针移动到文件开头
wrapper = FileWrapper(temp)
file_response = FileResponse(wrapper, content_type='application/zip')
file_response["Content-Disposition"] = "attachment;filename=archive.zip"
return file_response
注意
HttpResponse
会直接读取文件对象,然后将对象的内容存储成字符串,然后返回给客户端,同时释放内存。可以看出,当文件变大,这是一个非常耗费时间和内存的过程,很容易造成服务器卡死。
from tempfile import TemporaryFile
from wsgiref.util import FileWrapper
from zipfile import ZipFile, ZIP_DEFLATED
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from django.http.response import StreamingHttpResponse
from app01.models import Task
class TaskDownloadViewSet(ViewSet):
@staticmethod
def download_log(request):
pk_list = request.data.get('pk_list')
task_obj_list = Task.objects.filter(pk__in=pk_list, is_delete=False)
if not task_obj_list.count():
return Response(data={"msg": "任务不存在", "code": 404})
temp = TemporaryFile() # 创建临时文件
with ZipFile(temp, 'a', ZIP_DEFLATED) as archive: # 在临时文件基础上,创建一个压缩文件对象
for task_obj in task_obj_list:
name = f'{task_obj.name}.txt'
content = task_obj.log.encode(encoding='utf-8')
archive.writestr(name, content) # 将内容写入压缩文件对象中
temp.seek(0) # 将指针移动到文件开头
wrapper = FileWrapper(temp) # 使用python内置的文件包装器,将文件对象转换成迭代器,分块处理。
file_response = StreamingHttpResponse(wrapper, content_type='application/octet-stream')
file_response["Content-Disposition"] = "attachment;filename=archive.zip"
return file_response
StreamingHttpResponse
是将文件内容进行流式传输,数据量大可以用这个方法。但是千万注意:如果需要返回的是具体的文件内容,而不是 文件对象,就不能使这个响应类,只能使用HttpResponse
构建响应对象。
下载docx报告
from tempfile import TemporaryFile
from wsgiref.util import FileWrapper
from docx import Document
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from django.http.response import StreamingHttpResponse
from app01.models import Task
class TaskDownloadViewSet(ViewSet):
@staticmethod
def download_report(request):
pk = request.data.get('pk')
if not pk:
return Response(data={"msg": "缺少必要的任务id", "code": 400})
task_obj = Task.objects.filter(pk=pk).first()
if not task_obj:
return Response(data={"msg": "任务不存在", "code": 404})
document = Document() # 创建docx文档对象
document.add_heading(task_obj.name, 0) # 添加标题
for line in task_obj.log.split('\n'): # 将每一行日志作为一个段落写入文档
document.add_paragraph(line)
temp = TemporaryFile() # 保存到临时文件
document.save(temp)
temp.seek(0)
wrapper = FileWrapper(temp)
file_response = StreamingHttpResponse(wrapper, content_type='application/msword')
file_response["Content-Disposition"] = "attachment;filename=log.docx"
return file_response
下载图片
import os
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from django.http.response import StreamingHttpResponse
from django01.settings import BASE_DIR
class TaskDownloadViewSet(ViewSet):
@staticmethod
def download_img(request):
filename = request.data.get('filename')
if not filename:
return Response(data={"msg": "缺少必要的图片名", "code": 400})
path = os.path.join(BASE_DIR, f'media/{str(filename)}')
if not os.path.exists(path):
return Response(data={"msg": "图片不存在", "code": 404})
file_response = StreamingHttpResponse(open(path, 'rb'), content_type='image/png')
file_response["Content-Disposition"] = "attachment;filename=log.png"
return file_response
因为图片是已经存在的文件,所以需要打开文件,得到文件操作符,StreamingHttpResponse
需要的是可迭代对象,文件操作符本身就是可迭代的,因此,不需要额外的其他操作。
Vue
在前端,主要是发送请求,接收后端的文件流,然后按照对应的格式保存到本地。需要注意的是,请求文件流时,需要指明 responseType: "arraybuffer"
。如果不指明该参数,则会导致保存的文件格式损坏 。
保存文件对象
function saveFile(data, type, fileName) {
let blob = new Blob([data], {
type: type
})
if (window.navigator.msSaveOrOpenBlob) {
// IE10+下载
navigator.msSaveBlob(blob, fileName)
} else {
// 非IE下载
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob)
link.download = fileName
link.click()
// 释放内存
window.URL.revokeObjectURL(link.href)
}
}
下载txt文件
因为后端返回的数据可能是 文件流,也可能是json数据,所以通过转换判断是否为json,从而决定接下来的逻辑处理。
async function download_log(pk) {
let res = await this.$axios.post("download_log/", {
'pk': pk
}, {responseType: 'arraybuffer'});
if (!res) return;
try {
//如果JSON.parse(enc.decode(new Uint8Array(res.data)))不报错,说明后台返回的是json对象,则弹框提示
//如果JSON.parse(enc.decode(new Uint8Array(res.data)))报错,说明返回的是文件流,进入catch,下载文件
let enc = new TextDecoder('utf-8')
res = JSON.parse(enc.decode(new Uint8Array(res.data))) //转化成json对象
alert(res.msg)
} catch (err) {
const timestamp = (new Date()).valueOf();
const fileName = timestamp + '.txt'
this.saveFile(res.data, 'text/plain', fileName)
}
}
下载zip压缩包
async function download_log_zip(pk_list) {
let res = await this.$axios.post("download_log_zip/", {
'pk_list': pk_list
}, {responseType: 'arraybuffer'});
if (!res) return;
try {
let enc = new TextDecoder('utf-8')
res = JSON.parse(enc.decode(new Uint8Array(res.data))) //转化成json对象
alert(res.msg)
} catch (err) {
const timestamp = (new Date()).valueOf();
const fileName = timestamp + '.zip'
this.saveFile(res.data, 'application/zip', fileName)
}
}
下载docx报告
async function download_report(pk) {
let res = await this.$axios.post("download_report/", {
'pk': pk
}, {responseType: 'arraybuffer'});
if (!res) return;
try {
let enc = new TextDecoder('utf-8')
res = JSON.parse(enc.decode(new Uint8Array(res.data))) //转化成json对象
alert(res.msg)
} catch (err) {
const timestamp = (new Date()).valueOf();
const fileName = timestamp + '.docx'
this.saveFile(res.data, 'application/msword', fileName)
}
}
下载图片
async function download_img(fileName) {
var res = await this.$axios.post("download_img/", {
'filename': fileName
}, {responseType: 'arraybuffer'});
if (!res) return;
try {
let enc = new TextDecoder('utf-8')
res = JSON.parse(enc.decode(new Uint8Array(res.data))) //转化成json对象
alert(res.msg)
} catch (err) {
const timestamp = (new Date()).valueOf();
const fileName = timestamp + '.png'
this.saveFile(res.data, 'image/png', fileName)
}
}