Tesis Rutas integrates with Cloudinary for robust multimedia storage and delivery. Each destination can store up to 10 multimedia files (images or videos) with automatic metadata extraction and optimized delivery.
Hard Limit: Each destination is restricted to a maximum of 10 multimedia files. This limit is enforced at the API level.
Check that the upload won’t exceed the 10-file limit for the destination.
2
Upload to Cloudinary
Use async upload for all files in parallel:
Async Upload
async def upload_async(file: UploadFile): file_bytes = await file.read() return await asyncio.to_thread( cloudinary.uploader.upload, file_bytes, folder=f"destinations/{id}", resource_type="auto" # Auto-detect image vs video )resultados_cloudinary = await asyncio.gather(*[ upload_async(file) for file in files])
3
Extract Metadata
Create Multimedia entities from Cloudinary response:
Metadata Extraction
for data in resultados_cloudinary: tipo = data.get("resource_type") item = Multimedia( destino_id=id, url=data.get("secure_url"), public_id=data.get("public_id"), tipo=tipo, formato=data.get("format"), tamanio_bytes=data.get("bytes"), anchura=data.get("width"), altura=data.get("height"), duracion=data.get("duration") if tipo == "video" else None )
4
Save to Database
Use AgregarMultimediaDestinoUseCase to persist metadata.
5
Rollback on Failure
If database save fails, automatically delete uploaded files from Cloudinary:
Rollback Logic
except Exception as e: # Delete from Cloudinary for pid in public_ids_subidos: try: cloudinary.uploader.destroy(pid) except: pass raise HTTPException( status_code=500, detail="Error al guardar multimedia, las subidas fueron revertidas." )
DELETE/destinos/{id}/multimedia?public_id={public_id}Requires: Admin role
Delete Endpoint
@router.delete("/{id}/multimedia")async def eliminar_multimedia_destino( id: str, public_id: str, # Query parameter db=Depends(get_database), admin=Depends(require_admin)): repo = DestinoRepositoryImpl(db) # Validate destination and file existence destino = repo.obtener_por_id(id) if not destino: raise HTTPException(status_code=404, detail="Destino no encontrado") multimedia_actual = destino.get("multimedia", []) archivo = next((m for m in multimedia_actual if m["public_id"] == public_id), None) if not archivo: raise HTTPException(status_code=404, detail="Archivo no existe") tipo_cloudinary = archivo.get("tipo", "image") # Delete from database first use_case = EliminarMultimediaDestinoUseCase(repo) eliminado_db = await use_case.execute(id, public_id) if not eliminado_db: raise HTTPException(status_code=500, detail="Error al eliminar de BD") # Then delete from Cloudinary try: cloudinary.uploader.destroy(public_id, resource_type=tipo_cloudinary) except Exception as e: return { "message": "Archivo eliminado de BD. Error al borrar de Cloudinary.", "error_cloudinary": str(e) } return { "message": "Multimedia eliminada correctamente", "public_id": public_id }
Deletion follows a “database-first” approach. If Cloudinary deletion fails, the file is orphaned in cloud storage but removed from the app.