Files
seaweedfs/weed/mount/winfsp/fs_windows.go
T
Chris Lu 214d3599d3 windows mount: cache file data, resolved paths and attributes (#10703)
* benchmark tool for mounted filesystems

* ci: on-demand mount benchmark, native WinFsp vs rclone plus a Linux reference

* windows mount: let the Windows cache manager cache file data

WinFsp only turns the cache manager on for a file when FileInfoTimeout
is infinite; at any finite value every application read and write is a
synchronous trip into the mount process at whatever size the application
issued. Metadata events already reach FspFileSystemNotify, which purges
a changed file's cached pages and attributes, so an infinite timeout
stays coherent. The dir listing, volume info and EA timeouts are pinned
to one second so they do not silently inherit the infinity.

* windows mount: cache resolved paths and attributes in the adapter

WinFsp addresses every operation by path and has no FORGET, so the
adapter walked the whole path through Lookup on each one, and in a
directory the filer has not listed yet every walk was a filer round
trip; nothing played the part of the kernel's dentry and attribute
caches. The path cache owns one lookup reference per entry the way the
kernel holds one until FORGET, serves attribute reads for files without
an open handle, and is purged by the mount's own mutations and by
metadata events, with the timeout as backstop.

* windows mount: keep a closed file's attributes cached

Open steals the path's cache entry for its handle and Release returned
the reference with a purge, so the stat that follows every copied file
walked to the filer again. Reading the handle's final attributes before
it goes away and moving the reference back into the cache serves that
stat locally, the way the kernel's attribute cache does after a close.

Only if the path still names that inode, though: WinFsp reports the
path the handle opened with, and after a delete-on-close or a rename
caching it would resurrect an entry that is gone.

* windows mount: persist entries at create, and let the flush stay at close

WinFsp posts the cleanup and close that carry the flush after
CloseHandle has returned, so deferring the filer entry to the flush let
everything that reads through the filer race an unflushed close: a
listing missed just-written files, and a directory rename moved a
directory on the filer before its newest child existed there, leaving
the straggler flush to recreate the child under the dead path.

Flush-at-cleanup is not the answer either: it makes every handle's
cleanup flush, and those flushes race the unlinks of delete-on-close,
re-inserting the entry the unlink just removed. Persisting the entry at
create takes the ordering question away.

* mount: flush written pages before a truncate shrinks past them

The shrink trims chunks, but written pages that have not become chunks
yet are invisible to it, so the next flush wrote them back and the file
grew again, resurrecting the truncated bytes. Windows hits this on
every write-then-shrink because its flush runs after CloseHandle, but
the gap is platform-neutral.

* mount: order a file's unlink against its in-flight flush

Unlink set the handle's deleted flag bare, so a flush already past its
own check of that flag wrote the entry back right after the delete
removed it, and a delete-on-close file outlived its last handle. The
flag is now set under the handle's flush lock and re-checked under it,
so a flush either completes before the delete or sees the flag and
skips. An eagerly created handle also starts clean: the dirty mark
existed to make the deferred filer create happen at flush, and eager
creates have nothing to flush.
2026-08-10 18:46:18 -07:00

852 lines
25 KiB
Go

package winfsp
import (
"strings"
"sync"
"syscall"
"time"
cgofuse "github.com/winfsp/cgofuse/fuse"
"github.com/seaweedfs/go-fuse/v2/fuse"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/mount"
"github.com/seaweedfs/seaweedfs/weed/util"
)
const (
rootInode = 1
// WinFsp passes this when an operation carries no open handle.
noHandle = ^uint64(0)
// utimeOmit is the nanosecond marker asking for a timestamp to be left as
// it is; cgofuse hands it through rather than resolving it.
utimeOmit = (1 << 30) - 2
// windowsEpochCutoff is 1601-01-02 in unix seconds. Anything at or below
// it is Windows' own epoch leaking through rather than a real time.
windowsEpochCutoff = -11644387200
// How many entries to pull from one readdir round before handing them to
// WinFsp. Bounded so a directory with millions of children does not
// materialise in one slice.
readdirBatch = 4096
// stealAttempts bounds the resolve-then-steal loop in the open paths. A
// miss needs the entry to be evicted in the instant between the two calls,
// so a second round is already unlikely.
stealAttempts = 4
)
// never is a nil channel: receiving blocks forever, which is what the raw
// operations expect from a caller that cannot cancel.
var never chan struct{}
// WinFS presents a mount.WFS through the path-based interface WinFsp speaks.
type WinFS struct {
cgofuse.FileSystemBase
wfs *mount.WFS
mountRoot util.FullPath
uid uint32
gid uint32
readOnly bool
// paths holds the lookup references for resolved paths, standing in for
// the kernel caches of the unix mounts.
paths *pathCache
// An open handle holds a lookup reference for its inode until Release,
// the way the kernel keeps one for an open file. WinFsp hands back only
// the handle, and the raw filesystem reuses one handle for repeated opens
// of the same inode, so the references are counted.
mu sync.Mutex
fileInodes map[uint64]*handleRef
dirInodes map[uint64]*handleRef
}
func NewWinFS(wfs *mount.WFS, uid, gid uint32, readOnly bool, cacheTimeout time.Duration) *WinFS {
w := &WinFS{
wfs: wfs,
mountRoot: wfs.MountRoot(),
uid: uid,
gid: gid,
readOnly: readOnly,
fileInodes: make(map[uint64]*handleRef),
dirInodes: make(map[uint64]*handleRef),
}
w.paths = newPathCache(cacheTimeout, w.forget)
return w
}
// stillNames reports whether the mount still tracks inode at the mount-relative
// key, which a delete-on-close or a rename while the handle was open has
// changed.
func (w *WinFS) stillNames(key string, inode uint64) bool {
current, ok := w.wfs.PathForInode(inode)
if !ok {
return false
}
rel, ok := relativeToMount(w.mountRoot, current)
return ok && cacheKey(rel) == key
}
// invalidatePath is what the notifier calls when a metadata event arrives:
// whatever is cached for that path is out of date. Directories take their
// subtree with them, since the children's cached paths point through them.
func (w *WinFS) invalidatePath(path string, isDirectory bool) {
w.paths.purge(cacheKey(path), isDirectory)
}
// handleRef is the lookup references an open handle is holding, one per open
// that has not yet been released.
type handleRef struct {
inode uint64
count int
}
// retain parks one reference under a handle.
func (w *WinFS) retain(table map[uint64]*handleRef, handle, inode uint64) {
if handle == noHandle {
return
}
var stranded uint64
w.mu.Lock()
switch existing, found := table[handle]; {
case found && existing.inode == inode:
existing.count++
case found:
// The handle was reissued for a different inode; its old reference
// would otherwise never be returned.
stranded = existing.inode
table[handle] = &handleRef{inode: inode, count: 1}
default:
table[handle] = &handleRef{inode: inode, count: 1}
}
w.mu.Unlock()
w.forget(stranded)
}
// releaseRetained hands back one reference, reporting the inode only once the
// last open of that handle is gone, so a repeated release cannot forget twice.
func (w *WinFS) releaseRetained(table map[uint64]*handleRef, handle uint64) uint64 {
w.mu.Lock()
defer w.mu.Unlock()
existing, found := table[handle]
if !found {
return 0
}
existing.count--
if existing.count > 0 {
return 0
}
delete(table, handle)
return existing.inode
}
// caller identifies who the raw filesystem should record as the owner of
// anything it creates. Windows has no uid to pass through, so entries carry
// the identity the mount was started with rather than root.
// denied reports whether a modification should be refused outright, which is
// how a read-only mount is enforced: WinFsp discards its own "ro" option.
func (w *WinFS) denied() bool { return w.readOnly }
// inodeForHandle reports the inode an open handle is holding, or 0. WinFsp
// keeps the path it opened with and never updates it across a rename, so a
// handle is the more reliable of the two.
func (w *WinFS) inodeForHandle(table map[uint64]*handleRef, handle uint64) uint64 {
if handle == noHandle {
return 0
}
w.mu.Lock()
defer w.mu.Unlock()
if existing, found := table[handle]; found {
return existing.inode
}
return 0
}
// ptr is for the operations that take the header by pointer.
func ptr(h fuse.InHeader) *fuse.InHeader { return &h }
func (w *WinFS) caller(inode uint64) fuse.InHeader {
return fuse.InHeader{
NodeId: inode,
Caller: fuse.Caller{Owner: fuse.Owner{Uid: w.uid, Gid: w.gid}},
}
}
// forget returns one reference. The root is never looked up, so it never holds
// one to give back.
func (w *WinFS) forget(inode uint64) {
if inode == rootInode || inode == 0 {
return
}
w.wfs.Forget(inode, 1)
}
// walk resolves a path one component at a time, filling the path cache as it
// goes. Every reference a lookup grants is owned by the cache, which keeps the
// returned inode alive for at least one cache sweep; an operation that needs
// to hold the inode longer steals the reference from the cache.
func (w *WinFS) walk(parts []string) (uint64, fuse.Status) {
inode := uint64(rootInode)
for i, name := range parts {
key := strings.Join(parts[:i+1], "/")
if cached, _, ok := w.paths.lookup(key); ok {
inode = cached
continue
}
var out fuse.EntryOut
if status := w.wfs.Lookup(never, ptr(w.caller(inode)), name, &out); status != fuse.OK {
return 0, status
}
w.paths.insert(key, out.NodeId, out.Attr)
inode = out.NodeId
}
return inode, fuse.OK
}
// resolve walks a WinFsp path down to an inode, valid at least until the next
// cache sweep.
func (w *WinFS) resolve(path string) (uint64, fuse.Status) {
return w.walk(splitPath(path))
}
// resolveParent resolves everything but the last component, which the create
// and delete operations need separately.
func (w *WinFS) resolveParent(path string) (uint64, string, fuse.Status) {
parentParts, name, ok := splitParent(path)
if !ok {
return 0, "", fuse.EINVAL
}
parent, status := w.walk(parentParts)
if status != fuse.OK {
return 0, "", status
}
return parent, name, fuse.OK
}
// resolveAndSteal resolves path and takes over the cache's reference on the
// final inode, for the open paths that hold it for the life of a handle. The
// root is handed out without a reference; it does not need one.
func (w *WinFS) resolveAndSteal(path string) (uint64, fuse.Status) {
key := cacheKey(path)
for attempt := 0; attempt < stealAttempts; attempt++ {
inode, status := w.resolve(path)
if status != fuse.OK {
return 0, status
}
if key == "" {
return inode, fuse.OK
}
if stolen, ok := w.paths.steal(key); ok {
return stolen, fuse.OK
}
}
return 0, fuse.EIO
}
func (w *WinFS) attrToStat(attr *fuse.Attr, stat *cgofuse.Stat_t) {
stat.Ino = attr.Ino
stat.Mode = attr.Mode
stat.Nlink = attr.Nlink
stat.Size = int64(attr.Size)
stat.Blocks = int64(attr.Blocks)
stat.Blksize = int64(attr.Blksize)
stat.Uid = w.uid
stat.Gid = w.gid
stat.Atim = cgofuse.Timespec{Sec: int64(attr.Atime), Nsec: int64(attr.Atimensec)}
stat.Mtim = cgofuse.Timespec{Sec: int64(attr.Mtime), Nsec: int64(attr.Mtimensec)}
stat.Ctim = cgofuse.Timespec{Sec: int64(attr.Ctime), Nsec: int64(attr.Ctimensec)}
// The filer records a creation time, but the raw protocol's Attr carries no
// field for it outside darwin, so ctime stands in until there is a way to
// read it through.
stat.Birthtim = stat.Ctim
// Windows expects a regular file to be marked archived; with no flags at
// all it synthesises NORMAL, which is a different thing and what
// create_fileattr_test checks.
if attr.Mode&cgofuse.S_IFMT == cgofuse.S_IFREG {
stat.Flags |= cgofuse.UF_ARCHIVE
}
}
// translateOpenFlags converts cgofuse's open flags, which follow MSVC's
// numbering, into the values the raw filesystem tests against. Only the access
// mode and O_TRUNC happen to agree; O_EXCL would otherwise read as O_APPEND.
func translateOpenFlags(flags int) uint32 {
out := uint32(flags & cgofuse.O_ACCMODE)
for _, pair := range []struct {
from int
to int
}{
{cgofuse.O_APPEND, syscall.O_APPEND},
{cgofuse.O_CREAT, syscall.O_CREAT},
{cgofuse.O_TRUNC, syscall.O_TRUNC},
{cgofuse.O_EXCL, syscall.O_EXCL},
} {
if flags&pair.from != 0 {
out |= uint32(pair.to)
}
}
return out
}
// logResolveFailure keeps a missing file quiet. Windows probes for entries
// that do not exist as a matter of course, so ENOENT is an answer rather than
// a fault; anything else is worth a line.
func logResolveFailure(op, path string, status fuse.Status) {
if status == fuse.ENOENT {
glog.V(4).Infof("%s %s: no such entry", op, path)
return
}
glog.Errorf("%s %s: resolving: %v", op, path, status)
}
func (w *WinFS) Statfs(path string, stat *cgofuse.Statfs_t) int {
var out fuse.StatfsOut
if status := w.wfs.StatFs(never, ptr(w.caller(rootInode)), &out); status != fuse.OK {
return toErrno(status)
}
stat.Bsize = uint64(out.Bsize)
stat.Frsize = uint64(out.Frsize)
stat.Blocks = out.Blocks
stat.Bfree = out.Bfree
stat.Bavail = out.Bavail
stat.Files = out.Files
stat.Ffree = out.Ffree
stat.Favail = out.Ffree
stat.Namemax = uint64(out.NameLen)
return 0
}
// getattrFromCache serves attributes the way the kernel would from its
// attribute cache. A file with an open handle is excluded: its live size is
// on the handle, not in the cache.
func (w *WinFS) getattrFromCache(key string, stat *cgofuse.Stat_t) bool {
if key == "" {
return false
}
inode, attr, ok := w.paths.lookup(key)
if !ok || w.wfs.IsFileOpen(inode) {
return false
}
w.attrToStat(&attr, stat)
return true
}
func (w *WinFS) Getattr(path string, stat *cgofuse.Stat_t, fh uint64) int {
inode := w.inodeForHandle(w.fileInodes, fh)
if inode == 0 {
key := cacheKey(path)
if fh == noHandle && w.getattrFromCache(key, stat) {
return 0
}
var status fuse.Status
inode, status = w.resolve(path)
if status != fuse.OK {
logResolveFailure("getattr", path, status)
return toErrno(status)
}
// The walk just refreshed the cache, so this hits unless the file is
// open or is the root, and saves the second load of the same entry.
if fh == noHandle && w.getattrFromCache(key, stat) {
return 0
}
}
in := &fuse.GetAttrIn{InHeader: w.caller(inode)}
if fh != noHandle {
in.Fh_ = fh
in.Flags_ = fuse.FUSE_GETATTR_FH
}
var out fuse.AttrOut
if status := w.wfs.GetAttr(never, in, &out); status != fuse.OK {
return toErrno(status)
}
w.attrToStat(&out.Attr, stat)
return 0
}
// purgeWithParent drops a mutated path from the cache along with its parent,
// whose cached mtime the mutation just outdated.
func (w *WinFS) purgeWithParent(path string, prefix bool) {
key := cacheKey(path)
w.paths.purge(key, prefix)
if i := strings.LastIndexByte(key, '/'); i > 0 {
w.paths.purge(key[:i], false)
}
}
func (w *WinFS) Mkdir(path string, mode uint32) int {
if w.denied() {
return -eROFS
}
parent, name, status := w.resolveParent(path)
if status != fuse.OK {
return toErrno(status)
}
in := &fuse.MkdirIn{InHeader: w.caller(parent), Mode: mode}
var out fuse.EntryOut
status = w.wfs.Mkdir(never, in, name, &out)
if status == fuse.OK {
w.purgeWithParent(path, false)
// The new directory is about to be filled; cache it, reference and all.
w.paths.insert(cacheKey(path), out.NodeId, out.Attr)
}
return toErrno(status)
}
func (w *WinFS) Rmdir(path string) int {
if w.denied() {
return -eROFS
}
parent, name, status := w.resolveParent(path)
if status != fuse.OK {
return toErrno(status)
}
status = w.wfs.Rmdir(never, ptr(w.caller(parent)), name)
if status == fuse.OK {
w.purgeWithParent(path, true)
}
return toErrno(status)
}
func (w *WinFS) Unlink(path string) int {
if w.denied() {
return -eROFS
}
parent, name, status := w.resolveParent(path)
if status != fuse.OK {
return toErrno(status)
}
status = w.wfs.Unlink(never, ptr(w.caller(parent)), name)
if status == fuse.OK {
w.purgeWithParent(path, false)
}
return toErrno(status)
}
func (w *WinFS) Rename(oldpath string, newpath string) int {
if w.denied() {
return -eROFS
}
oldParent, oldName, status := w.resolveParent(oldpath)
if status != fuse.OK {
return toErrno(status)
}
newParent, newName, status := w.resolveParent(newpath)
if status != fuse.OK {
return toErrno(status)
}
in := &fuse.RenameIn{InHeader: w.caller(oldParent), Newdir: newParent}
status = w.wfs.Rename(never, in, oldName, newName)
if status == fuse.OK {
// Directories carry their subtree's cached paths with them, and the
// rename may have replaced an entry at the destination.
w.purgeWithParent(oldpath, true)
w.purgeWithParent(newpath, true)
}
return toErrno(status)
}
func (w *WinFS) Create(path string, flags int, mode uint32) (int, uint64) {
if w.denied() {
return -eROFS, noHandle
}
parent, name, status := w.resolveParent(path)
if status != fuse.OK {
glog.Errorf("create %s: resolving the parent directory: %v", path, status)
return toErrno(status), noHandle
}
in := &fuse.CreateIn{InHeader: w.caller(parent), Flags: translateOpenFlags(flags), Mode: mode}
var out fuse.CreateOut
if status := w.wfs.Create(never, in, name, &out); status != fuse.OK {
glog.Errorf("create %s in inode %d: %v", name, parent, status)
return toErrno(status), noHandle
}
// Whatever the cache held for this path was replaced by the create.
w.purgeWithParent(path, false)
// Create grants a reference on the new inode; hold it for as long as the
// handle lives and give it back in Release.
w.retain(w.fileInodes, out.Fh, out.NodeId)
return 0, out.Fh
}
func (w *WinFS) Open(path string, flags int) (int, uint64) {
inode, status := w.resolveAndSteal(path)
if status != fuse.OK {
logResolveFailure("open", path, status)
return toErrno(status), noHandle
}
in := &fuse.OpenIn{InHeader: w.caller(inode), Flags: translateOpenFlags(flags)}
var out fuse.OpenOut
if status := w.wfs.Open(never, in, &out); status != fuse.OK {
glog.Errorf("open %s inode %d: %v", path, inode, status)
w.forget(inode)
return toErrno(status), noHandle
}
w.retain(w.fileInodes, out.Fh, inode)
return 0, out.Fh
}
func (w *WinFS) Read(path string, buff []byte, ofst int64, fh uint64) int {
if fh == noHandle {
return -eBADF
}
in := &fuse.ReadIn{
Fh: fh,
Offset: uint64(ofst),
Size: uint32(len(buff)),
}
result, status := w.wfs.Read(never, in, buff)
if status != fuse.OK {
return toErrno(status)
}
data, status := result.Bytes(buff)
if status != fuse.OK {
return toErrno(status)
}
if len(data) > 0 && &data[0] != &buff[0] {
copy(buff, data)
}
return len(data)
}
func (w *WinFS) Write(path string, buff []byte, ofst int64, fh uint64) int {
if w.denied() {
return -eROFS
}
if fh == noHandle {
return -eBADF
}
in := &fuse.WriteIn{
Fh: fh,
Offset: uint64(ofst),
Size: uint32(len(buff)),
}
written, status := w.wfs.Write(never, in, buff)
if status != fuse.OK {
if status == fuse.ENOENT {
glog.Errorf("write %s: handle %d is not open", path, fh)
}
return toErrno(status)
}
return int(written)
}
func (w *WinFS) Truncate(path string, size int64, fh uint64) int {
if w.denied() {
return -eROFS
}
inode := w.inodeForHandle(w.fileInodes, fh)
if inode == 0 {
var status fuse.Status
inode, status = w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
}
in := &fuse.SetAttrIn{}
in.NodeId = inode
in.Valid = fuse.FATTR_SIZE
in.Size = uint64(size)
if fh != noHandle {
in.Valid |= fuse.FATTR_FH
in.Fh = fh
}
var out fuse.AttrOut
status := w.wfs.SetAttr(never, in, &out)
if status == fuse.OK {
w.paths.purge(cacheKey(path), false)
}
return toErrno(status)
}
func (w *WinFS) Chmod(path string, mode uint32) int {
if w.denied() {
return -eROFS
}
inode, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
in := &fuse.SetAttrIn{}
in.NodeId = inode
in.Valid = fuse.FATTR_MODE
in.Mode = mode
var out fuse.AttrOut
status = w.wfs.SetAttr(never, in, &out)
if status == fuse.OK {
w.paths.purge(cacheKey(path), false)
}
return toErrno(status)
}
func (w *WinFS) Utimens(path string, tmsp []cgofuse.Timespec) int {
if w.denied() {
return -eROFS
}
inode, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
in := &fuse.SetAttrIn{}
in.NodeId = inode
if len(tmsp) < 2 {
in.Valid = fuse.FATTR_ATIME_NOW | fuse.FATTR_MTIME_NOW
} else {
// Windows sends times around its own 1601 epoch, which arrive here as
// a large negative second count and would be stored as a year-1601
// timestamp that every other client then reads. Leave those alone.
if applyTimespec(tmsp[0]) {
in.Valid |= fuse.FATTR_ATIME
in.Atime, in.Atimensec = uint64(tmsp[0].Sec), uint32(tmsp[0].Nsec)
}
if applyTimespec(tmsp[1]) {
in.Valid |= fuse.FATTR_MTIME
in.Mtime, in.Mtimensec = uint64(tmsp[1].Sec), uint32(tmsp[1].Nsec)
}
if in.Valid == 0 {
return 0
}
}
var out fuse.AttrOut
status = w.wfs.SetAttr(never, in, &out)
if status == fuse.OK {
w.paths.purge(cacheKey(path), false)
}
return toErrno(status)
}
// applyTimespec reports whether a timestamp should be written. UTIME_OMIT asks
// for the existing value to be kept, and a time at or below Windows' own 1601
// epoch arrives as a large negative second count. Exactly zero is what Windows
// sends for a field it is not setting; storing it put 1970 in the atime
// overlay, which then read back as the file's access time. A date genuinely
// before 1970 is still allowed through — only the epoch itself is the
// sentinel.
func applyTimespec(ts cgofuse.Timespec) bool {
if ts.Nsec == utimeOmit {
return false
}
if ts.Sec == 0 && ts.Nsec == 0 {
return false
}
return ts.Sec > windowsEpochCutoff
}
func (w *WinFS) Flush(path string, fh uint64) int {
if fh == noHandle {
return 0
}
return toErrno(w.wfs.Flush(never, &fuse.FlushIn{InHeader: w.caller(0), Fh: fh}))
}
func (w *WinFS) Fsync(path string, datasync bool, fh uint64) int {
if fh == noHandle {
return 0
}
return toErrno(w.wfs.Fsync(never, &fuse.FsyncIn{Fh: fh}))
}
func (w *WinFS) Release(path string, fh uint64) int {
if fh == noHandle {
return 0
}
// The handle's view of the file is the freshest there is, and the close
// has flushed by now, so read it before the handle goes away: the next
// operation on the path — a stat right after a copy, most often — is then
// served from the cache instead of walking to the filer again.
var attr *fuse.Attr
if inode := w.inodeForHandle(w.fileInodes, fh); inode != 0 {
in := &fuse.GetAttrIn{InHeader: w.caller(inode)}
in.Fh_ = fh
in.Flags_ = fuse.FUSE_GETATTR_FH
var out fuse.AttrOut
if w.wfs.GetAttr(never, in, &out) == fuse.OK {
attr = &out.Attr
}
}
w.wfs.Release(never, &fuse.ReleaseIn{Fh: fh})
if inode := w.releaseRetained(w.fileInodes, fh); inode != 0 {
// The reference moves from the handle to the cache — but only if the
// path still names this inode: WinFsp reports the path the handle
// opened with, and after a delete-on-close or a rename caching it
// would resurrect an entry that is gone.
if key := cacheKey(path); attr != nil && w.stillNames(key, inode) {
w.paths.insert(key, inode, *attr)
} else {
w.forget(inode)
}
}
return 0
}
func (w *WinFS) Opendir(path string) (int, uint64) {
inode, status := w.resolveAndSteal(path)
if status != fuse.OK {
return toErrno(status), noHandle
}
var out fuse.OpenOut
if status := w.wfs.OpenDir(never, &fuse.OpenIn{InHeader: w.caller(inode)}, &out); status != fuse.OK {
w.forget(inode)
return toErrno(status), noHandle
}
w.retain(w.dirInodes, out.Fh, inode)
return 0, out.Fh
}
func (w *WinFS) Releasedir(path string, fh uint64) int {
if fh == noHandle {
return 0
}
w.wfs.ReleaseDir(&fuse.ReleaseIn{Fh: fh})
w.forget(w.releaseRetained(w.dirInodes, fh))
return 0
}
// readdirSink collects one readdir round. The raw operation fills the returned
// EntryOut after AddEntryPlus returns, so nothing can be converted until the
// round is over.
type readdirSink struct {
names []string
offsets []uint64
inodes []uint64
modes []uint32
attrs []*fuse.EntryOut
limit int
// lastOffset advances over dropped entries too, so a batch that is all
// dot entries still moves the enumeration along.
lastOffset uint64
seen int
}
// WinFsp strips "." and ".." for the root directory itself and expects every
// other directory to report them, the way a real NTFS enumeration does.
func (s *readdirSink) AddEntry(entry fuse.DirEntry) bool {
if len(s.names) >= s.limit {
return false
}
s.seen++
s.lastOffset = entry.Off
s.names = append(s.names, entry.Name)
s.offsets = append(s.offsets, entry.Off)
s.inodes = append(s.inodes, entry.Ino)
s.modes = append(s.modes, entry.Mode)
s.attrs = append(s.attrs, nil)
return true
}
func (s *readdirSink) AddEntryPlus(entry fuse.DirEntry) *fuse.EntryOut {
if len(s.names) >= s.limit {
return nil
}
s.seen++
s.lastOffset = entry.Off
out := &fuse.EntryOut{}
s.names = append(s.names, entry.Name)
s.offsets = append(s.offsets, entry.Off)
s.inodes = append(s.inodes, entry.Ino)
s.modes = append(s.modes, entry.Mode)
s.attrs = append(s.attrs, out)
return out
}
// WinFsp has no FORGET, and every EntryOut is converted before the round ends.
func (s *readdirSink) TakesLookupRef() bool { return false }
func (w *WinFS) Readdir(path string, fill func(name string, stat *cgofuse.Stat_t, ofst int64) bool, ofst int64, fh uint64) int {
inode := w.inodeForHandle(w.dirInodes, fh)
if inode == 0 {
var status fuse.Status
inode, status = w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
}
offset := uint64(ofst)
for {
sink := &readdirSink{limit: readdirBatch}
in := &fuse.ReadIn{
InHeader: w.caller(inode),
Fh: fh,
Offset: offset,
Size: 1 << 20,
}
if status := w.wfs.ReadDirectoryInto(in, sink, true); status != fuse.OK {
return toErrno(status)
}
if sink.seen == 0 {
return 0
}
filled := true
for i, name := range sink.names {
var stat cgofuse.Stat_t
var statp *cgofuse.Stat_t
if attr := sink.attrs[i]; attr != nil && attr.Attr.Mode != 0 {
w.attrToStat(&attr.Attr, &stat)
statp = &stat
} else if sink.modes[i] != 0 {
// "." and ".." are reported without attributes: the readdir
// only fills a block for real children. Windows still needs
// their type, and enumerating a directory whose first entry is
// not marked as one fails outright.
stat.Mode = sink.modes[i]
stat.Ino = sink.inodes[i]
stat.Nlink = 1
stat.Uid, stat.Gid = w.uid, w.gid
statp = &stat
}
if filled && !fill(name, statp, int64(sink.offsets[i])) {
filled = false
}
}
if !filled {
return 0
}
if sink.lastOffset <= offset {
return 0
}
offset = sink.lastOffset
}
}
func (w *WinFS) Readlink(path string) (int, string) {
// WinFsp probes the root to decide whether the volume has symlinks, and
// turns them on unless this fails. Leaving them on costs a getattr per
// path component on every open, for a feature Symlink already refuses.
if len(splitPath(path)) == 0 {
return -eNOSYS, ""
}
inode, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status), ""
}
target, status := w.wfs.Readlink(never, ptr(w.caller(inode)))
if status != fuse.OK {
return toErrno(status), ""
}
return 0, string(target)
}
// Symlink is refused for now. The entry is easy to create, but WinFsp only
// follows it once the reparse point is wired up, so it would otherwise read
// back as an empty file.
func (w *WinFS) Symlink(target string, newpath string) int {
return -eNOSYS
}
// Chown accepts and discards. Windows has no uid to record, but WinFsp passes
// a chown failure straight out of SetSecurity, so refusing it breaks Explorer's
// Security tab and icacls for changes that are not about ownership at all.
func (w *WinFS) Chown(path string, uid uint32, gid uint32) int {
return 0
}
// Link is not implemented: WinFsp has no hard links.
func (w *WinFS) Link(oldpath string, newpath string) int {
return -eNOSYS
}