Files
seaweedFS/weed/admin/dash/file_browser_data.go
undefined 9c784cf9e2 fix: use path to handle urls in weed admin file browser (#7858)
* fix: use path instead of filepath to handle urls in weed admin file browser

* test: add comprehensive tests for file browser path handling

- Test breadcrumb generation for various path scenarios
- Test path handling with forward slashes (URL compatibility)
- Test parent path calculation for Windows compatibility
- Test file extension handling using path.Ext
- Test bucket path detection logic

These tests verify that the switch from filepath to path package works
correctly and handles URLs properly across all platforms.

* refactor: simplify fullPath construction using path.Join

Replace verbose manual path construction with path.Join which:
- Handles trailing slashes automatically
- Is more concise and readable
- Is more robust for edge cases

* fix: normalize path in ShowFileBrowser and rename generateBreadcrumbs parameter

Critical fix:
- Add util.CleanWindowsPath() normalization to path parameter in ShowFileBrowser
  handler, matching the pattern used in other file operation handlers
  (lines 273, 464)
- This ensures Windows-style backslashes are converted to forward slashes
  before processing, fixing path handling issues on Windows

Consistency improvement:
- Rename path parameter to dir in generateBreadcrumbs function
- Aligns with parameter rename in GetFileBrowser for consistent naming
  throughout the file

* test: improve coverage for Windows path handling and production code behavior

Address reviewer feedback by enhancing test quality:

1. Improved test documentation:
   - Added clear comments explaining what each test validates
   - Clarified that some tests validate expected behavior vs production code
   - Documented the Windows path normalization flow

2. Enhanced actual production code testing:
   - TestGenerateBreadcrumbs: Calls actual production function
   - TestBreadcrumbPathFormatting: Validates production output format
   - TestDirectoryNavigation: Integration-style test for complete flow

3. Added new test functions for better coverage:
   - TestPathJoinHandlesEdgeCases: Verifies path.Join behavior
   - TestWindowsPathNormalizationBehavior: Documents expected normalization
   - TestDirectoryNavigation: Complete navigation flow test

4. Improved test organization:
   - Fixed duplicate field naming issues
   - Better test names for clarity
   - More comprehensive edge case coverage

These improvements ensure the fix for issue #7628 (Windows path handling)
is properly validated across the complete flow from handler to path logic.

* test: use actual util.CleanWindowsPath function in Windows path normalization test

Address reviewer feedback by testing the actual production function:
- Import util package for CleanWindowsPath
- Call the real util.CleanWindowsPath() instead of reimplementing logic
- Ensures test validates actual implementation, not just expected behavior
- Added more test cases for edge cases (simple path, deep nesting)

This change validates that the Windows path normalization in the
ShowFileBrowser handler (handlers/file_browser_handlers.go:64)
works correctly with the actual util.CleanWindowsPath function.

* style: fix indentation in TestPathJoinHandlesEdgeCases

Align t.Errorf statement inside the if block with proper indentation.
The error message now correctly aligns with the if block body,
maintaining consistent indentation throughout the function.

* test: restore backslash validation check in TestPathJoinHandlesEdgeCases

---------

Co-authored-by: Chris Lu <chris.lu@gmail.com>
2025-12-23 10:11:23 -08:00

269 lines
6.4 KiB
Go

package dash
import (
"context"
"path"
"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(dir string) (*FileBrowserData, error) {
if dir == "" {
dir = "/"
}
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: dir,
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.Join(dir, 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(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,
}
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(dir)
// Calculate parent path
parentPath := "/"
if dir != "/" {
parentPath = path.Dir(dir)
if parentPath == "." {
parentPath = "/"
}
}
// Check if this is a bucket path
isBucketPath := false
bucketName := ""
if strings.HasPrefix(dir, "/buckets/") {
isBucketPath = true
pathParts := strings.Split(strings.Trim(dir, "/"), "/")
if len(pathParts) >= 2 {
bucketName = pathParts[1]
}
}
return &FileBrowserData{
CurrentPath: dir,
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(dir string) []BreadcrumbItem {
var breadcrumbs []BreadcrumbItem
// Always start with root
breadcrumbs = append(breadcrumbs, BreadcrumbItem{
Name: "Root",
Path: "/",
})
if dir == "/" {
return breadcrumbs
}
// Split path and build breadcrumbs
parts := strings.Split(strings.Trim(dir, "/"), "/")
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(dir, "/buckets/") {
displayName = "📦 " + part // Add bucket icon to bucket name
}
breadcrumbs = append(breadcrumbs, BreadcrumbItem{
Name: displayName,
Path: currentPath,
})
}
return breadcrumbs
}