mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-15 19:56:39 +00:00
* fix(filer.sync): scope filesystem key sanitization to the local sink destKey ran every sink key through escapeKey, whose Windows build strips colons. Colons are illegal in NTFS filenames so the local sink needs that, but s3/filer/azure/gcs/b2 accept them as ordinary key bytes — stripping them silently diverged the destination key (a source a:b replicated as ab). Move the sanitization into the local sink behind a Windows build tag, applied at every entry point so the previously-unescaped in-place-update paths stay consistent. Non-local sinks now keep the raw key; non-Windows builds are unchanged; a leading drive-letter colon is preserved. * test(filer.sync): cover incremental destKey and localsink update/delete sanitization Lock the colon-preserving behavior for the incremental destKey branch, and extend the Windows local-sink test to assert UpdateEntry and DeleteEntry also sanitize the key, not just CreateEntry.
144 lines
5.3 KiB
Go
144 lines
5.3 KiB
Go
package command
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/replication/sink"
|
|
"github.com/seaweedfs/seaweedfs/weed/replication/source"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
var _ sink.ReplicationSink = (*recordingSyncSink)(nil)
|
|
|
|
type recordingSyncSink struct {
|
|
deleteKeys []string
|
|
createKeys []string
|
|
updateKeys []string
|
|
incremental bool
|
|
}
|
|
|
|
func (s *recordingSyncSink) GetName() string { return "recording" }
|
|
func (s *recordingSyncSink) Initialize(util.Configuration, string) error {
|
|
return nil
|
|
}
|
|
func (s *recordingSyncSink) DeleteEntry(key string, isDirectory, deleteIncludeChunks bool, signatures []int32) error {
|
|
s.deleteKeys = append(s.deleteKeys, key)
|
|
return nil
|
|
}
|
|
func (s *recordingSyncSink) CreateEntry(key string, entry *filer_pb.Entry, signatures []int32) error {
|
|
s.createKeys = append(s.createKeys, key)
|
|
return nil
|
|
}
|
|
func (s *recordingSyncSink) UpdateEntry(key string, oldEntry *filer_pb.Entry, newParentPath string, newEntry *filer_pb.Entry, deleteIncludeChunks bool, signatures []int32) (bool, error) {
|
|
s.updateKeys = append(s.updateKeys, key)
|
|
return true, nil
|
|
}
|
|
func (s *recordingSyncSink) GetSinkToDirectory() string { return "/dest" }
|
|
func (s *recordingSyncSink) SetSourceFiler(*source.FilerSource) {}
|
|
func (s *recordingSyncSink) IsIncremental() bool { return s.incremental }
|
|
|
|
// TestDestKeyPreservesColonForNonLocalSink guards against the command layer
|
|
// stripping colons from keys destined for non-local sinks. Colon
|
|
// sanitization belongs to the local filesystem sink only.
|
|
func TestDestKeyPreservesColonForNonLocalSink(t *testing.T) {
|
|
t.Run("non-incremental", func(t *testing.T) {
|
|
got := destKey(&recordingSyncSink{}, "/backup", "/src", util.FullPath("/src/2024:01:02/file:name.txt"), 0)
|
|
want := "/backup/2024:01:02/file:name.txt"
|
|
if got != want {
|
|
t.Errorf("destKey() = %q, want %q", got, want)
|
|
}
|
|
})
|
|
t.Run("incremental", func(t *testing.T) {
|
|
got := destKey(&recordingSyncSink{incremental: true}, "/backup", "/src", util.FullPath("/src/file:name.txt"), 0)
|
|
// the date partition varies by timezone; assert the colon-bearing name survives.
|
|
if !strings.HasPrefix(got, "/backup/") || !strings.HasSuffix(got, "/file:name.txt") {
|
|
t.Errorf("destKey() = %q, want /backup/<date>/file:name.txt with colon preserved", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPathIsEqualOrUnderUsesDirectoryBoundaries(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
candidate string
|
|
other string
|
|
expected bool
|
|
}{
|
|
{name: "equal", candidate: "/foo", other: "/foo", expected: true},
|
|
{name: "descendant", candidate: "/foo/bar", other: "/foo", expected: true},
|
|
{name: "sibling prefix", candidate: "/foobar/bar", other: "/foo", expected: false},
|
|
{name: "root", candidate: "/foo/bar", other: "/", expected: true},
|
|
{name: "empty", candidate: "", other: "/foo", expected: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := util.IsEqualOrUnder(tt.candidate, tt.other); got != tt.expected {
|
|
t.Fatalf("IsEqualOrUnder(%q, %q) = %v, want %v", tt.candidate, tt.other, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMatchesExcludePathUsesDirectoryBoundaries(t *testing.T) {
|
|
if !matchesExcludePath("/tmp", []string{"/tmp"}) {
|
|
t.Fatal("expected exact directory match to be excluded")
|
|
}
|
|
if !matchesExcludePath("/tmp/sub", []string{"/tmp"}) {
|
|
t.Fatal("expected descendant directory to be excluded")
|
|
}
|
|
if matchesExcludePath("/tmp2/sub", []string{"/tmp"}) {
|
|
t.Fatal("did not expect sibling directory to be excluded")
|
|
}
|
|
}
|
|
|
|
func TestGenProcessFunctionRenameToSiblingPrefixBecomesDelete(t *testing.T) {
|
|
dataSink := &recordingSyncSink{}
|
|
processFn := genProcessFunction("/foo", "/dest", nil, nil, nil, nil, dataSink, true, false)
|
|
|
|
err := processFn(&filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/foo/dir",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
OldEntry: &filer_pb.Entry{Name: "file.txt"},
|
|
NewEntry: &filer_pb.Entry{Name: "file.txt"},
|
|
NewParentPath: "/foobar/dir",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("processFn rename to sibling prefix: %v", err)
|
|
}
|
|
|
|
if len(dataSink.deleteKeys) != 1 || dataSink.deleteKeys[0] != "/dest/dir/file.txt" {
|
|
t.Fatalf("delete keys = %v, want [/dest/dir/file.txt]", dataSink.deleteKeys)
|
|
}
|
|
if len(dataSink.createKeys) != 0 || len(dataSink.updateKeys) != 0 {
|
|
t.Fatalf("unexpected create/update calls: creates=%v updates=%v", dataSink.createKeys, dataSink.updateKeys)
|
|
}
|
|
}
|
|
|
|
func TestGenProcessFunctionRenameFromExcludedDirBecomesCreate(t *testing.T) {
|
|
dataSink := &recordingSyncSink{}
|
|
processFn := genProcessFunction("/foo", "/dest", []string{"/foo/excluded"}, nil, nil, nil, dataSink, true, false)
|
|
|
|
err := processFn(&filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/foo/excluded",
|
|
EventNotification: &filer_pb.EventNotification{
|
|
OldEntry: &filer_pb.Entry{Name: "file.txt"},
|
|
NewEntry: &filer_pb.Entry{Name: "file.txt"},
|
|
NewParentPath: "/foo/live",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("processFn rename from excluded dir: %v", err)
|
|
}
|
|
|
|
if len(dataSink.createKeys) != 1 || dataSink.createKeys[0] != "/dest/live/file.txt" {
|
|
t.Fatalf("create keys = %v, want [/dest/live/file.txt]", dataSink.createKeys)
|
|
}
|
|
if len(dataSink.deleteKeys) != 0 || len(dataSink.updateKeys) != 0 {
|
|
t.Fatalf("unexpected delete/update calls: deletes=%v updates=%v", dataSink.deleteKeys, dataSink.updateKeys)
|
|
}
|
|
}
|