#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, чьи идеи очень помогли мне с этой задачей.