Flask - Как выдать send_file внутри ответа - PullRequest
0 голосов
/ 16 января 2020

Я кодировал небольшое приложение Flask для загрузки файлов с Google Drive.

@app.route("/downloadFile/<id>")
def downloadFile(id):
    ioBytes, name, mime = gdrive.downloadFile(id)
    return send_file(ioBytes, mime, True, name)

Я использовал метод загрузки из примера здесь , с небольшими изменениями

def downloadFile(self, file_id):
        file = self.drive.files().get(fileId=file_id).execute()
        request = self.drive.files().get_media(fileId=file_id)
        fh = io.BytesIO()
        downloader = MediaIoBaseDownload(fh, request)
        done = False
        while done is False:
            status, done = downloader.next_chunk()
            print("Downloading {} - {}%".format(file.get('name'), int(status.progress() * 100)))
        fh.seek(0)
        return (fh, file.get('name'), file.get('mimeType'))

Он работал как положено и загрузил файл на мой компьютер.

Теперь я хочу развернуть это приложение Flask на Heroku. Моя проблема связана с тайм-аутами HTTP, как указано здесь :

У HTTP-запросов есть начальное 30-секундное окно, в котором веб-процесс должен возвращать данные ответа

Поскольку загрузка некоторых из моих файлов может занять более 30 секунд, это становится большой проблемой.

Я пытался использовать класс Response и оператор yield для продолжения отправки пустых байтов до Я скачал и отправил файл со следующей функцией:

def sendUntilEndOfRequest(func, args=()):
    def thread():
        with app.app_context(), app.test_request_context():
            return func(*args)

    with concurrent.futures.ThreadPoolExecutor() as executor:
        ret = ""
        def exec():
            while ret == "":
                yield ""
                time.sleep(1)
            yield ret
        future = executor.submit(thread)
        def getValue():
            nonlocal ret
            ret = future.result()
        threading.Thread(target=getValue).start()
        return Response(stream_with_context(exec()))

Я попытался сделать его несколько обобщенным c, чтобы, если у меня есть какая-либо другая функция, выполнение которой занимает более 30 секунд, я могу используйте его.

Теперь мой код загрузки

@app.route("/downloadFile/<id>")
def downloadFile(id):
    def downloadAndSendFile():
        ioBytes, name, mime = gdrive.downloadFile(id)
        return send_file(ioBytes, mime, True, name)
    return sendUntilEndOfRequest(downloadAndSendFile)

Но каждый раз, когда я пытаюсь запустить этот код, он выдает эту ошибку:

127.0.0.1 - - [15/Jan/2020 20:38:06] "[37mGET /downloadFile/1heeoEBZrhW0crgDSLbhLpcyMfvXqSmqi HTTP/1.1[0m" 200 -
Error on request:
Traceback (most recent call last):
  File "C:\Users\fsvic\AppData\Local\Programs\Python\Python37\lib\site-packages\werkzeug\serving.py", line 303, in run_wsgi
    execute(self.server.app)
  File "C:\Users\fsvic\AppData\Local\Programs\Python\Python37\lib\site-packages\werkzeug\serving.py", line 294, in execute
    write(data)
  File "C:\Users\fsvic\AppData\Local\Programs\Python\Python37\lib\site-packages\werkzeug\serving.py", line 274, in write
    assert isinstance(data, bytes), "applications must write bytes"
AssertionError: applications must write bytes

Видимо, файл загружается правильно. Я проверил замену send_file на команду render_template, чтобы проверить, возможно ли получение объектов flask, и это работало отлично. Я также протестировал возвращаемые строки, и это также сработало.

В конце концов, как я могу получить загруженный файл?

1 Ответ

1 голос
/ 16 января 2020

Все MediaIoBaseDownload вызывает метод write обработчика файла. Таким образом, вы можете реализовать свой собственный ввод-вывод следующим образом:

import io

from googleapiclient import discovery
from httplib2 import Http
from oauth2client import file, client, tools
from googleapiclient.http import MediaIoBaseDownload

from flask import Flask
from flask import Response

app = Flask(__name__)


SCOPES = 'https://www.googleapis.com/auth/drive.readonly'
store = file.Storage('storage.json')
creds = store.get()
if not creds or creds.invalid:
    flow = client.flow_from_clientsecrets('client_id.json', SCOPES)
    creds = tools.run_flow(flow, store)
drive_service = discovery.build('drive', 'v3', http=creds.authorize(Http()))


class ChunkHolder(object):

    def __init__(self):
        self.chunk = None

    def write(self, chunk):
        """Save current chunk"""
        self.chunk = chunk


@app.route('/<file_id>')
def download_file(file_id):
    request = drive_service.files().get_media(fileId=file_id)

    def download_stream():
        done = False
        fh = ChunkHolder()
        downloader = MediaIoBaseDownload(fh, request)
        while not done:
            status, done = downloader.next_chunk()
            print("Download %d%%." % int(status.progress() * 100))
            yield fh.chunk

    return Response(download_stream())


if __name__ == '__main__':
    app.run(port=5000)

Мы выдаем загруженные чанки сразу после их загрузки и не сохраняем предыдущие чанки в памяти.

...