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