HTTP Загрузить очень большой файл
Я работаю над веб-приложением в Python/Twisted.
Я хочу, чтобы пользователь мог загрузить очень большой файл ( > 100 Мб). Конечно, я не хочу загружать весь файл в память (на сервере).
серверная сторона У меня есть эта идея:
...
request.setHeader('Content-Type', 'text/plain')
fp = open(fileName, 'rb')
try:
r = None
while r != '':
r = fp.read(1024)
request.write(r)
finally:
fp.close()
request.finish()
Я ожидал, что это сработает, но у меня проблемы:
Я тестирую FF... Кажется, браузер заставляет меня ждать, пока файл не будет загружен, а затем у меня появится диалоговое окно "Открыть/сохранить".
Я ожидал диалоговое окно сразу, а затем индикатор выполнения в действии...
Возможно, мне нужно добавить что-то в заголовок Http... Что-то вроде размера файла?
Ответы
Ответ 1
Две большие проблемы с кодом, который вы опубликовали, это то, что он несовместим и загружает весь файл в память перед его отправкой.
while r != '':
r = fp.read(1024)
request.write(r)
Помните, что Twisted использует совместную многозадачность для достижения любого типа concurrency. Итак, первая проблема с этим фрагментом заключается в том, что это цикл while над содержимым целого файла (который, как вы говорите, большой). Это означает, что весь файл будет считываться в память и записываться в ответ, прежде чем что-то еще может произойти в этом процессе. В этом случае бывает, что "что-либо" также включает в себя толкание байтов из буфера в памяти в сеть, поэтому ваш код будет одновременно хранить весь файл в памяти и только начнет избавляться от него, когда этот цикл завершается.
Итак, как правило, вы не должны писать код для использования в приложении на основе Twisted, которое использует такой цикл, чтобы выполнить большую работу. Вместо этого вам нужно сделать каждую маленькую часть большой работы таким образом, чтобы она взаимодействовала с циклом события. Для отправки файла по сети лучший способ приблизиться к этому - с производителями и потребителями. Это два связанных API для перемещения больших объемов данных вокруг использования событий с пустым буфером, чтобы сделать это эффективно и не тратить необоснованные объемы памяти.
Здесь вы можете найти документацию по этим API:
http://twistedmatrix.com/projects/core/documentation/howto/producers.html
К счастью, для этого очень распространенного случая есть еще и продюсер, уже написанный, который вы можете использовать, а не реализовать свой собственный:
http://twistedmatrix.com/documents/current/api/twisted.protocols.basic.FileSender.html
Вероятно, вы захотите использовать его вроде:
from twisted.protocols.basic import FileSender
from twisted.python.log import err
from twisted.web.server import NOT_DONE_YET
class Something(Resource):
...
def render_GET(self, request):
request.setHeader('Content-Type', 'text/plain')
fp = open(fileName, 'rb')
d = FileSender().beginFileTransfer(fp, request)
def cbFinished(ignored):
fp.close()
request.finish()
d.addErrback(err).addCallback(cbFinished)
return NOT_DONE_YET
Вы можете узнать больше о NOT_DONE_YET
и других связанных с ним идеях серии "Twisted Web in 60 Seconds" в моем блоге http://jcalderone.livejournal.com/50562.html (в частности, см. записи "асинхронные ответы" ).
Ответ 2
Да, заголовок Content-Length даст вам индикатор выполнения, который вы хотите!
Ответ 3
Если это действительно text/plain
контент, вы должны серьезно подумать о том, чтобы отправить его с помощью Content-Encoding: gzip
, когда клиент указывает, что он может с ним справиться. Вы должны видеть огромную экономию полосы. Кроме того, если это статический файл, то вы действительно хотите использовать sendfile(2)
. Что касается браузеров, которые не выполняют то, что вы ожидаете от загрузки, вы можете посмотреть заголовок Content-Disposition
. Так или иначе, логика выглядит следующим образом:
Если клиент указывает, что он может обрабатывать кодировку gzip
через заголовок Accept-Encoding
(например, Accept-Encoding: compress;q=0.5, gzip;q=1.0
или Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0
или аналогичный), затем сжимать файл, кэшировать сжатый результат где-то, записывать правильные заголовки для ответа (Content-Encoding: gzip
, Content-Length: n
, Content-Type: text/plain
и т.д.), а затем используйте sendfile(2)
(однако это может быть или не быть доступно в вашей среде) для копирования содержимого из дескриптора открытого файла в поток ответов.
Если они не принимают gzip
, выполните одно и то же, но без gzipping.
В качестве альтернативы, если у вас есть Apache, Lighttpd или подобное, действующее как прозрачный прокси-сервер перед вашим сервером, вы можете использовать заголовок X-Sendfile
, который чрезвычайно быстро:
response.setHeader('Content-Type', 'text/plain')
response.setHeader(
'Content-Disposition',
'attachment; filename="' + os.path.basename(fileName) + '"'
)
response.setHeader('X-Sendfile', fileName)
response.setHeader('Content-Length', os.stat(fileName).st_size)
Ответ 4
Ниже приведен пример загрузки файлов в куски с использованием urllib2, который вы можете использовать изнутри вызова функции скрученной функции
import os
import urllib2
import math
def downloadChunks(url):
"""Helper to download large files
the only arg is a url
this file will go to a temp directory
the file will also be downloaded
in chunks and print out how much remains
"""
baseFile = os.path.basename(url)
#move the file to a more uniq path
os.umask(0002)
temp_path = "/tmp/"
try:
file = os.path.join(temp_path,baseFile)
req = urllib2.urlopen(url)
total_size = int(req.info().getheader('Content-Length').strip())
downloaded = 0
CHUNK = 256 * 10240
with open(file, 'wb') as fp:
while True:
chunk = req.read(CHUNK)
downloaded += len(chunk)
print math.floor( (downloaded / total_size) * 100 )
if not chunk: break
fp.write(chunk)
except urllib2.HTTPError, e:
print "HTTP Error:",e.code , url
return False
except urllib2.URLError, e:
print "URL Error:",e.reason , url
return False
return file