@@ -152,7 +152,7 @@ func (h *BucketsHandler) DeleteBucket(c echo.Context) error {
152152 return c .NoContent (http .StatusOK )
153153}
154154
155- // BrowseBucket renders the object browser with folder support
155+ // BrowseBucket renders the object browser with folder support and pagination
156156func (h * BucketsHandler ) BrowseBucket (c echo.Context ) error {
157157 creds , err := GetCredentialsOrRedirect (c )
158158 if err != nil {
@@ -161,16 +161,19 @@ func (h *BucketsHandler) BrowseBucket(c echo.Context) error {
161161
162162 bucketName := c .Param ("bucketName" )
163163 prefix := c .QueryParam ("prefix" )
164+ continuationToken := c .QueryParam ("continuation" )
164165
165166 client , err := h .minioFactory .NewClient (* creds )
166167 if err != nil {
167168 return echo .NewHTTPError (http .StatusInternalServerError , "Failed to connect to MinIO" )
168169 }
169170
170- // List objects with prefix for folder support
171- rawObjects , err := client .ListObjects (c .Request ().Context (), bucketName , minio.ListObjectsOptions {
172- Prefix : prefix ,
173- Recursive : false , // Non-recursive to get folders
171+ // List objects with pagination to limit memory usage
172+ result , err := client .ListObjectsPaginated (c .Request ().Context (), bucketName , services.ListObjectsOptions {
173+ Prefix : prefix ,
174+ Recursive : false , // Non-recursive to get folders
175+ MaxKeys : services .DefaultPageSize ,
176+ ContinuationToken : continuationToken ,
174177 })
175178 if err != nil {
176179 return echo .NewHTTPError (http .StatusInternalServerError , "Failed to list objects" )
@@ -180,7 +183,7 @@ func (h *BucketsHandler) BrowseBucket(c echo.Context) error {
180183 var folders []models.FolderInfo
181184 seenFolders := make (map [string ]bool )
182185
183- for _ , obj := range rawObjects {
186+ for _ , obj := range result . Objects {
184187 // Check if it's a folder (ends with /)
185188 if strings .HasSuffix (obj .Key , "/" ) {
186189 folderName := strings .TrimPrefix (obj .Key , prefix )
@@ -235,12 +238,14 @@ func (h *BucketsHandler) BrowseBucket(c echo.Context) error {
235238 }
236239
237240 return c .Render (http .StatusOK , "browser" , map [string ]interface {}{
238- "ActiveNav" : "buckets" ,
239- "BucketName" : bucketName ,
240- "Prefix" : prefix ,
241- "Objects" : objects ,
242- "Folders" : folders ,
243- "Breadcrumbs" : breadcrumbs ,
241+ "ActiveNav" : "buckets" ,
242+ "BucketName" : bucketName ,
243+ "Prefix" : prefix ,
244+ "Objects" : objects ,
245+ "Folders" : folders ,
246+ "Breadcrumbs" : breadcrumbs ,
247+ "HasMore" : result .IsTruncated ,
248+ "NextContinuationToken" : result .NextContinuationToken ,
244249 })
245250}
246251
@@ -387,7 +392,7 @@ func (h *BucketsHandler) CreateFolder(c echo.Context) error {
387392 return HTMXRedirect (c , "/buckets/" + bucketName + "?prefix=" + prefix )
388393}
389394
390- // DeleteFolder deletes a folder and all its contents
395+ // DeleteFolder deletes a folder and all its contents using streaming
391396func (h * BucketsHandler ) DeleteFolder (c echo.Context ) error {
392397 creds , err := GetCredentials (c )
393398 if err != nil {
@@ -402,17 +407,17 @@ func (h *BucketsHandler) DeleteFolder(c echo.Context) error {
402407 return echo .NewHTTPError (http .StatusInternalServerError , "Failed to connect to MinIO" )
403408 }
404409
405- // List all objects with this prefix
406- objectsList , err := client .ListObjects (c .Request ().Context (), bucketName , minio.ListObjectsOptions {
410+ // Stream objects and delete one at a time to avoid loading all into memory
411+ objectsChan := client .ListObjectsChannel (c .Request ().Context (), bucketName , minio.ListObjectsOptions {
407412 Prefix : prefix ,
408413 Recursive : true ,
409414 })
410- if err != nil {
411- return echo .NewHTTPError (http .StatusInternalServerError , "Failed to list objects" )
412- }
413415
414- // Delete all objects
415- for _ , obj := range objectsList {
416+ // Delete objects as they stream in
417+ for obj := range objectsChan {
418+ if obj .Err != nil {
419+ return echo .NewHTTPError (http .StatusInternalServerError , "Failed to list objects: " + obj .Err .Error ())
420+ }
416421 err := client .RemoveObject (c .Request ().Context (), bucketName , obj .Key , minio.RemoveObjectOptions {})
417422 if err != nil {
418423 return echo .NewHTTPError (http .StatusInternalServerError , "Failed to delete object: " + obj .Key )
@@ -422,7 +427,7 @@ func (h *BucketsHandler) DeleteFolder(c echo.Context) error {
422427 return c .NoContent (http .StatusOK )
423428}
424429
425- // DownloadZip streams a folder as a ZIP archive
430+ // DownloadZip streams a folder as a ZIP archive using streaming object listing
426431func (h * BucketsHandler ) DownloadZip (c echo.Context ) error {
427432 creds , err := GetCredentialsOrRedirect (c )
428433 if err != nil {
@@ -437,31 +442,9 @@ func (h *BucketsHandler) DownloadZip(c echo.Context) error {
437442 return echo .NewHTTPError (http .StatusInternalServerError , "Failed to connect to MinIO" )
438443 }
439444
440- // List all objects with this prefix recursively
441- objectsList , err := client .ListObjects (c .Request ().Context (), bucketName , minio.ListObjectsOptions {
442- Prefix : prefix ,
443- Recursive : true ,
444- })
445- if err != nil {
446- return echo .NewHTTPError (http .StatusInternalServerError , "Failed to list objects" )
447- }
448-
449- // Filter out folder markers (objects ending with /)
450- var files []minio.ObjectInfo
451- for _ , obj := range objectsList {
452- if ! strings .HasSuffix (obj .Key , "/" ) {
453- files = append (files , obj )
454- }
455- }
456-
457- if len (files ) == 0 {
458- return echo .NewHTTPError (http .StatusNotFound , "No files to download" )
459- }
460-
461445 // Determine ZIP filename from prefix or bucket name
462446 zipName := bucketName + ".zip"
463447 if prefix != "" {
464- // Remove trailing slash and get the folder name
465448 folderName := strings .TrimSuffix (prefix , "/" )
466449 if idx := strings .LastIndex (folderName , "/" ); idx >= 0 {
467450 folderName = folderName [idx + 1 :]
@@ -478,12 +461,26 @@ func (h *BucketsHandler) DownloadZip(c echo.Context) error {
478461 zipWriter := zip .NewWriter (c .Response ().Writer )
479462 defer func () { _ = zipWriter .Close () }()
480463
481- // Add each file to the ZIP
482- for _ , obj := range files {
464+ // Stream objects and add to ZIP one at a time to avoid loading all into memory
465+ objectsChan := client .ListObjectsChannel (c .Request ().Context (), bucketName , minio.ListObjectsOptions {
466+ Prefix : prefix ,
467+ Recursive : true ,
468+ })
469+
470+ fileCount := 0
471+ for obj := range objectsChan {
472+ if obj .Err != nil {
473+ continue
474+ }
475+
476+ // Skip folder markers (objects ending with /)
477+ if strings .HasSuffix (obj .Key , "/" ) {
478+ continue
479+ }
480+
483481 // Get the file content
484482 reader , _ , err := client .GetObjectReader (c .Request ().Context (), bucketName , obj .Key , minio.GetObjectOptions {})
485483 if err != nil {
486- // Log error but continue with other files
487484 continue
488485 }
489486
@@ -501,8 +498,12 @@ func (h *BucketsHandler) DownloadZip(c echo.Context) error {
501498 if err != nil {
502499 continue
503500 }
501+ fileCount ++
504502 }
505503
504+ // Note: If fileCount == 0, headers are already sent so we can't return an error.
505+ // The ZIP will just be empty, which is acceptable behavior.
506+
506507 return nil
507508}
508509
0 commit comments