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 | |
| }
 |