Вернуть несколько файлов из fastapi

Используя fastapi, я не могу понять, как отправить несколько файлов в качестве ответа. Например, чтобы отправить один файл, я буду использовать что-то вроде этого

from fastapi import FastAPI, Response

app = FastAPI()

@app.get("/image_from_id/")
async def image_from_id(image_id: int):

    # Get image from the database
    img = ...
    return Response(content=img, media_type="application/png")

Однако я не уверен, как выглядит отправка списка изображений. В идеале я хотел бы сделать что-то вроде этого:

@app.get("/images_from_ids/")
async def image_from_id(image_ids: List[int]):

    # Get a list of images from the database
    images = ...
    return Response(content=images, media_type="multipart/form-data")

Однако это возвращает ошибку

    def render(self, content: typing.Any) -> bytes:
        if content is None:
            return b""
        if isinstance(content, bytes):
            return content
>       return content.encode(self.charset)
E       AttributeError: 'list' object has no attribute 'encode'

person Hooked    schedule 11.04.2020    source источник
comment
Не уверен, но если content является типом List, то содержимое цикла: for c in content: c.encode() ...   -  person felipsmartins    schedule 11.04.2020
comment
@felipsmartins объекты в списке уже являются байтами, запуск img.encode() на них не работает 'bytes' object has no attribute 'encode'   -  person Hooked    schedule 11.04.2020


Ответы (3)


Архивирование — лучший вариант, который будет иметь одинаковые результаты во всех браузерах. вы можете архивировать файлы динамически.

import os
import zipfile
import StringIO


def zipfiles(filenames):
    zip_subdir = "archive"
    zip_filename = "%s.zip" % zip_subdir

    # Open StringIO to grab in-memory ZIP contents
    s = StringIO.StringIO()
    # The zip compressor
    zf = zipfile.ZipFile(s, "w")

    for fpath in filenames:
        # Calculate path for file in zip
        fdir, fname = os.path.split(fpath)
        zip_path = os.path.join(zip_subdir, fname)

        # Add file, at correct path
        zf.write(fpath, zip_path)

    # Must close zip for all contents to be written
    zf.close()

    # Grab ZIP file from in-memory, make response with correct MIME-type
    resp = Response(s.getvalue(), mimetype = "application/x-zip-compressed")
    # ..and correct content-disposition
    resp['Content-Disposition'] = 'attachment; filename=%s' % zip_filename

    return resp


@app.get("/image_from_id/")
async def image_from_id(image_id: int):

    # Get image from the database
    img = ...
    return zipfiles(img)

В качестве альтернативы вы можете использовать кодировку base64 для встраивания (очень маленького) изображения в ответ json. но я не рекомендую это.

Вы также можете использовать MIME/multipart, но имейте в виду, что i был создан для сообщений электронной почты и/или POST-передачи на HTTP-сервер. Он никогда не предназначался для получения и анализа на стороне клиента HTTP-транзакции. Некоторые браузеры поддерживают это, некоторые нет. (поэтому я думаю, что вы не должны использовать это тоже)

person kia    schedule 11.04.2020
comment
Привет @kia, и спасибо за ответ! Переполнение стека не поощряет ответы с одной ссылкой (и в результате за него могут проголосовать). Ответ был бы лучше, если бы вы могли предоставить небольшой пример, показывающий, как использовать aiofiles в этом конкретном контексте. - person Hooked; 11.04.2020
comment
Привет @Hooked и @kia, если я что-то не упустил, архивирование файлов, как в вашем примере, является блокирующей операцией ввода-вывода, которая может испортить асинхронный цикл событий под капотом. Рассмотрите возможность создания синхронного обработчика (удалите async в определении image_for_id или рассмотрите возможность запуска функции zipfiles под управлением TheadPoolExecutor, как в этом примере: docs.python.org/3/library/ - person glenfant; 19.05.2020
comment
@glenfant Можете ли вы сказать мне, почему блокирующая операция ввода-вывода может испортить асинхронный цикл обработки событий под капотом? благодарю вас. - person kia; 20.05.2020

У меня есть некоторые проблемы с ответом @kia на Python3 и последней версией fastapi, поэтому вот исправление, которое я заработал, оно включает BytesIO вместо Stringio, исправления для атрибута ответа и удаление папки архива верхнего уровня.

import os
import zipfile
import io


def zipfiles(filenames):
    zip_filename = "archive.zip"

    s = io.BytesIO()
    zf = zipfile.ZipFile(s, "w")

    for fpath in filenames:
        # Calculate path for file in zip
        fdir, fname = os.path.split(fpath)

        # Add file, at correct path
        zf.write(fpath, fname)

    # Must close zip for all contents to be written
    zf.close()

    # Grab ZIP file from in-memory, make response with correct MIME-type
    resp = Response(s.getvalue(), media_type="application/x-zip-compressed", headers={
        'Content-Disposition': f'attachment;filename={zip_filename}'
    })

    return resp
person vozman    schedule 05.03.2021

Кроме того, вы можете создать zip-файл «на лету» и передать его пользователю с помощью объекта StreamingResponse:

import os
import zipfile
import StringIO
from fastapi.responses import StreamingResponse

def zipfile(filenames):
    zip_io = BytesIO()
    with zipfile.ZipFile(zip_io, mode='w', compression=zipfile.ZIP_DEFLATED) as temp_zip:
        for fpath in filenames:
            # Calculate path for file in zip
            fdir, fname = os.path.split(fpath)
            zip_path = os.path.join(zip_subdir, fname)
            # Add file, at correct path
            temp_zip.write((fpath, zip_path))
    return StreamingResponse(
        iter([zip_io.getvalue()]), 
        media_type="application/x-zip-compressed", 
        headers = { "Content-Disposition": f"attachment; filename=images.zip"}
    )
person shleimel    schedule 05.07.2021