* mount: let filer handle chunk deletion decision Remove chunk deletion decision from FUSE mount's Unlink operation. Previously, the mount decided whether to delete chunks based on its locally cached entry's HardLinkCounter, which could be stale. Now always pass isDeleteData=true and let the filer make the authoritative decision based on its own data. This prevents potential inconsistencies when: - The FUSE mount's cached entry is stale - Race conditions occur between multiple mounts - Direct filer operations change hard link counts * filer: check hard link counter before deleting chunks When deleting an entry, only delete the underlying chunks if: 1. It is not a hard link 2. OR it is the last hard link (counter <= 1) This protects against data loss when a client (like FUSE mount) requests chunk deletion for a file that has multiple hard links.
162 lines
4.3 KiB
Go
162 lines
4.3 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/hanwen/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 and open a file
|
|
*
|
|
* If the file does not exist, first create it with the specified
|
|
* mode, and then open it.
|
|
*
|
|
* If this method is not implemented or under Linux kernel
|
|
* versions earlier than 2.6.15, the mknod() and open() methods
|
|
* will be called instead.
|
|
*/
|
|
func (wfs *WFS) Create(cancel <-chan struct{}, in *fuse.CreateIn, name string, out *fuse.CreateOut) (code fuse.Status) {
|
|
// if implemented, need to use
|
|
// inode := wfs.inodeToPath.Lookup(entryFullPath)
|
|
// to ensure nlookup counter
|
|
return fuse.ENOSYS
|
|
}
|
|
|
|
/** Create a file node
|
|
*
|
|
* This is called for creation of all non-directory, non-symlink
|
|
* nodes. If the filesystem defines a create() method, then for
|
|
* regular files that will be called instead.
|
|
*/
|
|
func (wfs *WFS) Mknod(cancel <-chan struct{}, in *fuse.MknodIn, name string, out *fuse.EntryOut) (code fuse.Status) {
|
|
|
|
if wfs.IsOverQuotaWithUncommitted() {
|
|
return fuse.Status(syscall.ENOSPC)
|
|
}
|
|
|
|
if s := checkName(name); s != fuse.OK {
|
|
return s
|
|
}
|
|
|
|
dirFullPath, code := wfs.inodeToPath.GetPath(in.NodeId)
|
|
if code != fuse.OK {
|
|
return
|
|
}
|
|
|
|
entryFullPath := dirFullPath.Child(name)
|
|
fileMode := toOsFileMode(in.Mode)
|
|
now := time.Now().Unix()
|
|
inode := wfs.inodeToPath.AllocateInode(entryFullPath, now)
|
|
|
|
newEntry := &filer_pb.Entry{
|
|
Name: name,
|
|
IsDirectory: false,
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Mtime: now,
|
|
Crtime: now,
|
|
FileMode: uint32(fileMode),
|
|
Uid: in.Uid,
|
|
Gid: in.Gid,
|
|
TtlSec: wfs.option.TtlSec,
|
|
Rdev: in.Rdev,
|
|
Inode: inode,
|
|
},
|
|
}
|
|
|
|
err := wfs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
|
|
|
wfs.mapPbIdFromLocalToFiler(newEntry)
|
|
defer wfs.mapPbIdFromFilerToLocal(newEntry)
|
|
|
|
request := &filer_pb.CreateEntryRequest{
|
|
Directory: string(dirFullPath),
|
|
Entry: newEntry,
|
|
Signatures: []int32{wfs.signature},
|
|
SkipCheckParentDirectory: true,
|
|
}
|
|
|
|
glog.V(1).Infof("mknod: %v", request)
|
|
if err := filer_pb.CreateEntry(context.Background(), client, request); err != nil {
|
|
glog.V(0).Infof("mknod %s: %v", entryFullPath, err)
|
|
return err
|
|
}
|
|
|
|
// Only cache the entry if the parent directory is already cached.
|
|
// This avoids polluting the cache with partial directory data.
|
|
if wfs.metaCache.IsDirectoryCached(dirFullPath) {
|
|
if err := wfs.metaCache.InsertEntry(context.Background(), filer.FromPbEntry(request.Directory, request.Entry)); err != nil {
|
|
return fmt.Errorf("local mknod %s: %w", entryFullPath, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
glog.V(3).Infof("mknod %s: %v", entryFullPath, err)
|
|
|
|
if err != nil {
|
|
return fuse.EIO
|
|
}
|
|
|
|
// this is to increase nlookup counter
|
|
inode = wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, false, false, inode, true)
|
|
|
|
wfs.outputPbEntry(out, inode, newEntry)
|
|
|
|
return fuse.OK
|
|
|
|
}
|
|
|
|
/** Remove a file */
|
|
func (wfs *WFS) Unlink(cancel <-chan struct{}, header *fuse.InHeader, name string) (code fuse.Status) {
|
|
|
|
dirFullPath, code := wfs.inodeToPath.GetPath(header.NodeId)
|
|
if code != fuse.OK {
|
|
if code == fuse.ENOENT {
|
|
return fuse.OK
|
|
}
|
|
return code
|
|
}
|
|
entryFullPath := dirFullPath.Child(name)
|
|
|
|
entry, code := wfs.maybeLoadEntry(entryFullPath)
|
|
if code != fuse.OK {
|
|
if code == fuse.ENOENT {
|
|
return fuse.OK
|
|
}
|
|
return code
|
|
}
|
|
|
|
if wormEnforced, _ := wfs.wormEnforcedForEntry(entryFullPath, entry); wormEnforced {
|
|
return fuse.EPERM
|
|
}
|
|
|
|
// first, ensure the filer store can correctly delete
|
|
glog.V(3).Infof("remove file: %v", entryFullPath)
|
|
// Always let the filer decide whether to delete chunks based on its authoritative data.
|
|
// The filer has the correct hard link count and will only delete chunks when appropriate.
|
|
err := filer_pb.Remove(context.Background(), wfs, string(dirFullPath), name, true, false, false, false, []int32{wfs.signature})
|
|
if err != nil {
|
|
glog.V(0).Infof("remove %s: %v", entryFullPath, err)
|
|
return fuse.OK
|
|
}
|
|
|
|
// then, delete meta cache
|
|
if err = wfs.metaCache.DeleteEntry(context.Background(), entryFullPath); err != nil {
|
|
glog.V(3).Infof("local DeleteEntry %s: %v", entryFullPath, err)
|
|
return fuse.EIO
|
|
}
|
|
|
|
wfs.inodeToPath.RemovePath(entryFullPath)
|
|
|
|
return fuse.OK
|
|
|
|
}
|