微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

从Django服务器一次流式传输多个文件

如何解决从Django服务器一次流式传输多个文件

我正在运行Django服务器,以从受保护的网络中的另一台服务器提供文件。当用户发出请求一次访问多个文件的请求时,我希望Django服务器将所有这些文件立即流式传输给该用户

由于在浏览器中一次下载多个文件不容易,因此文件需要以某种方式捆绑在一起。我不希望服务器先下载所有文件然后再提供现成的捆绑文件,因为这样会增加较大文件的时间损耗。对于拉链,我的理解是在组装时无法流式传输。

一旦远程服务器的第一个字节可用,是否有任何方法可以开始流传输容器?

解决方法

Tar文件用于将多个文件收集到一个存档中。它们是为磁带录音机开发的,因此可以提供顺序的写入和读取。

使用Django,可以使用FileResponse()将文件流式传输到浏览器,该浏览器可以将生成器作为参数。

如果我们向它提供一个生成器,该生成器将tar文件与用户请求的数据组合在一起,那么tar文件将及时生成。但是,内置的tarfile模块python并没有提供这种功能。

不过,我们可以利用tarfile的能力来传递类似File的对象来自己处理档案的汇编。因此,我们可以创建一个BytesIO()对象,该tarfile将被递增地写入,并将其内容刷新到Django的FileResponse()方法中。为此,我们需要实现FileResponse()tarfile希望访问的一些方法。让我们创建一个类FileStream

class FileStream:
    def __init__(self):
        self.buffer = BytesIO()
        self.offset = 0

    def write(self,s):
        self.buffer.write(s)
        self.offset += len(s)

    def tell(self):
        return self.offset

    def close(self):
        self.buffer.close()

    def pop(self):
        s = self.buffer.getvalue()
        self.buffer.close()
        self.buffer = BytesIO()
        return s

现在,当我们将write()数据发送到FileStream的缓冲区并且yield FileStream.pop()时,Django会将这些数据立即发送给用户。

作为数据,我们现在要汇编该tar文件。在FileStream类中,我们添加了另一个方法:

    @classmethod
    def yield_tar(cls,file_data_iterable):
        stream = FileStream()
        tar = tarfile.TarFile.open(mode='w|',fileobj=stream,bufsize=tarfile.BLOCKSIZE)

这将在内存中创建一个FileStream实例和一个文件句柄。文件句柄访问FileStream实例以读取和写入数据,而不是磁盘上的文件。

现在,在tar文件中,我们首先必须添加一个tarfile.TarInfo()对象,该对象代表顺序写入的数据的标头,并提供诸如文件名,大小和修改时间之类的信息。

        for file_name,file_size,file_date,file_data in file_data_iterable:
            tar_info = tarfile.TarInfo(file_name)
            tar_info.size = int(file_size)
            tar_info.mtime = file_date
            tar.addfile(tar_info)
            yield stream.pop()

您还可以看到将任何数据传递给该方法的结构。 file_data_iterable是包含以下内容的元组的列表
((str) file_name,(int/str) file_size,(str) unix_timestamp,(bytes) file_data)

发送TarInfo后,将遍历file_data。 此数据需要可迭代。例如,您可以使用通过requests.response检索的requests.get(url,stream=True)对象。

            for chunk in (requests.get(url,stream=True).iter_content(chunk_size=cls.RECORDSIZE)):
                # you can freely choose that chunk size,but this gives me good performance
                tar.fileobj.write(chunk)
                yield stream.pop()

注意:在这里,我使用变量url来请求文件。您将需要在元组参数中传递它而不是file_data。如果您选择传递可迭代的文件,则需要更新此行。

最后,tar文件需要特殊格式来指示文件已完成。 Tarfile由块和记录组成。通常,一个块包含512个字节,一条记录包含20个块(20 * 512字节= 10240字节)。首先,包含最后一块文件数据的最后一块被NUL(通常是纯零)填充,然后下一个文件的下一个TarInfo标头开始。

要结束归档,当前记录将被NUL填充,但是必须至少有两个块完全被NUL填充。 tar.close()将解决此问题。另请参见此Wiki

            blocks,remainder = divmod(tar_info.size,tarfile.BLOCKSIZE)
            if remainder > 0:
                tar.fileobj.write(tarfile.NUL * (tarfile.BLOCKSIZE - remainder))
                yield stream.pop()
                blocks += 1
            tar.offset += blocks * tarfile.BLOCKSIZE
        tar.close()
        yield stream.pop()

您现在可以在Django视图中使用FileStream类:

from django.http import FileResponse
import FileStream

def stream_files(request,files):
    file_data_iterable = [(
        file.name,file.size,file.date.timestamp(),file.data
    ) for file in files]

    response = FileReponse(
        FileStream.yield_tar(file_data_iterable),content_type="application/x-tar"
    )
    response["Content-Disposition"] = 'attachment; filename="streamed.tar"'
    return response

如果要传递tar文件的大小,以便用户可以看到进度条,则可以提前确定未压缩的tar文件的大小。在FileStream类中,添加另一个方法:

    def tarsize(cls,sizes):
        # Each file is preceeded with a 512 byte long header
        header_size = 512
        # Each file will be appended to fill up a block
        tar_sizes = [ceil((header_size + size) / tarfile.BLOCKSIZE)
                     * tarfile.BLOCKSIZE for size in sizes]
        # the end of the archive is marked by at least two consecutive
        # zero filled blocks,and the final record block is filled up with
        # zeros.
        sum_size = sum(tar_sizes)
        remainder = cls.RECORDSIZE - (sum_size % cls.RECORDSIZE)
        if remainder < 2 * tarfile.BLOCKSIZE:
            sum_size += cls.RECORDSIZE
        total_size = sum_size + remainder
        assert total_size % cls.RECORDSIZE == 0
        return total_size

并使用它来设置响应头:

tar_size = FileStream.tarsize([file.size for file in files])
...
response["Content-Length"] = tar_size

非常感谢chipx86allista的要旨为我完成这项任务提供了极大的帮助。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。