From 4c050ad76b954f3bd890d35fe2601d11afd13d90 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Mon, 8 Jun 2026 13:56:02 -0700 Subject: [PATCH] Don't mangle filer paths with the OS separator on Windows (#9878) fix: don't mangle filer paths with the OS separator on Windows filepath.Dir/Join use the platform separator, so on Windows they rewrite a forward-slash filer path like /buckets/x into \buckets\x. The mangled value then goes into a filer RPC and operates on the wrong key, so the op silently targets nothing. The admin file browser hit this in New Folder (the entry landed under \buckets\my-bucket and never showed up under /buckets/my-bucket), and the same way in delete, view and properties. MQ topic retention and consumer-offset listing, and the SFTP home dir plus create-permission parent lookup, had the same bug. Switch all of these to the path package, which always uses "/". --- weed/admin/dash/mq_management.go | 6 ++-- weed/admin/dash/topic_retention.go | 10 +++---- weed/admin/handlers/file_browser_handlers.go | 29 ++++++++++---------- weed/sftpd/sftp_permissions.go | 4 +-- weed/sftpd/user/user.go | 4 +-- 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/weed/admin/dash/mq_management.go b/weed/admin/dash/mq_management.go index 211acaa8d..c6d8de1fc 100644 --- a/weed/admin/dash/mq_management.go +++ b/weed/admin/dash/mq_management.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "io" - "path/filepath" + "path" "strings" "time" @@ -325,7 +325,7 @@ func (s *AdminServer) GetConsumerGroupOffsets(namespace, topicName string) ([]Co // Only process directories that are versions (start with "v") if versionResp.Entry != nil && versionResp.Entry.IsDirectory && strings.HasPrefix(versionResp.Entry.Name, "v") { - versionDir := filepath.Join(topicDir, versionResp.Entry.Name) + versionDir := path.Join(topicDir, versionResp.Entry.Name) // List all partition directories under the version directory (e.g., 0315-0630) partitionStream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ @@ -360,7 +360,7 @@ func (s *AdminServer) GetConsumerGroupOffsets(namespace, topicName string) ([]Co continue } - partitionDir := filepath.Join(versionDir, partitionResp.Entry.Name) + partitionDir := path.Join(versionDir, partitionResp.Entry.Name) // List all .offset files in this partition directory offsetStream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ diff --git a/weed/admin/dash/topic_retention.go b/weed/admin/dash/topic_retention.go index cc66f9035..1a9140368 100644 --- a/weed/admin/dash/topic_retention.go +++ b/weed/admin/dash/topic_retention.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "io" - "path/filepath" + "path" "sort" "strings" "time" @@ -187,7 +187,7 @@ func (p *TopicRetentionPurger) purgeTopicData(topicRetention TopicRetentionConfi // Check if this version directory is expired if versionDir.VersionTime.Before(cutoffTime) { - dirPath := filepath.Join(topicDir, versionDir.Name) + dirPath := path.Join(topicDir, versionDir.Name) // Delete the entire version directory err := p.deleteDirectoryRecursively(client, dirPath) @@ -267,7 +267,7 @@ func (p *TopicRetentionPurger) deleteDirectoryRecursively(client filer_pb.Seawee if resp.Entry == nil { continue } - entryPath := filepath.Join(dirPath, resp.Entry.Name) + entryPath := path.Join(dirPath, resp.Entry.Name) if resp.Entry.IsDirectory { // Recursively delete subdirectory @@ -288,8 +288,8 @@ func (p *TopicRetentionPurger) deleteDirectoryRecursively(client filer_pb.Seawee } // Delete the directory itself - parentDir := filepath.Dir(dirPath) - dirName := filepath.Base(dirPath) + parentDir := path.Dir(dirPath) + dirName := path.Base(dirPath) _, err = client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{ Directory: parentDir, diff --git a/weed/admin/handlers/file_browser_handlers.go b/weed/admin/handlers/file_browser_handlers.go index b26f0cd4d..e68b5db6f 100644 --- a/weed/admin/handlers/file_browser_handlers.go +++ b/weed/admin/handlers/file_browser_handlers.go @@ -11,7 +11,6 @@ import ( "net/http" "os" "path" - "path/filepath" "strconv" "strings" "time" @@ -111,8 +110,8 @@ func (h *FileBrowserHandlers) DeleteFile(w http.ResponseWriter, r *http.Request) // Delete file via filer err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { _, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{ - Directory: filepath.Dir(request.Path), - Name: filepath.Base(request.Path), + Directory: path.Dir(request.Path), + Name: path.Base(request.Path), IsDeleteData: true, IsRecursive: true, IgnoreRecursiveError: false, @@ -143,8 +142,8 @@ func (h *FileBrowserHandlers) DeleteMultipleFiles(w http.ResponseWriter, r *http return } - for _, path := range request.Paths { - if strings.TrimSpace(path) == "" { + for _, p := range request.Paths { + if strings.TrimSpace(p) == "" { writeJSONError(w, http.StatusBadRequest, "path is required") return } @@ -155,11 +154,11 @@ func (h *FileBrowserHandlers) DeleteMultipleFiles(w http.ResponseWriter, r *http var errors []string // Delete each file/folder - for _, path := range request.Paths { + for _, p := range request.Paths { err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { _, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{ - Directory: filepath.Dir(path), - Name: filepath.Base(path), + Directory: path.Dir(p), + Name: path.Base(p), IsDeleteData: true, IsRecursive: true, IgnoreRecursiveError: false, @@ -169,7 +168,7 @@ func (h *FileBrowserHandlers) DeleteMultipleFiles(w http.ResponseWriter, r *http if err != nil { failedCount++ - errors = append(errors, fmt.Sprintf("%s: %v", path, err)) + errors = append(errors, fmt.Sprintf("%s: %v", p, err)) } else { deletedCount++ } @@ -230,9 +229,9 @@ func (h *FileBrowserHandlers) CreateFolder(w http.ResponseWriter, r *http.Reques // Create folder via filer err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ - Directory: filepath.Dir(fullPath), + Directory: path.Dir(fullPath), Entry: &filer_pb.Entry{ - Name: filepath.Base(fullPath), + Name: path.Base(fullPath), IsDirectory: true, Attributes: &filer_pb.FuseAttributes{ FileMode: uint32(0o755 | os.ModeDir), // Directory mode @@ -444,8 +443,8 @@ func (h *FileBrowserHandlers) ViewFile(w http.ResponseWriter, r *http.Request) { var fileEntry dash.FileEntry err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ - Directory: filepath.Dir(filePath), - Name: filepath.Base(filePath), + Directory: path.Dir(filePath), + Name: path.Base(filePath), }) if err != nil { return err @@ -549,8 +548,8 @@ func (h *FileBrowserHandlers) GetFileProperties(w http.ResponseWriter, r *http.R var properties map[string]interface{} err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ - Directory: filepath.Dir(filePath), - Name: filepath.Base(filePath), + Directory: path.Dir(filePath), + Name: path.Base(filePath), }) if err != nil { return err diff --git a/weed/sftpd/sftp_permissions.go b/weed/sftpd/sftp_permissions.go index 1bddd91f2..818de5a99 100644 --- a/weed/sftpd/sftp_permissions.go +++ b/weed/sftpd/sftp_permissions.go @@ -3,7 +3,7 @@ package sftpd import ( "fmt" "os" - "path/filepath" + stdpath "path" "strings" "github.com/seaweedfs/seaweedfs/weed/glog" @@ -66,7 +66,7 @@ func (fs *SftpServer) CheckFilePermission(path string, perm string) error { // If the path doesn't exist and we're checking for create/write/mkdir permission, // check permissions on the parent directory instead if err == os.ErrNotExist { - parentPath := filepath.Dir(path) + parentPath := stdpath.Dir(path) // Check if user can write to the parent directory return fs.CheckFilePermission(parentPath, perm) } diff --git a/weed/sftpd/user/user.go b/weed/sftpd/user/user.go index 9edaf1a6b..6aa3d3213 100644 --- a/weed/sftpd/user/user.go +++ b/weed/sftpd/user/user.go @@ -3,7 +3,7 @@ package user import ( "math/rand/v2" - "path/filepath" + "path" ) // User represents an SFTP user with authentication and permission details @@ -27,7 +27,7 @@ func NewUser(username string) *User { return &User{ Username: username, Permissions: make(map[string][]string), - HomeDir: filepath.Join("/home", username), + HomeDir: path.Join("/home", username), Uid: uint32(randomId), Gid: uint32(randomId), }