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 "/".
This commit is contained in:
Chris Lu
2026-06-08 13:56:02 -07:00
committed by GitHub
parent 8cc10460b4
commit 4c050ad76b
5 changed files with 26 additions and 27 deletions
+3 -3
View File
@@ -4,7 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"path/filepath" "path"
"strings" "strings"
"time" "time"
@@ -325,7 +325,7 @@ func (s *AdminServer) GetConsumerGroupOffsets(namespace, topicName string) ([]Co
// Only process directories that are versions (start with "v") // Only process directories that are versions (start with "v")
if versionResp.Entry != nil && versionResp.Entry.IsDirectory && strings.HasPrefix(versionResp.Entry.Name, "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) // List all partition directories under the version directory (e.g., 0315-0630)
partitionStream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ partitionStream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{
@@ -360,7 +360,7 @@ func (s *AdminServer) GetConsumerGroupOffsets(namespace, topicName string) ([]Co
continue continue
} }
partitionDir := filepath.Join(versionDir, partitionResp.Entry.Name) partitionDir := path.Join(versionDir, partitionResp.Entry.Name)
// List all .offset files in this partition directory // List all .offset files in this partition directory
offsetStream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ offsetStream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{
+5 -5
View File
@@ -4,7 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"path/filepath" "path"
"sort" "sort"
"strings" "strings"
"time" "time"
@@ -187,7 +187,7 @@ func (p *TopicRetentionPurger) purgeTopicData(topicRetention TopicRetentionConfi
// Check if this version directory is expired // Check if this version directory is expired
if versionDir.VersionTime.Before(cutoffTime) { if versionDir.VersionTime.Before(cutoffTime) {
dirPath := filepath.Join(topicDir, versionDir.Name) dirPath := path.Join(topicDir, versionDir.Name)
// Delete the entire version directory // Delete the entire version directory
err := p.deleteDirectoryRecursively(client, dirPath) err := p.deleteDirectoryRecursively(client, dirPath)
@@ -267,7 +267,7 @@ func (p *TopicRetentionPurger) deleteDirectoryRecursively(client filer_pb.Seawee
if resp.Entry == nil { if resp.Entry == nil {
continue continue
} }
entryPath := filepath.Join(dirPath, resp.Entry.Name) entryPath := path.Join(dirPath, resp.Entry.Name)
if resp.Entry.IsDirectory { if resp.Entry.IsDirectory {
// Recursively delete subdirectory // Recursively delete subdirectory
@@ -288,8 +288,8 @@ func (p *TopicRetentionPurger) deleteDirectoryRecursively(client filer_pb.Seawee
} }
// Delete the directory itself // Delete the directory itself
parentDir := filepath.Dir(dirPath) parentDir := path.Dir(dirPath)
dirName := filepath.Base(dirPath) dirName := path.Base(dirPath)
_, err = client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{ _, err = client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{
Directory: parentDir, Directory: parentDir,
+14 -15
View File
@@ -11,7 +11,6 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -111,8 +110,8 @@ func (h *FileBrowserHandlers) DeleteFile(w http.ResponseWriter, r *http.Request)
// Delete file via filer // Delete file via filer
err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
_, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{ _, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{
Directory: filepath.Dir(request.Path), Directory: path.Dir(request.Path),
Name: filepath.Base(request.Path), Name: path.Base(request.Path),
IsDeleteData: true, IsDeleteData: true,
IsRecursive: true, IsRecursive: true,
IgnoreRecursiveError: false, IgnoreRecursiveError: false,
@@ -143,8 +142,8 @@ func (h *FileBrowserHandlers) DeleteMultipleFiles(w http.ResponseWriter, r *http
return return
} }
for _, path := range request.Paths { for _, p := range request.Paths {
if strings.TrimSpace(path) == "" { if strings.TrimSpace(p) == "" {
writeJSONError(w, http.StatusBadRequest, "path is required") writeJSONError(w, http.StatusBadRequest, "path is required")
return return
} }
@@ -155,11 +154,11 @@ func (h *FileBrowserHandlers) DeleteMultipleFiles(w http.ResponseWriter, r *http
var errors []string var errors []string
// Delete each file/folder // 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 := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
_, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{ _, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{
Directory: filepath.Dir(path), Directory: path.Dir(p),
Name: filepath.Base(path), Name: path.Base(p),
IsDeleteData: true, IsDeleteData: true,
IsRecursive: true, IsRecursive: true,
IgnoreRecursiveError: false, IgnoreRecursiveError: false,
@@ -169,7 +168,7 @@ func (h *FileBrowserHandlers) DeleteMultipleFiles(w http.ResponseWriter, r *http
if err != nil { if err != nil {
failedCount++ failedCount++
errors = append(errors, fmt.Sprintf("%s: %v", path, err)) errors = append(errors, fmt.Sprintf("%s: %v", p, err))
} else { } else {
deletedCount++ deletedCount++
} }
@@ -230,9 +229,9 @@ func (h *FileBrowserHandlers) CreateFolder(w http.ResponseWriter, r *http.Reques
// Create folder via filer // Create folder via filer
err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
_, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
Directory: filepath.Dir(fullPath), Directory: path.Dir(fullPath),
Entry: &filer_pb.Entry{ Entry: &filer_pb.Entry{
Name: filepath.Base(fullPath), Name: path.Base(fullPath),
IsDirectory: true, IsDirectory: true,
Attributes: &filer_pb.FuseAttributes{ Attributes: &filer_pb.FuseAttributes{
FileMode: uint32(0o755 | os.ModeDir), // Directory mode 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 var fileEntry dash.FileEntry
err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{
Directory: filepath.Dir(filePath), Directory: path.Dir(filePath),
Name: filepath.Base(filePath), Name: path.Base(filePath),
}) })
if err != nil { if err != nil {
return err return err
@@ -549,8 +548,8 @@ func (h *FileBrowserHandlers) GetFileProperties(w http.ResponseWriter, r *http.R
var properties map[string]interface{} var properties map[string]interface{}
err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{ resp, err := client.LookupDirectoryEntry(context.Background(), &filer_pb.LookupDirectoryEntryRequest{
Directory: filepath.Dir(filePath), Directory: path.Dir(filePath),
Name: filepath.Base(filePath), Name: path.Base(filePath),
}) })
if err != nil { if err != nil {
return err return err
+2 -2
View File
@@ -3,7 +3,7 @@ package sftpd
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath" stdpath "path"
"strings" "strings"
"github.com/seaweedfs/seaweedfs/weed/glog" "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, // If the path doesn't exist and we're checking for create/write/mkdir permission,
// check permissions on the parent directory instead // check permissions on the parent directory instead
if err == os.ErrNotExist { if err == os.ErrNotExist {
parentPath := filepath.Dir(path) parentPath := stdpath.Dir(path)
// Check if user can write to the parent directory // Check if user can write to the parent directory
return fs.CheckFilePermission(parentPath, perm) return fs.CheckFilePermission(parentPath, perm)
} }
+2 -2
View File
@@ -3,7 +3,7 @@ package user
import ( import (
"math/rand/v2" "math/rand/v2"
"path/filepath" "path"
) )
// User represents an SFTP user with authentication and permission details // User represents an SFTP user with authentication and permission details
@@ -27,7 +27,7 @@ func NewUser(username string) *User {
return &User{ return &User{
Username: username, Username: username,
Permissions: make(map[string][]string), Permissions: make(map[string][]string),
HomeDir: filepath.Join("/home", username), HomeDir: path.Join("/home", username),
Uid: uint32(randomId), Uid: uint32(randomId),
Gid: uint32(randomId), Gid: uint32(randomId),
} }