Return multiple files from fastapi
Question:
Using fastapi, I can’t figure out how to send multiple files as a response. For example, to send a single file, I’ll use something like this
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")
However, I’m not sure what it looks like to send a list of images. Ideally, I’d like to do something like this:
@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")
However, this returns the error
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'
Answers:
Zipping is the best option that will have same results on all browsers. you can zip files dynamically.
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)
As alternative you can use base64 encoding to embed an (very small) image into json response. but i don’t recommend it.
You can also use MIME/multipart but keep in mind that i was created for email messages and/or POST transmission to the HTTP server. It was never intended to be received and parsed on the client side of a HTTP transaction. Some browsers support it, some others don’t. (so i think you shouldn’t use this either)
I’ve got some problems with @kia’s answer on Python3 and latest fastapi so here is a fix that I got working it includes BytesIO instead of Stringio, fixes for response attribute and removal of top level archive folder
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
@app.get("/image_from_id/")
async def image_from_id(image_id: int):
# Get image from the database
img = ...
return zipfiles(img)
Furthermore, you can create the zip on-the-fly and stream it back to the user using a StreamingResponse
object:
import os
import zipfile
import io
from fastapi.responses import StreamingResponse
zip_subdir = "/some_local_path/of_files_to_compress"
def zipfile(filenames):
zip_io = 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"}
)
Using fastapi, I can’t figure out how to send multiple files as a response. For example, to send a single file, I’ll use something like this
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")
However, I’m not sure what it looks like to send a list of images. Ideally, I’d like to do something like this:
@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")
However, this returns the error
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'
Zipping is the best option that will have same results on all browsers. you can zip files dynamically.
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)
As alternative you can use base64 encoding to embed an (very small) image into json response. but i don’t recommend it.
You can also use MIME/multipart but keep in mind that i was created for email messages and/or POST transmission to the HTTP server. It was never intended to be received and parsed on the client side of a HTTP transaction. Some browsers support it, some others don’t. (so i think you shouldn’t use this either)
I’ve got some problems with @kia’s answer on Python3 and latest fastapi so here is a fix that I got working it includes BytesIO instead of Stringio, fixes for response attribute and removal of top level archive folder
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
@app.get("/image_from_id/")
async def image_from_id(image_id: int):
# Get image from the database
img = ...
return zipfiles(img)
Furthermore, you can create the zip on-the-fly and stream it back to the user using a StreamingResponse
object:
import os
import zipfile
import io
from fastapi.responses import StreamingResponse
zip_subdir = "/some_local_path/of_files_to_compress"
def zipfile(filenames):
zip_io = 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"}
)