Reputation: 88198
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'
Upvotes: 11
Views: 25980
Reputation: 863
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"}
)
Upvotes: 11
Reputation: 1414
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)
Upvotes: 11
Reputation: 171
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)
Upvotes: 10