Вопрос-Ответ

How to Upload File using FastAPI?

Как загрузить файл с помощью FastAPI?

Я использую FastAPI для загрузки файла в соответствии с официальной документацией, как показано ниже:

@app.post("/create_file")
async def create_file(file: UploadFile = File(...)):
file2store = await file.read()
# some code to store the BytesIO(file2store) to the other database

Когда я отправляю запрос с использованием библиотеки запросов Python, как показано ниже:

f = open(".../file.txt", 'rb')
files = {"file": (f.name, f, "multipart/form-data")}
requests.post(url="SERVER_URL/create_file", files=files)

переменная file2store всегда пуста. Иногда (редко встречается) она может получать байты файла, но почти все время она пуста, поэтому я не могу восстановить файл в другой базе данных.

Я также пробовал bytes вместо UploadFile, но получаю те же результаты. Что-то не так с моим кодом, или я неправильно использую FastAPI для загрузки файла?

Переведено автоматически
Ответ 1

Сначала, согласно документации FastAPI, вам необходимо установить python-multipart — если вы еще этого не сделали — поскольку загруженные файлы отправляются как "данные формы". Например:

pip install python-multipart

В приведенных ниже примерах используется .file атрибут UploadFile объекта для получения фактического файла Python (т.Е. SpooledTemporaryFile), который позволяет вам вызывать SpooledTemporaryFileметоды, такие как .read() и .close(), без необходимости await их использовать. Однако важно определить вашу конечную точку с помощью def в этом случае — в противном случае такие операции будут блокировать сервер до их завершения, если конечная точка была определена с помощью async def. В FastAPI обычная def конечная точка запускается во внешнем пуле потоков, который затем ожидается, вместо того, чтобы вызываться напрямую (поскольку это заблокировало бы сервер).

У SpooledTemporaryFile, используемого FastAPI / Starlette, есть max_size атрибут, равный 1 МБ, означающий, что данные сохраняются в памяти до тех пор, пока размер файла не превысит 1 МБ, после чего данные записываются во временный файл на диске, в каталоге temp операционной системы. Следовательно, если вы загрузили файл размером более 1 МБ, он не будет сохранен в памяти, и вызов file.file.read() фактически считал бы данные с диска в память. Таким образом, если файл слишком велик, чтобы поместиться в оперативной памяти вашего сервера, вам лучше читать файл порциями и обрабатывать по одному фрагменту за раз, как описано в разделе "Чтение файла порциями" ниже. Вы также можете ознакомиться с этим ответом, который демонстрирует другой подход к загрузке большого файла порциями, используя .stream() метод Starlette и streaming-form-data пакет, который позволяет анализировать потоковые multipart/form-data фрагменты, что приводит к значительному сокращению времени, необходимого для загрузки файлов.

Если вам нужно определить свою конечную точку с помощью async def— как вам может понадобиться await для некоторых других сопрограмм внутри вашего маршрута — тогда вам лучше использовать асинхронное чтение и запись содержимого, как показано в этом ответе. Более того, если вам нужно отправить дополнительные данные (например, JSON данные) вместе с загрузкой файла (ов), пожалуйста, взгляните на этот ответ. Я бы также предложил вам взглянуть на этот ответ, который объясняет разницу между def и async def конечными точками.

Загрузить один файл

app.py

from fastapi import File, UploadFile

@app.post("/upload")
def upload(file: UploadFile = File(...)):
try:
contents = file.file.read()
with open(file.filename, 'wb') as f:
f.write(contents)
except Exception:
return {"message": "There was an error uploading the file"}
finally:
file.file.close()

return {"message": f"Successfully uploaded {file.filename}"}
Считывать файл по частям

Как описано ранее и в этом ответе, если файл слишком велик, чтобы поместиться в память — например, если у вас 8 ГБ оперативной памяти, вы не сможете загрузить файл объемом 50 ГБ (не говоря уже о том, что доступная оперативная память всегда будет меньше общего объема, установленного на вашем компьютере, поскольку другие приложения будут использовать часть оперативной памяти) — вам следует загружать файл в память порциями и обрабатывать данные по одной порции за раз. Однако выполнение этого метода может занять больше времени, в зависимости от выбранного вами размера фрагмента — в примере ниже размер фрагмента составляет 1024 * 1024 байт (т.е. 1 МБ). Вы можете настроить размер фрагмента по желанию.

