Files
seaweedfs/weed/mount/weedfs_attr_mount_root_test.go
T
Yip LeoandClaude Opus 5.5 911bfe4014 mount: stop overwriting the mount root's filer entry (#12623)
When a mount is rooted at an existing directory, two code paths replace
that directory's filer entry with one the mount builds itself:

- every mount start re-creates the root with a non-exclusive mkdir,
  which the filer handles as an update of the whole entry;
- creating or removing an entry directly in the root saves the root's
  entry to touch its mtime, and maybeLoadEntry synthesizes that entry
  from the mount options instead of reading the filer.

The filer keeps only crtime and inode on such an update, so the root
loses its mode, owner and extended attributes. When the root is a
bucket it also loses the bucket quota, which silently drops to 0
(unlimited), and the settings S3 keeps on the bucket entry. The explicit
allowEmptyFolders=false that #11370 meant to keep is replaced by true at
every mount start, because the mkdir has wiped it before
ensureBucketAllowEmptyFolders looks.

Create the root only when a lookup says it is missing, and then
exclusively. Leave the root out of the filer-side mtime touch: its
attributes come from the mount options (setRootAttr), so the filer copy
of its mtime was never visible through the mount anyway.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 12:27:15 -08:00

67 lines
2.1 KiB
Go

package mount
import (
"context"
"os"
"sync"
"testing"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
"google.golang.org/protobuf/proto"
)
// savedEntriesFiler records the entries a mount saves.
type savedEntriesFiler struct {
filer_pb.UnimplementedSeaweedFilerServer
mu sync.Mutex
updates []*filer_pb.UpdateEntryRequest
}
func (s *savedEntriesFiler) UpdateEntry(_ context.Context, req *filer_pb.UpdateEntryRequest) (*filer_pb.UpdateEntryResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.updates = append(s.updates, proto.Clone(req).(*filer_pb.UpdateEntryRequest))
return &filer_pb.UpdateEntryResponse{}, nil
}
// Creating or removing an entry directly in the mount root must not save the
// root: its entry is synthesized from the mount options, so saving it would
// replace the real directory on the filer, wiping a bucket's quota and the
// settings S3 keeps on it. Directories below the root are still saved.
func TestTouchDirMtimeCtimeLeavesTheMountRootOnTheFiler(t *testing.T) {
wfs := newInvalidateTestWFS(t)
root := util.FullPath("/buckets/b1")
wfs.option.FilerMountRootPath = string(root)
fake := &savedEntriesFiler{}
startFakeFiler(t, wfs, fake)
wfs.inodeToPath.Lookup(root, 1, true, false, 0, false)
wfs.inodeToPath.MarkChildrenCached(root)
sub := &filer_pb.Entry{
Name: "sub",
IsDirectory: true,
Attributes: &filer_pb.FuseAttributes{FileMode: uint32(os.ModeDir | 0755), Mtime: 1},
}
if err := wfs.metaCache.InsertEntry(context.Background(), filer.FromPbEntry(string(root), sub), 1); err != nil {
t.Fatal(err)
}
wfs.touchDirMtimeCtime(root)
wfs.touchDirMtimeCtime(root.Child("sub"))
fake.mu.Lock()
defer fake.mu.Unlock()
var saved []string
for _, req := range fake.updates {
saved = append(saved, string(util.NewFullPath(req.Directory, req.Entry.Name)))
}
if len(saved) != 1 || saved[0] != "/buckets/b1/sub" {
t.Fatalf("saved %v, want only /buckets/b1/sub", saved)
}
if fake.updates[0].Entry.Attributes.Mtime <= 1 {
t.Errorf("sub mtime = %d, want it touched", fake.updates[0].Entry.Attributes.Mtime)
}
}