From 28ac536280a2d4920da9211a0d450c64f0ed19be Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 5 Dec 2025 17:40:32 -0800 Subject: [PATCH] fix: normalize Windows backslash paths in weed admin file uploads (#7636) fix: normalize Windows backslash paths in file uploads When uploading files from a Windows client to a Linux server, file paths containing backslashes were not being properly interpreted as directory separators. This caused files intended for subdirectories to be created in the root directory with backslashes in their filenames. Changes: - Add util.CleanWindowsPath and util.CleanWindowsPathBase helper functions in weed/util/fullpath.go for reusable path normalization - Use path.Join/path.Clean/path.Base instead of filepath equivalents for URL path semantics (filepath is OS-specific) - Apply normalization in weed admin handlers and filer upload parsing Fixes #7628 --- weed/admin/handlers/file_browser_handlers.go | 22 +++++++++++++++----- weed/storage/needle/needle_parse_upload.go | 6 +++--- weed/util/fullpath.go | 13 ++++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/weed/admin/handlers/file_browser_handlers.go b/weed/admin/handlers/file_browser_handlers.go index bafaa60c3..eeb8e2d85 100644 --- a/weed/admin/handlers/file_browser_handlers.go +++ b/weed/admin/handlers/file_browser_handlers.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "os" + "path" "path/filepath" "strconv" "strings" @@ -21,6 +22,7 @@ import ( "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/util" "github.com/seaweedfs/seaweedfs/weed/util/http/client" ) @@ -267,8 +269,12 @@ func (h *FileBrowserHandlers) UploadFile(c *gin.Context) { continue } - // Create full path for the file - fullPath := filepath.Join(currentPath, fileName) + // Normalize Windows-style backslashes to forward slashes + fileName = util.CleanWindowsPath(fileName) + + // Create full path for the file using path.Join for URL path semantics + // path.Join handles double slashes and is not OS-specific like filepath.Join + fullPath := path.Join(currentPath, fileName) if !strings.HasPrefix(fullPath, "/") { fullPath = "/" + fullPath } @@ -349,8 +355,10 @@ func (h *FileBrowserHandlers) uploadFileToFiler(filePath string, fileHeader *mul var body bytes.Buffer writer := multipart.NewWriter(&body) - // Create form file field - part, err := writer.CreateFormFile("file", fileHeader.Filename) + // Create form file field with normalized base filename + // Use path.Base (not filepath.Base) since cleanFilePath uses URL path semantics + baseFileName := path.Base(cleanFilePath) + part, err := writer.CreateFormFile("file", baseFileName) if err != nil { return fmt.Errorf("failed to create form file: %w", err) } @@ -452,8 +460,12 @@ func (h *FileBrowserHandlers) validateAndCleanFilePath(filePath string) (string, return "", fmt.Errorf("file path cannot be empty") } + // Normalize Windows-style backslashes to forward slashes + filePath = util.CleanWindowsPath(filePath) + // Clean the path to remove any .. or . components - cleanPath := filepath.Clean(filePath) + // Use path.Clean (not filepath.Clean) since this is a URL path + cleanPath := path.Clean(filePath) // Ensure the path starts with / if !strings.HasPrefix(cleanPath, "/") { diff --git a/weed/storage/needle/needle_parse_upload.go b/weed/storage/needle/needle_parse_upload.go index 89708303d..6fadd80d6 100644 --- a/weed/storage/needle/needle_parse_upload.go +++ b/weed/storage/needle/needle_parse_upload.go @@ -128,7 +128,7 @@ func parseUpload(r *http.Request, sizeLimit int64, pu *ParsedUpload) (e error) { pu.FileName = part.FileName() if pu.FileName != "" { - pu.FileName = path.Base(pu.FileName) + pu.FileName = util.CleanWindowsPathBase(pu.FileName) } dataSize, e = pu.bytesBuffer.ReadFrom(io.LimitReader(part, sizeLimit+1)) @@ -169,7 +169,7 @@ func parseUpload(r *http.Request, sizeLimit int64, pu *ParsedUpload) (e error) { // update pu.Data = pu.bytesBuffer.Bytes() - pu.FileName = path.Base(fName) + pu.FileName = util.CleanWindowsPathBase(fName) contentType = part.Header.Get("Content-Type") part = part2 break @@ -207,7 +207,7 @@ func parseUpload(r *http.Request, sizeLimit int64, pu *ParsedUpload) (e error) { } if pu.FileName != "" { - pu.FileName = path.Base(pu.FileName) + pu.FileName = util.CleanWindowsPathBase(pu.FileName) } else { pu.FileName = path.Base(r.URL.Path) } diff --git a/weed/util/fullpath.go b/weed/util/fullpath.go index c145919da..b485cae0d 100644 --- a/weed/util/fullpath.go +++ b/weed/util/fullpath.go @@ -1,6 +1,7 @@ package util import ( + "path" "path/filepath" "strings" ) @@ -85,3 +86,15 @@ func StringSplit(separatedValues string, sep string) []string { } return strings.Split(separatedValues, sep) } + +// CleanWindowsPath normalizes Windows-style backslashes to forward slashes. +// This handles paths from Windows clients where paths use backslashes. +func CleanWindowsPath(p string) string { + return strings.ReplaceAll(p, "\\", "/") +} + +// CleanWindowsPathBase normalizes Windows-style backslashes to forward slashes +// and returns the base name of the path. +func CleanWindowsPathBase(p string) string { + return path.Base(strings.ReplaceAll(p, "\\", "/")) +}