You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							272 lines
						
					
					
						
							6.5 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							272 lines
						
					
					
						
							6.5 KiB
						
					
					
				
								package dash
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"context"
							 | 
						|
									"path/filepath"
							 | 
						|
									"sort"
							 | 
						|
									"strings"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// FileEntry represents a file or directory entry in the file browser
							 | 
						|
								type FileEntry struct {
							 | 
						|
									Name        string    `json:"name"`
							 | 
						|
									FullPath    string    `json:"full_path"`
							 | 
						|
									IsDirectory bool      `json:"is_directory"`
							 | 
						|
									Size        int64     `json:"size"`
							 | 
						|
									ModTime     time.Time `json:"mod_time"`
							 | 
						|
									Mode        string    `json:"mode"`
							 | 
						|
									Uid         uint32    `json:"uid"`
							 | 
						|
									Gid         uint32    `json:"gid"`
							 | 
						|
									Mime        string    `json:"mime"`
							 | 
						|
									Replication string    `json:"replication"`
							 | 
						|
									Collection  string    `json:"collection"`
							 | 
						|
									TtlSec      int32     `json:"ttl_sec"`
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// BreadcrumbItem represents a single breadcrumb in the navigation
							 | 
						|
								type BreadcrumbItem struct {
							 | 
						|
									Name string `json:"name"`
							 | 
						|
									Path string `json:"path"`
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// 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"`
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GetFileBrowser retrieves file browser data for a given path
							 | 
						|
								func (s *AdminServer) GetFileBrowser(path string) (*FileBrowserData, error) {
							 | 
						|
									if path == "" {
							 | 
						|
										path = "/"
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var entries []FileEntry
							 | 
						|
									var totalSize int64
							 | 
						|
								
							 | 
						|
									// Get directory listing from filer
							 | 
						|
									err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
							 | 
						|
										stream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{
							 | 
						|
											Directory:          path,
							 | 
						|
											Prefix:             "",
							 | 
						|
											Limit:              1000,
							 | 
						|
											InclusiveStartFrom: false,
							 | 
						|
										})
							 | 
						|
										if err != nil {
							 | 
						|
											return err
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										for {
							 | 
						|
											resp, err := stream.Recv()
							 | 
						|
											if err != nil {
							 | 
						|
												if err.Error() == "EOF" {
							 | 
						|
													break
							 | 
						|
												}
							 | 
						|
												return err
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											entry := resp.Entry
							 | 
						|
											if entry == nil {
							 | 
						|
												continue
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											fullPath := path
							 | 
						|
											if !strings.HasSuffix(fullPath, "/") {
							 | 
						|
												fullPath += "/"
							 | 
						|
											}
							 | 
						|
											fullPath += entry.Name
							 | 
						|
								
							 | 
						|
											var modTime time.Time
							 | 
						|
											if entry.Attributes != nil && entry.Attributes.Mtime > 0 {
							 | 
						|
												modTime = time.Unix(entry.Attributes.Mtime, 0)
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											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
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											// Get replication and collection from entry extended attributes or chunks
							 | 
						|
											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(filepath.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,
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											entries = append(entries, fileEntry)
							 | 
						|
											if !entry.IsDirectory {
							 | 
						|
												totalSize += size
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										return nil
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									if err != nil {
							 | 
						|
										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)
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									// Generate breadcrumbs
							 | 
						|
									breadcrumbs := s.generateBreadcrumbs(path)
							 | 
						|
								
							 | 
						|
									// Calculate parent path
							 | 
						|
									parentPath := "/"
							 | 
						|
									if path != "/" {
							 | 
						|
										parentPath = filepath.Dir(path)
							 | 
						|
										if parentPath == "." {
							 | 
						|
											parentPath = "/"
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Check if this is a bucket path
							 | 
						|
									isBucketPath := false
							 | 
						|
									bucketName := ""
							 | 
						|
									if strings.HasPrefix(path, "/buckets/") {
							 | 
						|
										isBucketPath = true
							 | 
						|
										pathParts := strings.Split(strings.Trim(path, "/"), "/")
							 | 
						|
										if len(pathParts) >= 2 {
							 | 
						|
											bucketName = pathParts[1]
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return &FileBrowserData{
							 | 
						|
										CurrentPath:  path,
							 | 
						|
										ParentPath:   parentPath,
							 | 
						|
										Breadcrumbs:  breadcrumbs,
							 | 
						|
										Entries:      entries,
							 | 
						|
										TotalEntries: len(entries),
							 | 
						|
										TotalSize:    totalSize,
							 | 
						|
										LastUpdated:  time.Now(),
							 | 
						|
										IsBucketPath: isBucketPath,
							 | 
						|
										BucketName:   bucketName,
							 | 
						|
									}, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// generateBreadcrumbs creates breadcrumb navigation for the current path
							 | 
						|
								func (s *AdminServer) generateBreadcrumbs(path string) []BreadcrumbItem {
							 | 
						|
									var breadcrumbs []BreadcrumbItem
							 | 
						|
								
							 | 
						|
									// Always start with root
							 | 
						|
									breadcrumbs = append(breadcrumbs, BreadcrumbItem{
							 | 
						|
										Name: "Root",
							 | 
						|
										Path: "/",
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									if path == "/" {
							 | 
						|
										return breadcrumbs
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Split path and build breadcrumbs
							 | 
						|
									parts := strings.Split(strings.Trim(path, "/"), "/")
							 | 
						|
									currentPath := ""
							 | 
						|
								
							 | 
						|
									for _, part := range parts {
							 | 
						|
										if part == "" {
							 | 
						|
											continue
							 | 
						|
										}
							 | 
						|
										currentPath += "/" + part
							 | 
						|
								
							 | 
						|
										// Special handling for bucket paths
							 | 
						|
										displayName := part
							 | 
						|
										if len(breadcrumbs) == 1 && part == "buckets" {
							 | 
						|
											displayName = "Object Store Buckets"
							 | 
						|
										} else if len(breadcrumbs) == 2 && strings.HasPrefix(path, "/buckets/") {
							 | 
						|
											displayName = "📦 " + part // Add bucket icon to bucket name
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										breadcrumbs = append(breadcrumbs, BreadcrumbItem{
							 | 
						|
											Name: displayName,
							 | 
						|
											Path: currentPath,
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return breadcrumbs
							 | 
						|
								}
							 |