Criar zip arquivo para download imediato
Pergunta
Em uma aplicação web que eu estou trabalhando, o usuário pode criar um arquivo zip de uma pasta cheia de arquivos. Aqui aqui está o código:
files = torrent[0].files
zipfile = z.ZipFile(zipname, 'w')
output = ""
for f in files:
zipfile.write(settings.PYRAT_TRANSMISSION_DOWNLOAD_DIR + "/" + f.name, f.name)
downloadurl = settings.PYRAT_DOWNLOAD_BASE_URL + "/" + settings.PYRAT_ARCHIVE_DIR + "/" + filename
output = "Download <a href=\"" + downloadurl + "\">" + torrent_name + "</a>"
return HttpResponse(output)
Mas isso tem o efeito colateral desagradável de uma longa espera (10 + segundos) enquanto o arquivo zip está sendo baixado. É possível ignorar isso? Em vez de salvar o arquivo em um arquivo, é possível enviá-lo diretamente para o usuário?
Eu faço beleive que torrentflux fornece esse recurso excat que estou falando. Ser capaz de zip GBs de dados e baixá-lo dentro de um segundo.
Solução
Outras dicas
Como mandrake diz, construtor do HttpResponse aceita objetos iteráveis.
Felizmente, ZIP formato é tal que arquivo pode ser criado em única passagem, ficha diretório central está localizado no final do arquivo:
(Imagem de Wikipedia )
E, felizmente, zipfile
na verdade não faz qualquer procura, desde que você só adicionar arquivos.
Aqui está o código que eu vim acima com. Algumas notas:
- Eu estou usando este código para fechar um monte de imagens JPEG. Não há nenhum ponto compressão -los, estou usando ZIP apenas como recipiente.
- O uso de memória é O (size_of_largest_file) não O (size_of_archive). E isso é bom o suficiente para mim: muitos arquivos relativamente pequenos que somam potencialmente enorme arquivo
- Este código não define cabeçalho Content-Length, assim o usuário não recebe indicação de progresso agradável. É deve ser possível para calcular isso com antecedência, se são conhecidos tamanhos de todos os arquivos.
- Servindo a ZIP direto para o usuário, como isso significa que currículo em downloads não vai funcionar.
Então, aqui vai:
import zipfile
class ZipBuffer(object):
""" A file-like object for zipfile.ZipFile to write into. """
def __init__(self):
self.data = []
self.pos = 0
def write(self, data):
self.data.append(data)
self.pos += len(data)
def tell(self):
# zipfile calls this so we need it
return self.pos
def flush(self):
# zipfile calls this so we need it
pass
def get_and_clear(self):
result = self.data
self.data = []
return result
def generate_zipped_stream():
sink = ZipBuffer()
archive = zipfile.ZipFile(sink, "w")
for filename in ["file1.txt", "file2.txt"]:
archive.writestr(filename, "contents of file here")
for chunk in sink.get_and_clear():
yield chunk
archive.close()
# close() generates some more data, so we yield that too
for chunk in sink.get_and_clear():
yield chunk
def my_django_view(request):
response = HttpResponse(generate_zipped_stream(), mimetype="application/zip")
response['Content-Disposition'] = 'attachment; filename=archive.zip'
return response
Aqui está uma simples função de visualização Django que fecha-se (como um exemplo) quaisquer arquivos legíveis em /tmp
e retorna o arquivo zip.
from django.http import HttpResponse
import zipfile
import os
from cStringIO import StringIO # caveats for Python 3.0 apply
def somezip(request):
file = StringIO()
zf = zipfile.ZipFile(file, mode='w', compression=zipfile.ZIP_DEFLATED)
for fn in os.listdir("/tmp"):
path = os.path.join("/tmp", fn)
if os.path.isfile(path):
try:
zf.write(path)
except IOError:
pass
zf.close()
response = HttpResponse(file.getvalue(), mimetype="application/zip")
response['Content-Disposition'] = 'attachment; filename=yourfiles.zip'
return response
É claro que esta abordagem só vai funcionar se os arquivos zip irá convenientemente caber na memória - se não, você terá que usar um arquivo de disco (que você está tentando evitar). Nesse caso, basta substituir o file = StringIO()
com file = open('/path/to/yourfiles.zip', 'wb')
e substituir o file.getvalue()
com código para ler o conteúdo do arquivo em disco.
A biblioteca zip você estiver usando permitir a saída para um fluxo. Você poderia transmitir diretamente para o usuário em vez de escrever temporariamente em um arquivo zip ENTÃO streaming para o usuário.
É possível passar um iterador para o construtor de um HttpResponse (ver docs) . Isso permitiria que você criar um iterador personalizado que gera dados como está sendo solicitado. No entanto, eu não acho que isso vai funcionar com um zip (você teria que enviar zip parcial como está sendo criado).
A maneira correta, eu acho, seria criar os arquivos off-line, em um processo separado. O usuário pode, em seguida, monitorar o progresso e, em seguida, baixar o arquivo quando o pronto (possivelmente usando o método iterator descrito acima). Isto seria semelhante que sites como o youtube uso quando você carregar um arquivo e esperar por ele para ser processado.