目录

Django+Vue实现文件下载

bookandmusic
bookandmusic 2022年01月24日  ·  阅读 20

在项目中,遇到一个需求,需要下载任务的执行日志。为了熟悉文件下载的流程,特将重点记录,以便后期翻阅。

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)
    }
}
分类: Python
标签: django