From 7f668b51f940946027813d10721ba47bb67db5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6sche?= Date: Wed, 28 Jan 2026 23:43:02 +0100 Subject: [PATCH] Implement folder download as zip in publicFileDownloadHandler --- .../lib/widgets/account_settings_dialog.dart | 5 +- go_cloud/internal/http/routes.go | 106 +++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/b0esche_cloud/lib/widgets/account_settings_dialog.dart b/b0esche_cloud/lib/widgets/account_settings_dialog.dart index 8c5a91b..e50ee83 100644 --- a/b0esche_cloud/lib/widgets/account_settings_dialog.dart +++ b/b0esche_cloud/lib/widgets/account_settings_dialog.dart @@ -423,7 +423,10 @@ class _AccountSettingsDialogState extends State { right: 8, child: IconButton( onPressed: _logout, - icon: Icon(Icons.logout, color: AppTheme.errorColor), + icon: Icon( + Icons.power_settings_new_rounded, + color: AppTheme.errorColor, + ), tooltip: 'Logout', iconSize: 28, splashColor: Colors.transparent, diff --git a/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index 4b80a09..cd8cf06 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -3186,9 +3186,111 @@ func publicFileDownloadHandler(w http.ResponseWriter, r *http.Request, db *datab return } - // Check if it's a folder - cannot download folders + // Check if it's a folder - if so, create a zip download if file.Type == "folder" { - errors.WriteError(w, errors.CodeInvalidArgument, "Cannot download folders", http.StatusBadRequest) + // Get all files under this folder path + var folderFiles []database.File + var err error + + if link.OrgID != nil { + // Org folder - need user ID from context or file owner + folderFiles, err = db.GetAllOrgFilesUnderPath(r.Context(), *link.OrgID, *file.UserID, file.Path) + } else { + // User folder + folderFiles, err = db.GetAllUserFilesUnderPath(r.Context(), *file.UserID, file.Path) + } + + if err != nil { + errors.LogError(r, err, "Failed to get folder contents") + errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError) + return + } + + // Filter out sub-folders (only include files) + var filesToZip []database.File + for _, f := range folderFiles { + if f.Type == "file" { + filesToZip = append(filesToZip, f) + } + } + + if len(filesToZip) == 0 { + errors.WriteError(w, errors.CodeInvalidArgument, "Folder is empty", http.StatusBadRequest) + return + } + + // Create zip file in memory + var zipBuffer bytes.Buffer + zipWriter := zip.NewWriter(&zipBuffer) + + // Get WebDAV client for the file's owner + client, err := getUserWebDAVClient(r.Context(), db, *file.UserID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass) + if err != nil { + errors.LogError(r, err, "Failed to get WebDAV client") + errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError) + return + } + + // Add each file to the zip + for _, fileToZip := range filesToZip { + // Download file from storage + downloadCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + resp, err := client.Download(downloadCtx, fileToZip.Path, "") + cancel() + if err != nil { + errors.LogError(r, err, fmt.Sprintf("Failed to download file %s for zip", fileToZip.Path)) + continue // Skip this file, continue with others + } + + // Read file content + fileData, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + errors.LogError(r, err, fmt.Sprintf("Failed to read file %s for zip", fileToZip.Path)) + continue + } + + // Create zip entry - use relative path within the folder + relativePath := strings.TrimPrefix(fileToZip.Path, file.Path) + if strings.HasPrefix(relativePath, "/") { + relativePath = strings.TrimPrefix(relativePath, "/") + } + + zipFile, err := zipWriter.Create(relativePath) + if err != nil { + errors.LogError(r, err, fmt.Sprintf("Failed to create zip entry for %s", relativePath)) + continue + } + + // Write file data to zip + _, err = zipFile.Write(fileData) + if err != nil { + errors.LogError(r, err, fmt.Sprintf("Failed to write file %s to zip", relativePath)) + continue + } + } + + // Close zip writer + err = zipWriter.Close() + if err != nil { + errors.LogError(r, err, "Failed to close zip writer") + errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError) + return + } + + // Add CORS headers for public access + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Range") + + // Set headers for zip download + w.Header().Set("Content-Type", "application/zip") + zipFilename := fmt.Sprintf("%s.zip", file.Name) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", zipFilename)) + w.Header().Set("Content-Length", fmt.Sprintf("%d", zipBuffer.Len())) + + // Stream the zip file + w.Write(zipBuffer.Bytes()) return }