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(