From 6296b79ad0d622928df310153736f1db53aa259a Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Wed, 18 Mar 2026 14:10:50 +0100 Subject: [PATCH] feat folder download as zip --- modules/routes/routeDataFiles.py | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/modules/routes/routeDataFiles.py b/modules/routes/routeDataFiles.py index c3138aed..470793bb 100644 --- a/modules/routes/routeDataFiles.py +++ b/modules/routes/routeDataFiles.py @@ -441,6 +441,72 @@ def move_folder( raise HTTPException(status_code=500, detail=str(e)) +@router.get("/folders/{folderId}/download") +@limiter.limit("10/minute") +def download_folder( + request: Request, + folderId: str = Path(..., description="ID of the folder to download as ZIP"), + currentUser: User = Depends(getCurrentUser), + context: RequestContext = Depends(getRequestContext) +) -> Response: + """Download a folder (including subfolders) as a ZIP archive.""" + import io + import zipfile + import urllib.parse + + try: + mgmt = interfaceDbManagement.getInterface( + currentUser, + mandateId=str(context.mandateId) if context.mandateId else None, + featureInstanceId=str(context.featureInstanceId) if context.featureInstanceId else None, + ) + + folder = mgmt.getFolder(folderId) + if not folder: + raise HTTPException(status_code=404, detail=f"Folder {folderId} not found") + + folderName = folder.get("name", "download") + + def _collectFiles(parentId: str, pathPrefix: str): + """Recursively collect (zipPath, fileId) tuples.""" + entries = [] + for f in mgmt._getFilesByCurrentUser(recordFilter={"folderId": parentId}): + fname = f.get("fileName") or f.get("name") or f.get("id", "file") + entries.append((f"{pathPrefix}{fname}", f["id"])) + for sub in mgmt.listFolders(parentId=parentId): + subName = sub.get("name", sub["id"]) + entries.extend(_collectFiles(sub["id"], f"{pathPrefix}{subName}/")) + return entries + + fileEntries = _collectFiles(folderId, "") + if not fileEntries: + raise HTTPException(status_code=404, detail="Folder is empty") + + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + for zipPath, fileId in fileEntries: + data = mgmt.getFileData(fileId) + if data: + zf.writestr(zipPath, data) + + buf.seek(0) + zipBytes = buf.getvalue() + encodedName = urllib.parse.quote(f"{folderName}.zip") + + return Response( + content=zipBytes, + media_type="application/zip", + headers={ + "Content-Disposition": f"attachment; filename*=UTF-8''{encodedName}" + } + ) + except HTTPException: + raise + except Exception as e: + logger.error(f"Error downloading folder as ZIP: {e}") + raise HTTPException(status_code=500, detail=f"Error downloading folder: {str(e)}") + + @router.post("/batch-delete") @limiter.limit("10/minute") def batch_delete_items(