Помимо обработки API-запросов и традиционного контента, например HTML, веб-серверы должны обрабатывать передачу файлов в обоих направлениях. Очень большие файлы могут передаваться частями, чтобы не занимать много памяти системы. Вы также можете предоставить доступ к папке с файлами (и подчиненными папками любой глубины) с помощью статических файлов — Static Files.
Чтобы обрабатывать большие файлы, функции выгрузки и скачивания 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() применяется в качестве типа для прямой выгрузки файла. Ваша функция пути может быть синхронной (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-текст.
• 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. Он создает объект 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
К сожалению, гравитация не ускоряет скачивание файлов. Вместо этого мы будем использовать эквиваленты методов выгрузки.
Первым (пример 15.7) представлен вариант «все и сразу», класс FileResponse.
Пример 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
Как и в случае с модулем 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) веб-стиле из каталога.