Estoy creando un punto final de FastAPI donde el usuario del cliente web puede esencialmente descargar archivos que están almacenados en MongoDB como fragmentos de GridFS. Sin embargo, StreamingResponse de FastAPI no toma el objeto AsyncIOMotorGridOut supuestamente similar a un archivo devuelto por el método open_download_stream de motor.
Ya tengo un punto final que puede tomar archivos en un formulario y hacer que se carguen en MongoDB. Esperaría que una función auxiliar de descarga similar fuera tan simple como esta:
async def upload_file(db, file: UploadFile): """ Uploads file to MongoDB GridFS file system and returns ID to be stored with collection document """ fs = AsyncIOMotorGridFSBucket(db) file_id = await fs.upload_from_stream( file.filename, file.file, # chunk_size_bytes=255*1024*1024, #default 255kB metadata={"contentType": file.content_type}) return file_idMi primer intento es usar un ayudante como este:
async def download_file(db, file_id): """Returns AsyncIOMotorGridOut (non-iterable file-like object)""" fs = AsyncIOMotorGridFSBucket(db) stream = await fs.open_download_stream(file_id) # return download_streamer(stream) return streamMi punto final de FastAPI se ve así:
app.get("/file/{file_id}") async def get_file(file_id): file = await download_file(db, file_id) return StreamingResponse(file, media_type=file.content_type) Cuando intento descargar un archivo con un file_id válido, aparece este error: TypeError: 'AsyncIOMotorGridOut' object is not an iterator
Mi segundo intento ha sido hacer un generador para iterar sobre fragmentos del archivo:
async def download_streamer(file: AsyncIOMotorGridOut): """ Returns generator file-like object to be served by StreamingResponse https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse """ chunk_size = 255*1024*1024 for chunk in await file.readchunk(): print(f"chunk: {chunk}") yield chunk Luego uso el return download_streamer(stream) comentado en mi ayudante download_file , pero por alguna razón, cada fragmento es solo un número entero de 255 .
¿Cuál es la mejor manera de sacar un archivo de MongoDB usando motor y transmitiéndolo como una respuesta web de FastAPI sin usar un archivo temporal? (No tengo acceso al disco duro y no quiero almacenar todo el archivo en la memoria; solo quiero transmitir archivos desde MongoDB a través de FastAPI directamente al cliente, fragmento por fragmento).
Mi solución es crear un generador que esté en la sintaxis de Python 3.6+ según esta respuesta SO . Dicho iterador funciona con la variante asíncrona de StreamingResponse de FastAPI y lee un fragmento de GridFS a la vez ( el valor predeterminado es 255 KB por documento de motor ) mediante el método readchunk() . Este tamaño de fragmento se establece cuando el archivo se almacena en MongoDB mediante upload_from_stream() . Una implementación opcional sería usar .read(n) para leer n bytes a la vez. Elegí usar readchunk() para que se obtenga exactamente 1 documento de base de datos a la vez durante la transmisión (cada archivo GridFS se divide en partes y se almacena una parte a la vez en la base de datos)
async def chunk_generator(grid_out): while True: # chunk = await grid_out.read(1024) chunk = await grid_out.readchunk() if not chunk: break yield chunk async def download_file(db, file_id): """Returns iterator over AsyncIOMotorGridOut object""" fs = AsyncIOMotorGridFSBucket(db) grid_out = await fs.open_download_stream(file_id) return chunk_generator(grid_out) Una mejora futura será que download_file() devuelva una tupla para incluir no solo el generador, sino también metadatos como ContentType .