From 8b683c7da7d41feea0f2fe216b0460915b4a4c08 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Mon, 3 Aug 2026 15:02:39 +0200 Subject: [PATCH] Phase 6.5 Fix: DMS Download Endpoint fuer alle Dateitypen - GET /api/v1/dms/files/{file_id}/download streamt alle Dateitypen - FileResponse fuer LocalStorage (automatisches Streaming) - StreamingResponse Fallback fuer S3 - Prueft dms:read Permission und entity access --- app/plugins/builtins/dms/routes.py | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/app/plugins/builtins/dms/routes.py b/app/plugins/builtins/dms/routes.py index e3774ce..c73554d 100644 --- a/app/plugins/builtins/dms/routes.py +++ b/app/plugins/builtins/dms/routes.py @@ -888,6 +888,56 @@ async def preview_file( ) +@router.get("/files/{file_id}/download", dependencies=[Depends(require_permission("dms:read"))]) +async def download_file( + file_id: str, + db: AsyncSession = Depends(get_db), + current_user: dict = Depends(get_current_user), +): + """Download any file type — streams directly from storage without loading into RAM.""" + tenant_id = uuid.UUID(current_user["tenant_id"]) + user_id = uuid.UUID(current_user["user_id"]) + is_admin = current_user.get("role") == "admin" + fid = _parse_uuid(file_id, "file_id") + + result = await db.execute( + select(DmsFile).where( + DmsFile.id == fid, + DmsFile.tenant_id == tenant_id, + DmsFile.deleted_at.is_(None), + ) + ) + dms_file = result.scalar_one_or_none() + if dms_file is None: + raise HTTPException(404, detail={"detail": "File not found", "code": "not_found"}) + + if not await check_single_entity_access(db, "dms_file", fid, user_id, tenant_id, "read", is_admin): + raise HTTPException(403, detail={"detail": "Access denied", "code": "forbidden"}) + + storage = get_storage_backend() + if not await storage.exists(dms_file.storage_path): + raise HTTPException(404, detail={"detail": "File not found on disk", "code": "file_missing"}) + + from fastapi.responses import FileResponse as FastApiFileResponse + + if isinstance(storage, LocalStorage): + full_path = storage._full_path(dms_file.storage_path) + return FastApiFileResponse( + path=full_path, + media_type=dms_file.mime_type or "application/octet-stream", + filename=dms_file.name, + ) + else: + content = await storage.read(dms_file.storage_path) + def _stream(): + yield content + return StreamingResponse( + _stream(), + media_type=dms_file.mime_type or "application/octet-stream", + headers={"Content-Disposition": f'attachment; filename="{dms_file.name}"'}, + ) + + @router.post("/files/{file_id}/edit-session", dependencies=[Depends(require_permission("dms:write"))]) async def create_edit_session( file_id: str,