diff --git a/weed/admin/dash/file_browser_data.go b/weed/admin/dash/file_browser_data.go index bd561e5ad..6e6e44c9d 100644 --- a/weed/admin/dash/file_browser_data.go +++ b/weed/admin/dash/file_browser_data.go @@ -3,7 +3,6 @@ package dash import ( "context" "path" - "sort" "strings" "time" @@ -34,34 +33,49 @@ type BreadcrumbItem struct { // FileBrowserData contains all data needed for the file browser view type FileBrowserData struct { - Username string `json:"username"` - CurrentPath string `json:"current_path"` - ParentPath string `json:"parent_path"` - Breadcrumbs []BreadcrumbItem `json:"breadcrumbs"` - Entries []FileEntry `json:"entries"` - TotalEntries int `json:"total_entries"` - TotalSize int64 `json:"total_size"` - LastUpdated time.Time `json:"last_updated"` - IsBucketPath bool `json:"is_bucket_path"` - BucketName string `json:"bucket_name"` + Username string `json:"username"` + CurrentPath string `json:"current_path"` + ParentPath string `json:"parent_path"` + Breadcrumbs []BreadcrumbItem `json:"breadcrumbs"` + Entries []FileEntry `json:"entries"` + + LastUpdated time.Time `json:"last_updated"` + IsBucketPath bool `json:"is_bucket_path"` + BucketName string `json:"bucket_name"` + // Pagination fields + PageSize int `json:"page_size"` + HasNextPage bool `json:"has_next_page"` + LastFileName string `json:"last_file_name"` // Cursor for next page + CurrentLastFileName string `json:"current_last_file_name"` // Cursor from current request (for page size changes) } -// GetFileBrowser retrieves file browser data for a given path -func (s *AdminServer) GetFileBrowser(dir string) (*FileBrowserData, error) { +// GetFileBrowser retrieves file browser data for a given path with cursor-based pagination +func (s *AdminServer) GetFileBrowser(dir string, lastFileName string, pageSize int) (*FileBrowserData, error) { if dir == "" { dir = "/" } + // Set defaults for pagination + if pageSize < 1 { + pageSize = 20 // Default page size + } + var entries []FileEntry - var totalSize int64 - // Get directory listing from filer + // Fetch entries using cursor-based pagination + // We fetch pageSize+1 to determine if there's a next page + fetchLimit := pageSize + 1 + var fetchedCount int + var lastEntryName string + err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { + // Fetch entries starting from the cursor (lastFileName) stream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ Directory: dir, Prefix: "", - Limit: 1000, - InclusiveStartFrom: false, + Limit: uint32(fetchLimit), + StartFromFileName: lastFileName, + InclusiveStartFrom: false, // Don't include the cursor file itself }) if err != nil { return err @@ -81,97 +95,102 @@ func (s *AdminServer) GetFileBrowser(dir string) (*FileBrowserData, error) { continue } - fullPath := path.Join(dir, entry.Name) + fetchedCount++ - var modTime time.Time - if entry.Attributes != nil && entry.Attributes.Mtime > 0 { - modTime = time.Unix(entry.Attributes.Mtime, 0) - } + // Only add entries up to pageSize (the +1 is just to check for next page) + if fetchedCount <= pageSize { + fullPath := path.Join(dir, entry.Name) - var mode string - var uid, gid uint32 - var size int64 - var replication, collection string - var ttlSec int32 - - if entry.Attributes != nil { - mode = FormatFileMode(entry.Attributes.FileMode) - uid = entry.Attributes.Uid - gid = entry.Attributes.Gid - size = int64(entry.Attributes.FileSize) - ttlSec = entry.Attributes.TtlSec - } + var modTime time.Time + if entry.Attributes != nil && entry.Attributes.Mtime > 0 { + modTime = time.Unix(entry.Attributes.Mtime, 0) + } - // Get replication and collection from entry extended attributes or chunks - if entry.Extended != nil { - if repl, ok := entry.Extended["replication"]; ok { - replication = string(repl) + var mode string + var uid, gid uint32 + var size int64 + var replication, collection string + var ttlSec int32 + + if entry.Attributes != nil { + mode = FormatFileMode(entry.Attributes.FileMode) + uid = entry.Attributes.Uid + gid = entry.Attributes.Gid + size = int64(entry.Attributes.FileSize) + ttlSec = entry.Attributes.TtlSec } - if coll, ok := entry.Extended["collection"]; ok { - collection = string(coll) + + // Get replication and collection from entry extended attributes + if entry.Extended != nil { + if repl, ok := entry.Extended["replication"]; ok { + replication = string(repl) + } + if coll, ok := entry.Extended["collection"]; ok { + collection = string(coll) + } } - } - // Determine MIME type based on file extension - mime := "application/octet-stream" - if entry.IsDirectory { - mime = "inode/directory" - } else { - ext := strings.ToLower(path.Ext(entry.Name)) - switch ext { - case ".txt", ".log": - mime = "text/plain" - case ".html", ".htm": - mime = "text/html" - case ".css": - mime = "text/css" - case ".js": - mime = "application/javascript" - case ".json": - mime = "application/json" - case ".xml": - mime = "application/xml" - case ".pdf": - mime = "application/pdf" - case ".jpg", ".jpeg": - mime = "image/jpeg" - case ".png": - mime = "image/png" - case ".gif": - mime = "image/gif" - case ".svg": - mime = "image/svg+xml" - case ".mp4": - mime = "video/mp4" - case ".mp3": - mime = "audio/mpeg" - case ".zip": - mime = "application/zip" - case ".tar": - mime = "application/x-tar" - case ".gz": - mime = "application/gzip" + // Determine MIME type based on file extension + mime := "application/octet-stream" + if entry.IsDirectory { + mime = "inode/directory" + } else { + ext := strings.ToLower(path.Ext(entry.Name)) + switch ext { + case ".txt", ".log": + mime = "text/plain" + case ".html", ".htm": + mime = "text/html" + case ".css": + mime = "text/css" + case ".js": + mime = "application/javascript" + case ".json": + mime = "application/json" + case ".xml": + mime = "application/xml" + case ".pdf": + mime = "application/pdf" + case ".jpg", ".jpeg": + mime = "image/jpeg" + case ".png": + mime = "image/png" + case ".gif": + mime = "image/gif" + case ".svg": + mime = "image/svg+xml" + case ".mp4": + mime = "video/mp4" + case ".mp3": + mime = "audio/mpeg" + case ".zip": + mime = "application/zip" + case ".tar": + mime = "application/x-tar" + case ".gz": + mime = "application/gzip" + } } - } - fileEntry := FileEntry{ - Name: entry.Name, - FullPath: fullPath, - IsDirectory: entry.IsDirectory, - Size: size, - ModTime: modTime, - Mode: mode, - Uid: uid, - Gid: gid, - Mime: mime, - Replication: replication, - Collection: collection, - TtlSec: ttlSec, - } + fileEntry := FileEntry{ + Name: entry.Name, + FullPath: fullPath, + IsDirectory: entry.IsDirectory, + Size: size, + ModTime: modTime, + Mode: mode, + Uid: uid, + Gid: gid, + Mime: mime, + Replication: replication, + Collection: collection, + TtlSec: ttlSec, + } + + entries = append(entries, fileEntry) + + lastEntryName = entry.Name - entries = append(entries, fileEntry) - if !entry.IsDirectory { - totalSize += size } } @@ -182,13 +201,8 @@ func (s *AdminServer) GetFileBrowser(dir string) (*FileBrowserData, error) { return nil, err } - // Sort entries: directories first, then files, both alphabetically - sort.Slice(entries, func(i, j int) bool { - if entries[i].IsDirectory != entries[j].IsDirectory { - return entries[i].IsDirectory - } - return strings.ToLower(entries[i].Name) < strings.ToLower(entries[j].Name) - }) + // Determine if there's a next page + hasNextPage := fetchedCount > pageSize // Generate breadcrumbs breadcrumbs := s.generateBreadcrumbs(dir) @@ -214,15 +228,19 @@ func (s *AdminServer) GetFileBrowser(dir string) (*FileBrowserData, error) { } return &FileBrowserData{ - CurrentPath: dir, - ParentPath: parentPath, - Breadcrumbs: breadcrumbs, - Entries: entries, - TotalEntries: len(entries), - TotalSize: totalSize, + CurrentPath: dir, + ParentPath: parentPath, + Breadcrumbs: breadcrumbs, + Entries: entries, + LastUpdated: time.Now(), IsBucketPath: isBucketPath, BucketName: bucketName, + // Pagination metadata + PageSize: pageSize, + HasNextPage: hasNextPage, + LastFileName: lastEntryName, // Store for next page navigation + CurrentLastFileName: lastFileName, // Store input cursor for page size changes }, nil } diff --git a/weed/admin/handlers/file_browser_handlers.go b/weed/admin/handlers/file_browser_handlers.go index d8f79337d..b7c44a69d 100644 --- a/weed/admin/handlers/file_browser_handlers.go +++ b/weed/admin/handlers/file_browser_handlers.go @@ -63,8 +63,19 @@ func (h *FileBrowserHandlers) ShowFileBrowser(c *gin.Context) { // Normalize Windows-style paths for consistency path = util.CleanWindowsPath(path) - // Get file browser data - browserData, err := h.adminServer.GetFileBrowser(path) + // Get pagination parameters + lastFileName := c.DefaultQuery("lastFileName", "") + + pageSize, err := strconv.Atoi(c.DefaultQuery("limit", "20")) + if err != nil || pageSize < 1 { + pageSize = 20 + } + if pageSize > 200 { + pageSize = 200 + } + + // Get file browser data with cursor-based pagination + browserData, err := h.adminServer.GetFileBrowser(path, lastFileName, pageSize) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file browser data: " + err.Error()}) return diff --git a/weed/admin/view/app/file_browser.templ b/weed/admin/view/app/file_browser.templ index 83db7df0f..6ae00b81b 100644 --- a/weed/admin/view/app/file_browser.templ +++ b/weed/admin/view/app/file_browser.templ @@ -7,6 +7,10 @@ import ( "github.com/seaweedfs/seaweedfs/weed/admin/dash" ) +script changePageSize(path string, lastFileName string) { + window.location.href = '/files?path=' + encodeURIComponent(path) + '&lastFileName=' + encodeURIComponent(lastFileName) + '&limit=' + this.value +} + templ FileBrowser(data dash.FileBrowserData) {
| Name | Size | Type | Modified | Permissions | Actions |
|---|
| Name | Size | Type | Modified | Permissions | Actions | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\"> | ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if entry.IsDirectory {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" class=\"text-decoration-none\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 199, Col: 25}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 144, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -301,7 +369,7 @@ func FileBrowser(data dash.FileBrowserData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 203, Col: 30}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 148, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " | ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " | ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if entry.IsDirectory { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "—") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "—") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -345,19 +413,19 @@ func FileBrowser(data dash.FileBrowserData) templ.Component { var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(entry.Size)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 211, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 156, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " | ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " | ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if entry.IsDirectory { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "Directory") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "Directory") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -365,14 +433,14 @@ func FileBrowser(data dash.FileBrowserData) templ.Component { var templ_7745c5c3_Var19 string templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(getMimeDisplayName(entry.Mime)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 219, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 164, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " | ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " | ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -380,199 +448,264 @@ func FileBrowser(data dash.FileBrowserData) templ.Component { var templ_7745c5c3_Var20 string templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(entry.ModTime.Format("2006-01-02 15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 225, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 170, Col: 53} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "—") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "—") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " | |
This directory contains no files or subdirectories.
This directory contains no files or subdirectories.