mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* 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
185 lines
5.9 KiB
Go
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
|
|
|
|
}
|