Книга: FastAPI: веб-разработка на Python
Назад: Глава 14. Базы данных, наука о данных и немного искусственного интеллекта
Дальше: Глава 16. Формы и шаблоны

Глава 15. Файлы

Обзор

Помимо обработки API-запросов и традиционного контента, например HTML, веб-серверы должны обрабатывать передачу файлов в обоих направлениях. Очень большие файлы могут передаваться частями, чтобы не занимать много памяти системы. Вы также можете предоставить доступ к папке с файлами (и подчиненными папками любой глубины) с помощью статических файлов — Static Files.

Поддержка Multipart

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

Python-Multipart (https://oreil.ly/FUBk7) — pip install python-multipart;

aio-files (https://oreil.ly/OZYYR) — pip install aiofiles.

Выгрузка файлов

FastAPI нацелен на разработку API, и в большинстве примеров в этой книге используются запросы и ответы в формате JSON. Но в следующей главе вы познакомитесь с формами, которые обрабатываются по-другому. Здесь же рассказывается о файлах, по некоторым параметрам похожих на формы.

FastAPI предлагает два способа выгрузки файлов: функцию File() и класс UploadFile.

Функция File()

Функция File() применяется в качестве типа для прямой выгрузки файла. Ваша функция пути может быть синхронной (def) или асинхронной (async def), но асинхронная версия лучше, потому что она не будет нагружать веб-сервер во время выгрузки файла.

FastAPI будет извлекать файл по частям и собирать его в памяти, поэтому функцию File() следует использовать только для относительно небольших файлов. Вместо того чтобы считать, что входные данные представлены в формате JSON, FastAPI кодирует файл как элемент формы.

Напишем код для запроса файла и протестируем его. Вы можете взять любой файл на своей машине для тестирования или загрузить его с такого сайта, как Fastest Fish (https://oreil.ly/EnlH-). Я взял оттуда файл размером 1 Кбайт и сохранил его локально под названием 1KB.bin. В примере 15.1 добавьте эти строки в файл main.py верхнего уровня.

Пример 15.1. Обработка выгрузки небольшого файла с помощью FastAPI

from fastapi import File

@app.post("/small")

async def upload_small_file(small_file: bytes = File()) -> str:

    return f"file size: {len(small_file)}"

После перезапуска Uvicorn попробуйте выполнить HTTPie-тест в примере 15.2.

Пример 15.2. Выгрузка небольшого файла с помощью HTTPie

$ http -f -b POST http://localhost:8000/small [email protected]

"file size: 1000"

Приведу несколько замечаний по этому тесту.

• Необходимо добавить аргумент -f или --form, потому что файлы выгружаются как формы, а не как JSON-текст.

[email protected]:

• small_FILE соответствует имени переменной small_file в функции пути FastAPI в примере 15.1;

• @ — сокращение HTTPie для создания формы;

• 1KB.bin — выгружаемый файл.

Пример 15.3 представляет собой эквивалентный программный тест.

Пример 15.3. Выгрузка небольшого файла с помощью Requests

$ python

>>> import requests

>>> url = "http://localhost:8000/small"

>>> files = {'small_file': open('1KB.bin', 'rb')}

>>> resp = requests.post(url, files=files)

>>> print(resp.json())

file size: 1000

Класс UploadFile

Для больших файлов лучше использовать класс UploadFile. Он создает объект Python под названием SpooledTemporary File, обычно на диске сервера, а не в памяти. Это файлоподобный объект Python, и он поддерживает методы read(), write() и seek(). Пример 15.4 показывает реализацию этого подхода; в нем также используется объявление async def вместо def, чтобы избежать блокировки веб-сервера во время выгрузки частей файла.

Пример 15.4. Выгрузка большого файла с помощью FastAPI (добавить к файлу main.py)

from fastapi import UploadFile

@app.post("/big")

async def upload_big_file(big_file: UploadFile) -> str:

    return f"file size: {big_file.size}, name: {big_file.filename}"

Функция File() создает объект bytes и нуждается в круглых скобках. UploadFile — это объект другого класса.

Если стартер Uvicorn еще не износился, значит, пришло время испытаний. На этот раз в примерах 15.5 и 15.6 используется файл размером 1 Гбайт (1GB.bin), я взял его на сайте Fastest.Fish.

Пример 15.5. Тестирование выгрузки большого файла с помощью HTTPie

$ http -f -b POST http://localhost:8000/big [email protected]

"file size: 1000000000, name: 1GB.bin"

Пример 15.6. Тестирование выгрузки большого файла с помощью Requests

>>> import requests

>>> url = "http://localhost:8000/big"

>>> files = {'big_file': open('1GB.bin', 'rb')}

>>> resp = requests.post(url, files=files)

>>> print(resp.json())

file size: 1000000000, name: 1GB.bin

Загрузка файлов

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

Класс FileResponse

Первым (пример 15.7) представлен вариант «все и сразу», класс FileRes­ponse.

Пример 15.7. Загрузка небольшого файла с помощью FileResponse (добавить в файл main.py)

from fastapi.responses import FileResponse

@app.get("/small/{name}")

async def download_small_file(name):

    return FileResponse(name)

Где-то здесь есть тест. Сначала поместите файл 1KB.bin в тот же каталог, что и main.py. Теперь запустите пример 15.8.

Пример 15.8. Загрузка небольшого файла с помощью HTTPie

$ http -b http://localhost:8000/small/1KB.bin

-----------------------------------------

| NOTE: binary data not shown in terminal |

-----------------------------------------

Если вы не доверяете этому сообщению об ограничениях, то пример 15.9 направляет вывод в утилиту типа wc, чтобы убедиться, что вы получили все 1000 байт.

Пример 15.9. Загрузка небольшого файла с помощью HTTPie с подсчетом байтов

$ http -b http://localhost:8000/small/1KB.bin | wc -c

    1000

Класс StreamingResponse

Как и в случае с модулем FileUpload, большие файлы лучше загружать с помощью класса StreamingResponse, возвращающего файл по частям. Пример 15.10 показывает такой подход к реализации с помощью функции пути, определенной как async def. Он позволяет избежать блокировки, когда процессор не используется. Я пока пропускаю проверку ошибок. Если файла path не существует, вызов функции open() выбросит исключение.

Пример 15.10. Возврат большого файла с помощью класса StreamingResponse (добавить в файл main.py)

from pathlib import path

from typing import Generator

from fastapi.responses import StreamingResponse

def gen_file(path: str) -> Generator:

    with open(file=path, mode="rb") as file:

        yield file.read()

@app.get("/download_big/{name}")

async def download_big_file(name:str):

    gen_expr = gen_file(file_path=path)

    response = StreamingResponse(

        content=gen_expr,

        status_code=200,

    )

    return response

gen_exprвыражение-генератор, возвращаемое функцией-генератором gen_file(). Класс StreamingResponse использует его для своего итерируемого аргумента content, чтобы загружать файл по частям.

Пример 15.11 представляет собой сопутствующий тест. (Для этого сначала потребуется разместить файл 1GB.bin рядом с файлом main.py, процесс займет немного больше времени.)

Пример 15.11. Загрузка большого файла с помощью HTTPie

$ http -b http://localhost:8000/big/1GB.bin | wc -c

1000000000

Предоставление статических файлов

Традиционные веб-серверы могут обращаться с файлами сервера так, как будто они находятся в обычной файловой системе. FastAPI позволяет делать это с помощью класса StaticFiles.

Для этого примера создадим каталог скучных бесплатных файлов для загрузки пользователями.

• Создайте каталог static на том же уровне, что и файл main.py. (У этого хранилища может быть любое название, я называю его static (статическим) только для того, чтобы не забыть, зачем я его сделал.)

• Поместите в него текстовый файл abc.txt с текстовым содержимым abc :).

Пример 15.12 предоставит любой URL, начинающийся с выражения /static (вы могли бы использовать здесь любую текстовую строку), с файлами из каталога static.

Пример 15.12. Предоставление всего содержимого в каталоге с помощью StaticFiles (добавить в файл main.py)

from pathlib import Path

from fastapi import FastAPI

from fastapi.staticfiles import StaticFiles

# Каталог, содержащий файл main.py:

top = Path(__file__).resolve.parent

app.mount("/static",

    StaticFiles(directory=f"{top}/static", html=True),

    name="free")

Расчет top гарантирует, что вы разместите каталог static радом с файлом main.py. Переменная __file__ представляет собой полное имя пути к этому файлу.

Пример 15.13 — это один из способов проверки примера 15.12 вручную.

Пример 15.13. Получение статического файла

$ http -b localhost:8000/static/abc.txt

abc :)

А что насчет аргумента html=True, который мы передаем в функцию StaticFiles()? Это делает ее работу немного более похожей на работу традиционного сервера, возвращая файл index.html, если он существует в этом каталоге, но вы не запрашивали файл index.html в явном виде в URL. Итак, создадим в каталоге static файл index.html с содержимым Oh. Hi!, а затем протестируем работу кода в примере 15.14.

Пример 15.14. Получение файла index.html из каталога /static

$ http -b localhost:8000/static/

Oh. Hi!

У вас может быть любое необходимое количество файлов (и подкаталогов с файлами и т.д.). Создайте подкаталог xyz в каталоге static и поместите туда два файла:

xyx.txt — содержит текст: xyz :(;

index.html — содержит текст How did you find me?.

Я не буду приводить здесь примеры. Попробуйте запустить их сами (надеюсь, у вас более богатое воображение).

Заключение

В этой главе было показано, как выгружать и скачивать файлы — маленькие, большие и даже гигантские. Кроме того, вы научились предоставлять статические файлы в ностальгическом (не API) веб-стиле из каталога.

Назад: Глава 14. Базы данных, наука о данных и немного искусственного интеллекта
Дальше: Глава 16. Формы и шаблоны