package leveldb import ( "context" "errors" "os" "testing" "github.com/seaweedfs/seaweedfs/weed/filer" "github.com/seaweedfs/seaweedfs/weed/filer/empty_folder_cleanup" "github.com/seaweedfs/seaweedfs/weed/pb" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/util" ) // hookStore runs a hook once, right after the store call it is attached to returns, // so a test can act inside a window that is otherwise not reachable. type hookStore struct { filer.FilerStore afterList func() listFired bool afterInsert func(entry *filer.Entry) insertFired bool } func (s *hookStore) ListDirectoryPrefixedEntries(ctx context.Context, dirPath util.FullPath, startFileName string, includeStartFile bool, limit int64, prefix string, eachEntryFunc filer.ListEachEntryFunc) (string, error) { lastFileName, err := s.FilerStore.ListDirectoryPrefixedEntries(ctx, dirPath, startFileName, includeStartFile, limit, prefix, eachEntryFunc) if s.afterList != nil && !s.listFired { s.listFired = true s.afterList() } return lastFileName, err } func (s *hookStore) InsertEntry(ctx context.Context, entry *filer.Entry) error { err := s.FilerStore.InsertEntry(ctx, entry) if s.afterInsert != nil && !s.insertFired { s.insertFired = true s.afterInsert(entry) } return err } // TestNonRecursiveFolderDeleteKeepsRacingChild covers the S3 empty-folder // cleanup path: the delete lists a folder, finds it empty, and must not then // bulk-delete its children, because an object written in between would be // destroyed after the write was already acknowledged. func TestNonRecursiveFolderDeleteKeepsRacingChild(t *testing.T) { testFiler := filer.NewFiler(pb.ServerDiscovery{}, nil, "", "", "", "", "", 255, nil) store := &LevelDB2Store{} if err := store.initialize(t.TempDir(), 2); err != nil { t.Fatal(err) } hooked := &hookStore{FilerStore: store} testFiler.SetStore(hooked) // the test has no metadata log consumer ctx := filer.WithSuppressedMetadataEvents(context.Background()) dir := util.FullPath("/buckets/testbucket/data/abc") child := dir.Child("obj") dirEntry := &filer.Entry{FullPath: dir, Attr: filer.Attr{Mode: os.ModeDir | 0755}} if err := testFiler.CreateEntry(ctx, dirEntry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err != nil { t.Fatalf("create folder: %v", err) } hooked.afterList = func() { entry := &filer.Entry{FullPath: child, Attr: filer.Attr{Mode: 0640}} if err := testFiler.CreateEntry(ctx, entry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err != nil { t.Errorf("create entry racing the folder delete: %v", err) } } if err := testFiler.DeleteEntryMetaAndData(ctx, dir, false, false, false, false, nil, 0); err != nil { t.Fatalf("delete empty folder: %v", err) } if _, err := testFiler.FindEntry(ctx, child); err != nil { t.Errorf("entry created during the folder delete was removed: %v", err) } // The folder entry itself still goes, so the surviving entry is reachable by // path but absent from listings until the folder comes back. Pinning that here // keeps the remaining exposure visible; tighten it if the two steps ever become // atomic. if _, err := testFiler.FindEntry(ctx, dir); !errors.Is(err, filer_pb.ErrNotFound) { t.Errorf("folder entry should still be removed, got %v", err) } } // TestEnsureDirectoryEntryRestoresRacingParent covers the leftover of the same // race: the entry survives the folder delete but its directory does not, so the // entry drops out of listings until the directory is put back. func TestEnsureDirectoryEntryRestoresRacingParent(t *testing.T) { testFiler := filer.NewFiler(pb.ServerDiscovery{}, nil, "", "", "", "", "", 255, nil) store := &LevelDB2Store{} if err := store.initialize(t.TempDir(), 2); err != nil { t.Fatal(err) } hooked := &hookStore{FilerStore: store} testFiler.SetStore(hooked) ctx := filer.WithSuppressedMetadataEvents(context.Background()) parent := util.FullPath("/buckets/testbucket/data") dir := parent.Child("abc") child := dir.Child("obj") dirEntry := &filer.Entry{FullPath: dir, Attr: filer.Attr{Mode: os.ModeDir | 0755}} if err := testFiler.CreateEntry(ctx, dirEntry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err != nil { t.Fatalf("create folder: %v", err) } // the cleaner reads these before deleting, so a restore does not have to guess attrs, err := testFiler.DirectoryAttributes(ctx, dir) if err != nil { t.Fatalf("read folder attributes: %v", err) } hooked.afterList = func() { entry := &filer.Entry{FullPath: child, Attr: filer.Attr{Mode: 0640}} if err := testFiler.CreateEntry(ctx, entry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err != nil { t.Errorf("create entry racing the folder delete: %v", err) } } if err := testFiler.DeleteEntryMetaAndData(ctx, dir, false, false, false, false, nil, 0); err != nil { t.Fatalf("delete empty folder: %v", err) } if names := listNames(ctx, t, testFiler, parent); len(names) != 0 { t.Fatalf("folder should be gone from its parent before the restore, got %v", names) } if err := testFiler.EnsureDirectoryEntry(ctx, dir, attrs); err != nil { t.Fatalf("restore folder: %v", err) } restored, err := testFiler.FindEntry(ctx, dir) if err != nil { t.Fatalf("find restored folder: %v", err) } if !restored.IsDirectory() { t.Errorf("restored %s is not a directory", dir) } if names := listNames(ctx, t, testFiler, parent); len(names) != 1 || names[0] != "abc" { t.Errorf("restored folder should be listed under its parent, got %v", names) } } // TestEnsureDirectoryEntryRestoresExactAttributes checks that a restored directory // comes back exactly as it was. Guessing from an ancestor, or forcing traversal bits, // would hand back a directory that grants access the original one denied. func TestEnsureDirectoryEntryRestoresExactAttributes(t *testing.T) { testFiler := filer.NewFiler(pb.ServerDiscovery{}, nil, "", "", "", "", "", 255, nil) store := &LevelDB2Store{} if err := store.initialize(t.TempDir(), 2); err != nil { t.Fatal(err) } testFiler.SetStore(store) ctx := filer.WithSuppressedMetadataEvents(context.Background()) // a private directory under a world-traversable parent dir := util.FullPath("/buckets/testbucket/data").Child("private") dirEntry := &filer.Entry{FullPath: dir, Attr: filer.Attr{Mode: os.ModeDir | 0700, Uid: 4242, Gid: 4343}} if err := testFiler.CreateEntry(ctx, dirEntry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err != nil { t.Fatalf("create folder: %v", err) } attrs, err := testFiler.DirectoryAttributes(ctx, dir) if err != nil { t.Fatalf("read folder attributes: %v", err) } if err := testFiler.DeleteEntryMetaAndData(ctx, dir, false, false, false, false, nil, 0); err != nil { t.Fatalf("delete folder: %v", err) } if err := testFiler.EnsureDirectoryEntry(ctx, dir, attrs); err != nil { t.Fatalf("restore folder: %v", err) } restored, err := testFiler.FindEntry(ctx, dir) if err != nil { t.Fatalf("find restored folder: %v", err) } if !restored.IsDirectory() { t.Errorf("restored %s is not a directory", dir) } if restored.Mode.Perm() != 0700 { t.Errorf("restored folder should keep mode 0700, got %o", restored.Mode.Perm()) } if restored.Uid != 4242 || restored.Gid != 4343 { t.Errorf("restored folder should keep its owner, got uid=%d gid=%d", restored.Uid, restored.Gid) } } // TestCreateEntryCreatesParentAfterTheEntry covers the writer's half of the pair: the // folder is taken while the entry is landing, which used to orphan it. func TestCreateEntryCreatesParentAfterTheEntry(t *testing.T) { testFiler := filer.NewFiler(pb.ServerDiscovery{}, nil, "", "", "", "", "", 255, nil) store := &LevelDB2Store{} if err := store.initialize(t.TempDir(), 2); err != nil { t.Fatal(err) } hooked := &hookStore{FilerStore: store} testFiler.SetStore(hooked) ctx := filer.WithSuppressedMetadataEvents(context.Background()) parent := util.FullPath("/buckets/testbucket/data") dir := parent.Child("abc") child := dir.Child("obj") dirEntry := &filer.Entry{FullPath: dir, Attr: filer.Attr{Mode: os.ModeDir | 0755}} if err := testFiler.CreateEntry(ctx, dirEntry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err != nil { t.Fatalf("create folder: %v", err) } // the cleaner found the folder empty a moment ago and removes it now hooked.afterInsert = func(entry *filer.Entry) { if entry.FullPath != child { return } if err := store.DeleteEntry(ctx, dir); err != nil { t.Errorf("delete folder racing the insert: %v", err) } } entry := &filer.Entry{FullPath: child, Attr: filer.Attr{Mode: 0640}} if err := testFiler.CreateEntry(ctx, entry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err != nil { t.Fatalf("create entry racing the folder delete: %v", err) } if _, err := testFiler.FindEntry(ctx, child); err != nil { t.Fatalf("entry created during the folder delete is gone: %v", err) } restored, err := testFiler.FindEntry(ctx, dir) if err != nil { t.Fatalf("parent taken during the insert should be created after it: %v", err) } if !restored.IsDirectory() { t.Errorf("recreated %s is not a directory", dir) } if names := listNames(ctx, t, testFiler, parent); len(names) != 1 || names[0] != "abc" { t.Errorf("entry should be reachable by listing again, got %v", names) } } // TestCreateEntryKeepsTheEntryWhenTheParentCannotBeCreated pins the cost of going // second: the entry is already stored when the parent fails, and it is left there, // since nothing can tell it apart from a concurrent create that has succeeded. func TestCreateEntryKeepsTheEntryWhenTheParentCannotBeCreated(t *testing.T) { testFiler := filer.NewFiler(pb.ServerDiscovery{}, nil, "", "", "", "", "", 255, nil) store := &LevelDB2Store{} if err := store.initialize(t.TempDir(), 2); err != nil { t.Fatal(err) } testFiler.SetStore(store) ctx := filer.WithSuppressedMetadataEvents(context.Background()) // an invalid bucket name is rejected while the parents are being created child := util.FullPath("/buckets/Not_A_Valid_Bucket/obj") entry := &filer.Entry{FullPath: child, Attr: filer.Attr{Mode: 0640}} if err := testFiler.CreateEntry(ctx, entry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err == nil { t.Fatal("an invalid bucket name should still fail the create") } if _, err := testFiler.FindEntry(ctx, child); err != nil { t.Errorf("the entry is kept rather than risking a concurrent create: %v", err) } } // TestCreateEntryDoesNotAnnounceAnEntryWhoseParentFailed pins the other half of keeping // the entry: it stays off the log. The aggregator replicates creates into peer stores, // so announcing a create that failed would put the parentless row on every filer. func TestCreateEntryDoesNotAnnounceAnEntryWhoseParentFailed(t *testing.T) { testFiler := filer.NewFiler(pb.ServerDiscovery{}, nil, "", "", "", "", "", 255, nil) store := &LevelDB2Store{} if err := store.initialize(t.TempDir(), 2); err != nil { t.Fatal(err) } testFiler.SetStore(store) ctx, sink := filer.WithMetadataEventSink(context.Background()) child := util.FullPath("/buckets/Not_A_Valid_Bucket/obj") entry := &filer.Entry{FullPath: child, Attr: filer.Attr{Mode: 0640}} if err := testFiler.CreateEntry(ctx, entry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err == nil { t.Fatal("an invalid bucket name should still fail the create") } if event := sink.Last(); event != nil { t.Errorf("a create that failed must not be announced, got %+v", event.EventNotification) } if _, err := testFiler.FindEntry(ctx, child); err != nil { t.Errorf("the entry itself is still kept: %v", err) } } // TestEnsureDirectoryEntryNarrowsAWiderRestore covers a writer getting to the missing // parent first and minting it wider than the one that was deleted. func TestEnsureDirectoryEntryNarrowsAWiderRestore(t *testing.T) { testFiler := filer.NewFiler(pb.ServerDiscovery{}, nil, "", "", "", "", "", 255, nil) store := &LevelDB2Store{} if err := store.initialize(t.TempDir(), 2); err != nil { t.Fatal(err) } testFiler.SetStore(store) ctx := filer.WithSuppressedMetadataEvents(context.Background()) dir := util.FullPath("/buckets/testbucket/data").Child("private") dirEntry := &filer.Entry{FullPath: dir, Attr: filer.Attr{Mode: os.ModeDir | 0700, Uid: 4242, Gid: 4343}} if err := testFiler.CreateEntry(ctx, dirEntry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err != nil { t.Fatalf("create folder: %v", err) } attrs, err := testFiler.DirectoryAttributes(ctx, dir) if err != nil { t.Fatalf("read folder attributes: %v", err) } if err := testFiler.DeleteEntryMetaAndData(ctx, dir, false, false, false, false, nil, 0); err != nil { t.Fatalf("delete folder: %v", err) } // the writer's own entry is 0640, so the parent it mints comes back 0751 entry := &filer.Entry{FullPath: dir.Child("obj"), Attr: filer.Attr{Mode: 0640}} if err := testFiler.CreateEntry(ctx, entry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err != nil { t.Fatalf("create entry: %v", err) } widened, err := testFiler.FindEntry(ctx, dir) if err != nil { t.Fatalf("find minted folder: %v", err) } if widened.Mode.Perm() == 0700 { t.Skip("the writer no longer widens the parent, nothing left to narrow") } if err := testFiler.EnsureDirectoryEntry(ctx, dir, attrs); err != nil { t.Fatalf("restore folder: %v", err) } restored, err := testFiler.FindEntry(ctx, dir) if err != nil { t.Fatalf("find restored folder: %v", err) } if !restored.IsDirectory() { t.Errorf("restored %s is not a directory", dir) } if restored.Mode.Perm() != 0700 { t.Errorf("restore should hand back mode 0700, got %o", restored.Mode.Perm()) } if _, err := testFiler.FindEntry(ctx, entry.FullPath); err != nil { t.Errorf("narrowing the folder should not disturb what it holds: %v", err) } } // TestEnsureDirectoryEntryKeepsATightenedDirectory checks the other direction: a // restore only ever narrows, so a deliberate tightening is left alone. func TestEnsureDirectoryEntryKeepsATightenedDirectory(t *testing.T) { testFiler := filer.NewFiler(pb.ServerDiscovery{}, nil, "", "", "", "", "", 255, nil) store := &LevelDB2Store{} if err := store.initialize(t.TempDir(), 2); err != nil { t.Fatal(err) } testFiler.SetStore(store) ctx := filer.WithSuppressedMetadataEvents(context.Background()) dir := util.FullPath("/buckets/testbucket/data").Child("tightened") dirEntry := &filer.Entry{FullPath: dir, Attr: filer.Attr{Mode: os.ModeDir | 0700}} if err := testFiler.CreateEntry(ctx, dirEntry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err != nil { t.Fatalf("create folder: %v", err) } if err := testFiler.EnsureDirectoryEntry(ctx, dir, empty_folder_cleanup.DirectoryAttributes{Mode: 0755}); err != nil { t.Fatalf("restore folder: %v", err) } kept, err := testFiler.FindEntry(ctx, dir) if err != nil { t.Fatalf("find folder: %v", err) } if kept.Mode.Perm() != 0700 { t.Errorf("a directory tightened since should keep mode 0700, got %o", kept.Mode.Perm()) } } // TestEnsureDirectoryEntryIntersectsMixedModes covers modes that are not nested, where // replacing rather than intersecting would grant a bit the directory denied. func TestEnsureDirectoryEntryIntersectsMixedModes(t *testing.T) { testFiler := filer.NewFiler(pb.ServerDiscovery{}, nil, "", "", "", "", "", 255, nil) store := &LevelDB2Store{} if err := store.initialize(t.TempDir(), 2); err != nil { t.Fatal(err) } testFiler.SetStore(store) ctx := filer.WithSuppressedMetadataEvents(context.Background()) dir := util.FullPath("/buckets/testbucket/data").Child("mixed") dirEntry := &filer.Entry{FullPath: dir, Attr: filer.Attr{Mode: os.ModeDir | 0705}} if err := testFiler.CreateEntry(ctx, dirEntry, nil, false, false, nil, false, testFiler.MaxFilenameLength); err != nil { t.Fatalf("create folder: %v", err) } // 0750 grants group access that 0705 denies, and denies the other access it grants if err := testFiler.EnsureDirectoryEntry(ctx, dir, empty_folder_cleanup.DirectoryAttributes{Mode: 0750}); err != nil { t.Fatalf("restore folder: %v", err) } restored, err := testFiler.FindEntry(ctx, dir) if err != nil { t.Fatalf("find folder: %v", err) } if restored.Mode.Perm() != 0700 { t.Errorf("restore should grant only what both modes allow, got %o", restored.Mode.Perm()) } } func listNames(ctx context.Context, t *testing.T, f *filer.Filer, dir util.FullPath) []string { t.Helper() entries, _, err := f.ListDirectoryEntries(ctx, dir, "", false, 100, "", "", "") if err != nil { t.Fatalf("list %s: %v", dir, err) } var names []string for _, entry := range entries { names = append(names, entry.Name()) } return names }