from fastapi import File, UploadFile

@app.post("/upload")
def upload(file: UploadFile = File(...)):
try:
with open(file.filename, 'wb') as f:
while contents := file.file.read(1024 * 1024):
f.write(contents)
except Exception:
return {"message": "There was an error uploading the file"}
finally:
file.file.close()

return {"message": f"Successfully uploaded {file.filename}"}

Другим вариантом было бы использовать shutil.copyfileobj(), который используется для копирования содержимого file-like объекта в другой file-like объект (также взгляните на этот ответ). По умолчанию данные считываются порциями, при этом размер буфера (чанка) по умолчанию равен 1 МБ (т.Е. 1024 * 1024 Байтам) для Windows и 64 КБ для других платформ, как показано в исходном коде здесь. Вы можете указать размер буфера, передав необязательный параметр length. Примечание: Если передано отрицательное length значение, вместо него будет прочитано все содержимое файла — смотрите f.read() также, который .copyfileobj() использует under the hoodle (как можно видеть в исходном коде здесь).

from fastapi import File, UploadFile
import shutil

@app.post("/upload")
def upload(file: UploadFile = File(...)):
try:
with open(file.filename, 'wb') as f:
shutil.copyfileobj(file.file, f)
except Exception:
return {"message": "There was an error uploading the file"}
finally:
file.file.close()

return {"message": f"Successfully uploaded {file.filename}"}

test.py

import requests

url = 'http://127.0.0.1:8000/upload'
file = {'file': open('images/1.png', 'rb')}
resp = requests.post(url=url, files=file)
print(resp.json())

<form>Пример HTML смотрите здесь.

Загрузить несколько файлов (список)

app.py

from fastapi import File, UploadFile
from typing import List

@app.post("/upload")
def upload(files: List[UploadFile] = File(...)):
for file in files:
try:
contents = file.file.read()
with open(file.filename, 'wb') as f:
f.write(contents)
except Exception:
return {"message": "There was an error uploading the file(s)"}
finally:
file.file.close()

return {"message": f"Successfuly uploaded {[file.filename for file in files]}"}
Считывать файлы по частям

Как описано ранее в этом ответе, если вы ожидаете получить довольно большой файл (ы) и у вас недостаточно оперативной памяти для размещения всех данных от начала до конца, вам лучше загружать файл в память порциями, таким образом обрабатывая данные по одному блоку за раз (Примечание: отрегулируйте размер блока по желанию, ниже это 1024 * 1024 байты).

from fastapi import File, UploadFile
from typing import List

@app.post("/upload")
def upload(files: List[UploadFile] = File(...)):
for file in files:
try:
with open(file.filename, 'wb') as f:
while contents := file.file.read(1024 * 1024):
f.write(contents)
except Exception:
return {"message": "There was an error uploading the file(s)"}
finally:
file.file.close()

return {"message": f"Successfuly uploaded {[file.filename for file in files]}"}

или, используя shutil.copyfileobj():

from fastapi import File, UploadFile
from typing import List
import shutil

@app.post("/upload")
def upload(files: List[UploadFile] = File(...)):
for file in files:
try:
with open(file.filename, 'wb') as f:
shutil.copyfileobj(file.file, f)
except Exception:
return {"message": "There was an error uploading the file(s)"}
finally:
file.file.close()

return {"message": f"Successfuly uploaded {[file.filename for file in files]}"}

test.py

import requests

url = 'http://127.0.0.1:8000/upload'
files = [('files', open('images/1.png', 'rb')), ('files', open('images/2.png', 'rb'))]
resp = requests.post(url=url, files=files)
print(resp.json())

<form>Пример HTML смотрите здесь.

Ответ 2
@app.post("/create_file/")
async def image(image: UploadFile = File(...)):
print(image.file)
# print('../'+os.path.isdir(os.getcwd()+"images"),"*************")
try:
os.mkdir("images")
print(os.getcwd())
except Exception as e:
print(e)
file_name = os.getcwd()+"/images/"+image.filename.replace(" ", "-")
with open(file_name,'wb+') as f:
f.write(image.file.read())
f.close()
file = jsonable_encoder({"imagePath":file_name})
new_image = await add_image(file)
return {"filename": new_image}
python