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)
This commit is contained in:
@@ -11,7 +11,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -552,8 +552,8 @@ func (s3a *S3ApiServer) completeMultipartUpload(r *http.Request, input *s3.Compl
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s3a *S3ApiServer) getEntryNameAndDir(input *s3.CompleteMultipartUploadInput) (string, string) {
|
func (s3a *S3ApiServer) getEntryNameAndDir(input *s3.CompleteMultipartUploadInput) (string, string) {
|
||||||
entryName := filepath.Base(*input.Key)
|
entryName := path.Base(*input.Key)
|
||||||
dirName := filepath.ToSlash(filepath.Dir(*input.Key))
|
dirName := path.Dir(*input.Key)
|
||||||
if dirName == "." {
|
if dirName == "." {
|
||||||
dirName = ""
|
dirName = ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package s3api
|
package s3api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
"github.com/aws/aws-sdk-go/service/s3"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInitiateMultipartUploadResult(t *testing.T) {
|
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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
// 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.
|
// 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 {
|
func NormalizeObjectKey(object string) string {
|
||||||
|
// Convert Windows-style backslashes to forward slashes
|
||||||
|
object = strings.ReplaceAll(object, "\\", "/")
|
||||||
object = removeDuplicateSlashes(object)
|
object = removeDuplicateSlashes(object)
|
||||||
if !strings.HasPrefix(object, "/") {
|
if !strings.HasPrefix(object, "/") {
|
||||||
object = "/" + object
|
object = "/" + object
|
||||||
|
|||||||
132
weed/s3api/s3_constants/header_test.go
Normal file
132
weed/s3api/s3_constants/header_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -491,7 +491,7 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, filePath string, dataReader
|
|||||||
|
|
||||||
// Create entry
|
// Create entry
|
||||||
entry := &filer_pb.Entry{
|
entry := &filer_pb.Entry{
|
||||||
Name: filepath.Base(filePath),
|
Name: path.Base(filePath),
|
||||||
IsDirectory: false,
|
IsDirectory: false,
|
||||||
Attributes: &filer_pb.FuseAttributes{
|
Attributes: &filer_pb.FuseAttributes{
|
||||||
Crtime: now.Unix(),
|
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
|
// Use context.Background() to ensure metadata save completes even if HTTP request is cancelled
|
||||||
// This matches the chunk upload behavior and prevents orphaned chunks
|
// 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",
|
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 {
|
createErr := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||||
req := &filer_pb.CreateEntryRequest{
|
req := &filer_pb.CreateEntryRequest{
|
||||||
Directory: filepath.Dir(filePath),
|
Directory: path.Dir(filePath),
|
||||||
Entry: entry,
|
Entry: entry,
|
||||||
}
|
}
|
||||||
glog.V(3).Infof("putToFiler: Calling CreateEntry for %s", filePath)
|
glog.V(3).Infof("putToFiler: Calling CreateEntry for %s", filePath)
|
||||||
|
|||||||
Reference in New Issue
Block a user