aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Lu <chrislusf@users.noreply.github.com>2025-12-14 11:18:23 -0800
committerGitHub <noreply@github.com>2025-12-14 11:18:23 -0800
commita77674ead3c5f1f9bd21a46f6f90019f178c6a70 (patch)
tree5e99297ace743826b6936d011c32ffa256d95635
parenteb860752e6c0a86131e39d648f0a64364408ab93 (diff)
downloadseaweedfs-a77674ead3c5f1f9bd21a46f6f90019f178c6a70.tar.xz
seaweedfs-a77674ead3c5f1f9bd21a46f6f90019f178c6a70.zip
fix: use path instead of filepath for S3 object paths on Windows (#7739)
fix: use path instead of filepath for S3 object paths on Windows (#7733)
-rw-r--r--weed/s3api/filer_multipart.go6
-rw-r--r--weed/s3api/filer_multipart_test.go62
-rw-r--r--weed/s3api/s3_constants/header.go3
-rw-r--r--weed/s3api/s3_constants/header_test.go132
-rw-r--r--weed/s3api/s3api_object_handlers_put.go8
5 files changed, 202 insertions, 9 deletions
diff --git a/weed/s3api/filer_multipart.go b/weed/s3api/filer_multipart.go
index 8dca4cedc..de6b35ae8 100644
--- a/weed/s3api/filer_multipart.go
+++ b/weed/s3api/filer_multipart.go
@@ -11,7 +11,7 @@ import (
"fmt"
"math"
"net/url"
- "path/filepath"
+ "path"
"slices"
"sort"
"strconv"
@@ -552,8 +552,8 @@ func (s3a *S3ApiServer) completeMultipartUpload(r *http.Request, input *s3.Compl
}
func (s3a *S3ApiServer) getEntryNameAndDir(input *s3.CompleteMultipartUploadInput) (string, string) {
- entryName := filepath.Base(*input.Key)
- dirName := filepath.ToSlash(filepath.Dir(*input.Key))
+ entryName := path.Base(*input.Key)
+ dirName := path.Dir(*input.Key)
if dirName == "." {
dirName = ""
}
diff --git a/weed/s3api/filer_multipart_test.go b/weed/s3api/filer_multipart_test.go
index 7f75a40de..bf4fa3ca2 100644
--- a/weed/s3api/filer_multipart_test.go
+++ b/weed/s3api/filer_multipart_test.go
@@ -1,12 +1,13 @@
package s3api
import (
+ "testing"
+ "time"
+
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
"github.com/stretchr/testify/assert"
- "testing"
- "time"
)
func TestInitiateMultipartUploadResult(t *testing.T) {
@@ -74,3 +75,60 @@ func Test_parsePartNumber(t *testing.T) {
})
}
}
+
+func TestGetEntryNameAndDir(t *testing.T) {
+ s3a := &S3ApiServer{
+ option: &S3ApiServerOption{
+ BucketsPath: "/buckets",
+ },
+ }
+
+ tests := []struct {
+ name string
+ bucket string
+ key string
+ expectedName string
+ expectedDirEnd string // We check the suffix since dir includes BucketsPath
+ }{
+ {
+ name: "simple file at root",
+ bucket: "test-bucket",
+ key: "/file.txt",
+ expectedName: "file.txt",
+ expectedDirEnd: "/buckets/test-bucket",
+ },
+ {
+ name: "file in subdirectory",
+ bucket: "test-bucket",
+ key: "/folder/file.txt",
+ expectedName: "file.txt",
+ expectedDirEnd: "/buckets/test-bucket/folder",
+ },
+ {
+ name: "file in nested subdirectory",
+ bucket: "test-bucket",
+ key: "/folder/subfolder/file.txt",
+ expectedName: "file.txt",
+ expectedDirEnd: "/buckets/test-bucket/folder/subfolder",
+ },
+ {
+ name: "key without leading slash",
+ bucket: "test-bucket",
+ key: "folder/file.txt",
+ expectedName: "file.txt",
+ expectedDirEnd: "/buckets/test-bucket/folder",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ input := &s3.CompleteMultipartUploadInput{
+ Bucket: aws.String(tt.bucket),
+ Key: aws.String(tt.key),
+ }
+ entryName, dirName := s3a.getEntryNameAndDir(input)
+ assert.Equal(t, tt.expectedName, entryName, "entry name mismatch")
+ assert.Equal(t, tt.expectedDirEnd, dirName, "directory mismatch")
+ })
+ }
+}
diff --git a/weed/s3api/s3_constants/header.go b/weed/s3api/s3_constants/header.go
index b7e1be9e5..4b34f397e 100644
--- a/weed/s3api/s3_constants/header.go
+++ b/weed/s3api/s3_constants/header.go
@@ -146,7 +146,10 @@ func GetBucketAndObject(r *http.Request) (bucket, object string) {
// NormalizeObjectKey ensures the object key has a leading slash and no duplicate slashes.
// This normalizes keys from various sources (URL path, form values, etc.) to a consistent format.
+// It also converts Windows-style backslashes to forward slashes for cross-platform compatibility.
func NormalizeObjectKey(object string) string {
+ // Convert Windows-style backslashes to forward slashes
+ object = strings.ReplaceAll(object, "\\", "/")
object = removeDuplicateSlashes(object)
if !strings.HasPrefix(object, "/") {
object = "/" + object
diff --git a/weed/s3api/s3_constants/header_test.go b/weed/s3api/s3_constants/header_test.go
new file mode 100644
index 000000000..b16cfc6a8
--- /dev/null
+++ b/weed/s3api/s3_constants/header_test.go
@@ -0,0 +1,132 @@
+package s3_constants
+
+import (
+ "testing"
+)
+
+func TestNormalizeObjectKey(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "simple key",
+ input: "file.txt",
+ expected: "/file.txt",
+ },
+ {
+ name: "key with leading slash",
+ input: "/file.txt",
+ expected: "/file.txt",
+ },
+ {
+ name: "key with directory",
+ input: "folder/file.txt",
+ expected: "/folder/file.txt",
+ },
+ {
+ name: "key with leading slash and directory",
+ input: "/folder/file.txt",
+ expected: "/folder/file.txt",
+ },
+ {
+ name: "key with duplicate slashes",
+ input: "folder//subfolder///file.txt",
+ expected: "/folder/subfolder/file.txt",
+ },
+ {
+ name: "Windows backslash - simple",
+ input: "folder\\file.txt",
+ expected: "/folder/file.txt",
+ },
+ {
+ name: "Windows backslash - nested",
+ input: "folder\\subfolder\\file.txt",
+ expected: "/folder/subfolder/file.txt",
+ },
+ {
+ name: "Windows backslash - with leading slash",
+ input: "/folder\\subfolder\\file.txt",
+ expected: "/folder/subfolder/file.txt",
+ },
+ {
+ name: "mixed slashes",
+ input: "folder\\subfolder/another\\file.txt",
+ expected: "/folder/subfolder/another/file.txt",
+ },
+ {
+ name: "Windows full path style (edge case)",
+ input: "C:\\Users\\test\\file.txt",
+ expected: "/C:/Users/test/file.txt",
+ },
+ {
+ name: "empty string",
+ input: "",
+ expected: "/",
+ },
+ {
+ name: "just a slash",
+ input: "/",
+ expected: "/",
+ },
+ {
+ name: "just a backslash",
+ input: "\\",
+ expected: "/",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := NormalizeObjectKey(tt.input)
+ if result != tt.expected {
+ t.Errorf("NormalizeObjectKey(%q) = %q, want %q", tt.input, result, tt.expected)
+ }
+ })
+ }
+}
+
+func TestRemoveDuplicateSlashes(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "no duplicates",
+ input: "/folder/file.txt",
+ expected: "/folder/file.txt",
+ },
+ {
+ name: "double slash",
+ input: "/folder//file.txt",
+ expected: "/folder/file.txt",
+ },
+ {
+ name: "triple slash",
+ input: "/folder///file.txt",
+ expected: "/folder/file.txt",
+ },
+ {
+ name: "multiple duplicate locations",
+ input: "//folder//subfolder///file.txt",
+ expected: "/folder/subfolder/file.txt",
+ },
+ {
+ name: "empty string",
+ input: "",
+ expected: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := removeDuplicateSlashes(tt.input)
+ if result != tt.expected {
+ t.Errorf("removeDuplicateSlashes(%q) = %q, want %q", tt.input, result, tt.expected)
+ }
+ })
+ }
+}
+
diff --git a/weed/s3api/s3api_object_handlers_put.go b/weed/s3api/s3api_object_handlers_put.go
index 3da9047ac..7c73f4ce0 100644
--- a/weed/s3api/s3api_object_handlers_put.go
+++ b/weed/s3api/s3api_object_handlers_put.go
@@ -9,7 +9,7 @@ import (
"io"
"net/http"
"net/url"
- "path/filepath"
+ "path"
"strconv"
"strings"
"time"
@@ -491,7 +491,7 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, filePath string, dataReader
// Create entry
entry := &filer_pb.Entry{
- Name: filepath.Base(filePath),
+ Name: path.Base(filePath),
IsDirectory: false,
Attributes: &filer_pb.FuseAttributes{
Crtime: now.Unix(),
@@ -611,10 +611,10 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, filePath string, dataReader
// Use context.Background() to ensure metadata save completes even if HTTP request is cancelled
// This matches the chunk upload behavior and prevents orphaned chunks
glog.V(3).Infof("putToFiler: About to create entry - dir=%s, name=%s, chunks=%d, extended keys=%d",
- filepath.Dir(filePath), filepath.Base(filePath), len(entry.Chunks), len(entry.Extended))
+ path.Dir(filePath), path.Base(filePath), len(entry.Chunks), len(entry.Extended))
createErr := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
req := &filer_pb.CreateEntryRequest{
- Directory: filepath.Dir(filePath),
+ Directory: path.Dir(filePath),
Entry: entry,
}
glog.V(3).Infof("putToFiler: Calling CreateEntry for %s", filePath)