Files
seaweedfs/weed/mount/weedfs_dir_mkrm.go
T
Chris Lu 60893c5ef3 Classify a filer error before a user-controlled path is wrapped into it (#11004)
* util, pb: classify a filer error by the status the server sent

DoSeaweedListWithSnapshot wrapped a failed ListEntries with %v, dropping the
gRPC status, so IsTransientError fell back to matching substrings against a
message that now held the caller's path. Keep the status with %w and let it
decide, reading the server's own text rather than the wrapper's.

Claude-Session: https://claude.ai/code/session_01BjDWtZsCoZY6x4pdDmGWxU

* s3: keep the bucket and prefix out of the list retry decision

A bucket named transport, or a prefix under logs/unavailable/, made a
PermissionDenied listing look transient and got it retried; a key holding the
not-found sentence suppressed a retry that should have run. Both checks now
read the filer's status, and only fall back to the text when there is none.

Claude-Session: https://claude.ai/code/session_01BjDWtZsCoZY6x4pdDmGWxU

* filer, s3: classify a delete failure before the path is wrapped into it

The filer put the non-empty-folder marker behind its own "delete directory %s"
wrapper and the gateway matched it as a substring, so a key named after the
marker turned a real delete failure into the demote-the-marker no-op and the
request answered 204. Keep the marker leading the message that crosses the
wire, turn it back into a sentinel where the response is read, and match that.

Claude-Session: https://claude.ai/code/session_01BjDWtZsCoZY6x4pdDmGWxU
2026-08-27 22:30:53 -07:00

185 lines
5.9 KiB
Go

package mount
import (
"context"
"os"
"syscall"
"time"
"github.com/seaweedfs/go-fuse/v2/fuse"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
)
/** Create a directory
*
* Note that the mode argument may not have the type specification
* bits set, i.e. S_ISDIR(mode) can be false. To obtain the
* correct directory type bits use mode|S_IFDIR
* */
func (wfs *WFS) Mkdir(cancel <-chan struct{}, in *fuse.MkdirIn, name string, out *fuse.EntryOut) (code fuse.Status) {
if wfs.IsOverQuotaWithUncommitted() {
return fuse.Status(syscall.ENOSPC)
}
var s fuse.Status
if name, s = checkName(name); s != fuse.OK {
return s
}
now := time.Now().Unix()
dirFullPath, code := wfs.inodeToPath.GetPath(in.NodeId)
if code != fuse.OK {
return
}
entryFullPath := dirFullPath.Child(name)
// Pre-allocate the mount's local inode and stamp it into the create
// request so both the mount and the filer agree on object identity from
// the start. Without this, the filer assigns its own inode in CreateEntry
// and the cached entry then reports a different value than the one we
// return to the kernel here.
inode := wfs.inodeToPath.AllocateInode(entryFullPath, now)
newEntry := &filer_pb.Entry{
Name: name,
IsDirectory: true,
Attributes: &filer_pb.FuseAttributes{
Mtime: now,
Crtime: now,
Ctime: now,
FileMode: uint32(os.ModeDir) | in.Mode,
Uid: in.Uid,
Gid: in.Gid,
Inode: inode,
},
}
wfs.mapPbIdFromLocalToFiler(newEntry)
// Defer restoring to local uid/gid AFTER the entry is sent to the filer
// but BEFORE outputPbEntry writes attributes to the kernel. We restore
// explicitly below instead of using defer so the kernel gets local values.
request := &filer_pb.CreateEntryRequest{
// Defensive: dirFullPath is clean by construction for mount-originated
// mutations, but could carry invalid-UTF-8 bytes if metaCache was
// populated from a non-gRPC source (direct store write, legacy import).
// Sanitizing here keeps the marshal strictly per-request on the off
// chance invalid bytes do reach us.
Directory: dirFullPath.Sanitized(),
Entry: newEntry,
Signatures: []int32{wfs.signature},
SkipCheckParentDirectory: true,
}
glog.V(1).Infof("mkdir: %v", request)
resp, err := wfs.streamCreateEntry(context.Background(), request)
if err != nil {
glog.V(0).Infof("mkdir %s: %v", entryFullPath, err)
} else {
event := resp.GetMetadataEvent()
if event == nil {
event = metadataCreateEvent(string(dirFullPath), newEntry)
}
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
glog.Warningf("mkdir %s: best-effort metadata apply failed: %v", entryFullPath, applyErr)
wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
}
wfs.inodeToPath.TouchDirectory(dirFullPath)
wfs.touchDirMtimeCtimeBest(dirFullPath)
wfs.inodeToPath.AdjustSubdirCount(dirFullPath, 1)
}
glog.V(3).Infof("mkdir %s: %v", entryFullPath, err)
if err != nil {
wfs.mapPbIdFromFilerToLocal(newEntry)
return fuse.EIO
}
// Map uid/gid back to local-space before writing attributes to the
// kernel. The kernel (especially macFUSE) caches these and uses them
// for subsequent permission checks on children.
wfs.mapPbIdFromFilerToLocal(newEntry)
inode = wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, true, false, inode, true)
// The newly created directory is guaranteed to be empty, so mark it as
// cached immediately to avoid a needless filer round-trip on the first
// Lookup or ReadDir inside this directory.
wfs.inodeToPath.MarkChildrenCached(entryFullPath)
wfs.outputPbEntry(out, inode, newEntry)
return fuse.OK
}
/** Remove a directory */
func (wfs *WFS) Rmdir(cancel <-chan struct{}, header *fuse.InHeader, name string) (code fuse.Status) {
if name == "." {
return fuse.Status(syscall.EINVAL)
}
if name == ".." {
return fuse.Status(syscall.ENOTEMPTY)
}
// Sanitize before it reaches DeleteEntryRequest.Name; see sanitizeFuseName.
name = sanitizeFuseName(name)
dirFullPath, code := wfs.inodeToPath.GetPath(header.NodeId)
if code != fuse.OK {
return
}
entryFullPath := dirFullPath.Child(name)
// POSIX: enforce sticky bit on the parent directory.
if dirEntry, _, dirCode := wfs.maybeLoadEntry(dirFullPath); dirCode == fuse.OK && dirEntry != nil && dirEntry.Attributes != nil {
targetUid := uint32(0)
if targetEntry, _, targetCode := wfs.maybeLoadEntry(entryFullPath); targetCode == fuse.OK && targetEntry != nil && targetEntry.Attributes != nil {
targetUid = targetEntry.Attributes.Uid
}
if code := checkStickyBit(dirEntry.Attributes.FileMode, dirEntry.Attributes.Uid, targetUid, header.Uid); code != fuse.OK {
return code
}
}
glog.V(3).Infof("remove directory: %v", entryFullPath)
deleteReq := &filer_pb.DeleteEntryRequest{
Directory: string(dirFullPath),
Name: name,
IsDeleteData: true,
IgnoreRecursiveError: true, // ignore recursion error since the OS should manage it
Signatures: []int32{wfs.signature},
}
resp, err := wfs.streamDeleteEntry(context.Background(), deleteReq)
if err != nil {
glog.V(1).Infof("remove %s: %v", entryFullPath, err)
if filer.IsNonEmptyFolderError(err) {
return fuse.Status(syscall.ENOTEMPTY)
}
return fuse.ENOENT
}
event := metadataDeleteEvent(string(dirFullPath), name, true)
if resp != nil && resp.MetadataEvent != nil {
event = resp.MetadataEvent
}
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
glog.Warningf("rmdir %s: best-effort metadata apply failed: %v", entryFullPath, applyErr)
wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
}
wfs.inodeToPath.RemovePath(entryFullPath)
wfs.inodeToPath.TouchDirectory(dirFullPath)
wfs.touchDirMtimeCtimeBest(dirFullPath)
wfs.inodeToPath.AdjustSubdirCount(dirFullPath, -1)
return fuse.OK
}