diff --git a/weed/command/filer_remote_gateway_buckets.go b/weed/command/filer_remote_gateway_buckets.go index 53fb50da7..5238e7fc5 100644 --- a/weed/command/filer_remote_gateway_buckets.go +++ b/weed/command/filer_remote_gateway_buckets.go @@ -390,11 +390,10 @@ func (option *RemoteGatewayOptions) detectBucketInfo(actualDir string) (bucket u } func extractBucketPath(bucketsDir, dir string) (util.FullPath, bool) { - if !strings.HasPrefix(dir, bucketsDir+"/") { - return "", false + if bucketPath, ok := util.ExtractBucketPath(bucketsDir, dir, false); ok { + return util.FullPath(bucketPath), true } - parts := strings.SplitN(dir[len(bucketsDir)+1:], "/", 2) - return util.FullPath(bucketsDir).Child(parts[0]), true + return "", false } func (option *RemoteGatewayOptions) collectRemoteStorageConf() (err error) { diff --git a/weed/filer/empty_folder_cleanup/empty_folder_cleaner.go b/weed/filer/empty_folder_cleanup/empty_folder_cleaner.go index 370d5275b..943d86c6f 100644 --- a/weed/filer/empty_folder_cleanup/empty_folder_cleaner.go +++ b/weed/filer/empty_folder_cleanup/empty_folder_cleaner.go @@ -340,7 +340,7 @@ func (efc *EmptyFolderCleaner) deleteFolder(ctx context.Context, folder string) } func (efc *EmptyFolderCleaner) getBucketCleanupPolicy(ctx context.Context, folder string) (bucketPath string, autoRemove bool, source string, attrValue string, err error) { - bucketPath, ok := extractBucketPath(folder, efc.bucketPath) + bucketPath, ok := util.ExtractBucketPath(efc.bucketPath, folder, true) if !ok { return "", true, "default", "", nil } @@ -375,26 +375,6 @@ func (efc *EmptyFolderCleaner) getBucketCleanupPolicy(ctx context.Context, folde return bucketPath, autoRemove, "filer", attrValue, nil } -func extractBucketPath(folder string, bucketPath string) (string, bool) { - if bucketPath == "" { - return "", false - } - - cleanBucketPath := strings.TrimSuffix(bucketPath, "/") - prefix := cleanBucketPath + "/" - if !strings.HasPrefix(folder, prefix) { - return "", false - } - - rest := strings.TrimPrefix(folder, prefix) - bucketName, _, found := strings.Cut(rest, "/") - if !found || bucketName == "" { - return "", false - } - - return prefix + bucketName, true -} - func autoRemoveEmptyFoldersEnabled(attrs map[string][]byte) (bool, string) { if attrs == nil { return true, "" diff --git a/weed/filer/empty_folder_cleanup/empty_folder_cleaner_test.go b/weed/filer/empty_folder_cleanup/empty_folder_cleaner_test.go index 4fdd0687a..25cf1ec68 100644 --- a/weed/filer/empty_folder_cleanup/empty_folder_cleaner_test.go +++ b/weed/filer/empty_folder_cleanup/empty_folder_cleaner_test.go @@ -96,50 +96,6 @@ func Test_isUnderBucketPath(t *testing.T) { } } -func Test_extractBucketPath(t *testing.T) { - tests := []struct { - name string - folder string - bucketRoot string - expected string - ok bool - }{ - { - name: "folder under bucket", - folder: "/buckets/test/a/b", - bucketRoot: "/buckets", - expected: "/buckets/test", - ok: true, - }, - { - name: "bucket root folder should not match", - folder: "/buckets/test", - bucketRoot: "/buckets", - expected: "", - ok: false, - }, - { - name: "outside buckets", - folder: "/data/test/a", - bucketRoot: "/buckets", - expected: "", - ok: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotPath, gotOK := extractBucketPath(tt.folder, tt.bucketRoot) - if gotOK != tt.ok { - t.Fatalf("expected ok=%v, got %v", tt.ok, gotOK) - } - if gotPath != tt.expected { - t.Fatalf("expected path %q, got %q", tt.expected, gotPath) - } - }) - } -} - func Test_autoRemoveEmptyFoldersEnabled(t *testing.T) { tests := []struct { name string @@ -750,7 +706,7 @@ func TestEmptyFolderCleaner_executeCleanup_bucketPolicyDisabledSkips(t *testing. }, attrsFn: func(path util.FullPath) (map[string][]byte, error) { if string(path) == "/buckets/test" { - return map[string][]byte{s3_constants.ExtAllowEmptyFolders: []byte("true")}, nil + return map[string][]byte{s3_constants.ExtAllowEmptyFolders: []byte("true")}, nil } return nil, nil }, diff --git a/weed/util/buckets.go b/weed/util/buckets.go new file mode 100644 index 000000000..8a4189a82 --- /dev/null +++ b/weed/util/buckets.go @@ -0,0 +1,33 @@ +package util + +import "strings" + +// ExtractBucketPath returns the bucket path under basePath that contains target. +// If requireChild is true, the target must include additional segments beyond the bucket itself. +func ExtractBucketPath(basePath, target string, requireChild bool) (string, bool) { + cleanBase := strings.TrimSuffix(basePath, "/") + if cleanBase == "" { + return "", false + } + + prefix := cleanBase + "/" + if !strings.HasPrefix(target, prefix) { + return "", false + } + + rest := strings.TrimPrefix(target, prefix) + if rest == "" { + return "", false + } + + bucketName, _, found := strings.Cut(rest, "/") + if bucketName == "" { + return "", false + } + + if requireChild && !found { + return "", false + } + + return prefix + bucketName, true +} diff --git a/weed/util/buckets_test.go b/weed/util/buckets_test.go new file mode 100644 index 000000000..3ae9d9f51 --- /dev/null +++ b/weed/util/buckets_test.go @@ -0,0 +1,63 @@ +package util + +import "testing" + +func TestExtractBucketPath(t *testing.T) { + for _, tt := range []struct { + name string + base string + target string + requireChild bool + expected string + ok bool + }{ + { + name: "child paths return bucket", + base: "/buckets", + target: "/buckets/test/folder/file", + requireChild: true, + expected: "/buckets/test", + ok: true, + }, + { + name: "bucket root without child fails when required", + base: "/buckets", + target: "/buckets/test", + requireChild: true, + ok: false, + }, + { + name: "bucket root allowed when not required", + base: "/buckets", + target: "/buckets/test", + requireChild: false, + expected: "/buckets/test", + ok: true, + }, + { + name: "path outside buckets fails", + base: "/buckets", + target: "/data/test/folder", + requireChild: true, + ok: false, + }, + { + name: "trailing slash on base is normalized", + base: "/buckets/", + target: "/buckets/test/sub", + requireChild: true, + expected: "/buckets/test", + ok: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + got, ok := ExtractBucketPath(tt.base, tt.target, tt.requireChild) + if ok != tt.ok { + t.Fatalf("expected ok=%v, got %v", tt.ok, ok) + } + if got != tt.expected { + t.Fatalf("expected path %q, got %q", tt.expected, got) + } + }) + } +}