Поток нескольких файлов одновременно с сервера Django

#python #django #django-views #zip #tar

#python #django #django-просмотры #zip #tar

Вопрос:

Я использую сервер Django для обслуживания файлов с другого сервера в защищенной сети. Когда пользователь делает запрос на одновременный доступ к нескольким файлам, я бы хотел, чтобы мой сервер Django передавал эти файлы сразу этому пользователю.

Поскольку одновременная загрузка нескольких файлов в браузере затруднена, файлы должны быть каким-то образом объединены. Я не хочу, чтобы мой сервер сначала загружал все файлы, а затем отправлял готовый файл в комплекте, потому что это увеличивает временные потери для файлов большего размера. Насколько я понимаю, с помощью zip-файлов он не может передаваться в потоковом режиме во время сборки.

Есть ли какой-либо способ начать потоковую передачу контейнера, как только будут доступны первые байты с удаленного сервера?

Ответ №1:

Tar-файлы созданы для сбора нескольких файлов в один архив. Они были разработаны для магнитофонов, поэтому предлагают последовательную запись и чтение.

С помощью Django можно передавать файлы в браузер с FileResponse() помощью, который может принимать генератор в качестве аргумента.

Если мы передадим ему генератор, который собирает tar-файл с данными, запрошенными пользователем, tar-файл будет сгенерирован точно в срок. Однако встроенный tarfile модуль pythons не предлагает такой возможности из коробки.

Однако мы можем использовать tarfile возможность передавать файлоподобный объект для самостоятельной сборки архива. Поэтому мы могли бы создать BytesIO() объект, в который будет постепенно записываться tarfile, и сбросить его содержимое в FileResponse() метод Django. Для того, чтобы это работало, нам нужно реализовать несколько методов, к которым 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 аргументов кортежа. Если вы решите передать итеративный файл, вам нужно будет обновить эту строку.

Наконец, файл tarfile требует специального формата, чтобы указать, что файл завершен. Tarfiles состоят из блоков и записей. Обычно блок содержит 512 байт, а запись содержит 20 блоков (20 * 512 байт = 10240 байт). Сначала последний блок, содержащий последний фрагмент данных файла, заполняется NUL (обычно простыми нулями), затем начинается следующий заголовок TarInfo следующего файла.

Для завершения архива текущая запись будет заполнена NUL, но должно быть не менее двух блоков, полностью заполненных NUL. Об этом позаботится tar.close() . Также смотрите Эту Вики.

             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()
  

Теперь вы можете использовать FileStream класс в своем представлении Django:

 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
  

Огромное спасибо chipx86 и allista, чьи идеи очень помогли мне с этой задачей.