HTTP Загрузить очень большой файл - PullRequest
14 голосов
/ 08 октября 2009

Я работаю над веб-приложением на 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 ... Что-то вроде размера файла?

Ответы [ 4 ]

35 голосов
/ 01 ноября 2009

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

while r != '':
    r = fp.read(1024)
    request.write(r)

Помните, что Twisted использует совместную многозадачность для достижения любого вида параллелизма. Итак, первая проблема с этим фрагментом состоит в том, что это цикл 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 и других связанных с этим идеях из серии «Витая сеть за 60 секунд» в моем блоге, http://jcalderone.livejournal.com/50562.html (см., В частности, записи «Асинхронные ответы»).

3 голосов
/ 09 октября 2009

Если это действительно 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, сделайте то же самое, но сначала без gzip.

В качестве альтернативы, если у вас есть 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)
3 голосов
/ 08 октября 2009

Да, заголовок Content-Length даст вам индикатор выполнения, который вы желаете!

0 голосов
/ 04 декабря 2011

Вот пример загрузки файлов по частям с помощью 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
...