snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 03:23:42 +05:00
commit 89f5be005d
3997 changed files with 961970 additions and 0 deletions

118
weed/mount/winfsp/errno.go Normal file
View File

@@ -0,0 +1,118 @@
// Package winfsp mounts a SeaweedFS filesystem on Windows through WinFsp.
//
// WinFsp speaks a path-based FUSE dialect, while weed/mount implements the
// inode-based raw protocol the Linux kernel uses. This package translates
// between the two so both platforms run the same filesystem code.
package winfsp
import (
"syscall"
"github.com/seaweedfs/go-fuse/v2/fuse"
)
// cgofuse decodes what these operations return using MSVC/UCRT numbering,
// which parts company with Linux above 35: ENAMETOOLONG is 38 there and 36 is
// EDEADLK, so borrowing the Linux values silently reported the wrong error.
// errno_windows_test.go pins each of these to cgofuse's own constant.
const (
ePERM = 1
eNOENT = 2
eINTR = 4
eIO = 5
eNXIO = 6
eBADF = 9
eAGAIN = 11
eNOMEM = 12
eACCES = 13
eBUSY = 16
eEXIST = 17
eXDEV = 18
eNODEV = 19
eNOTDIR = 20
eISDIR = 21
eINVAL = 22
eNFILE = 23
eMFILE = 24
eFBIG = 27
eNOSPC = 28
eSPIPE = 29
eROFS = 30
eMLINK = 31
ePIPE = 32
eRANGE = 34
eNAMETOOLONG = 38
eNOSYS = 40
eNOTEMPTY = 41
eLOOP = 114
eNODATA = 120
eNOTSUP = 129
)
// statusErrno is keyed by the running platform's errno values, since that is
// what the raw filesystem returns, and yields the POSIX number for the wire.
// Built rather than declared: platforms alias errnos differently (freebsd has
// no ENODATA, linux makes ENOATTR the same value), so entries can collide.
// First one wins, so the general codes keep their meaning.
var statusErrno = buildStatusErrno()
func buildStatusErrno() map[fuse.Status]int {
table := []struct {
status fuse.Status
errno int
}{
{fuse.Status(syscall.EPERM), ePERM},
{fuse.Status(syscall.ENOENT), eNOENT},
{fuse.Status(syscall.EINTR), eINTR},
{fuse.Status(syscall.EIO), eIO},
{fuse.Status(syscall.ENXIO), eNXIO},
{fuse.Status(syscall.EBADF), eBADF},
{fuse.Status(syscall.EAGAIN), eAGAIN},
{fuse.Status(syscall.ENOMEM), eNOMEM},
{fuse.Status(syscall.EACCES), eACCES},
{fuse.Status(syscall.EBUSY), eBUSY},
{fuse.Status(syscall.EEXIST), eEXIST},
{fuse.Status(syscall.EXDEV), eXDEV},
{fuse.Status(syscall.ENODEV), eNODEV},
{fuse.Status(syscall.ENOTDIR), eNOTDIR},
{fuse.Status(syscall.EISDIR), eISDIR},
{fuse.Status(syscall.EINVAL), eINVAL},
{fuse.Status(syscall.ENFILE), eNFILE},
{fuse.Status(syscall.EMFILE), eMFILE},
{fuse.Status(syscall.EFBIG), eFBIG},
{fuse.Status(syscall.ENOSPC), eNOSPC},
{fuse.Status(syscall.ESPIPE), eSPIPE},
{fuse.Status(syscall.EROFS), eROFS},
{fuse.Status(syscall.EMLINK), eMLINK},
{fuse.Status(syscall.EPIPE), ePIPE},
{fuse.Status(syscall.ERANGE), eRANGE},
{fuse.Status(syscall.ENAMETOOLONG), eNAMETOOLONG},
{fuse.Status(syscall.ENOSYS), eNOSYS},
{fuse.Status(syscall.ENOTEMPTY), eNOTEMPTY},
{fuse.Status(syscall.ELOOP), eLOOP},
{fuse.ENOTSUP, eNOTSUP},
{fuse.ENODATA, eNODATA},
{fuse.ENOATTR, eNODATA},
}
m := make(map[fuse.Status]int, len(table))
for _, entry := range table {
if _, exists := m[entry.status]; !exists {
m[entry.status] = entry.errno
}
}
return m
}
// toErrno converts a raw filesystem status into the negative errno WinFsp
// wants. Unrecognised failures become -EIO rather than passing through a
// number that means something else on the other side.
func toErrno(status fuse.Status) int {
if status == fuse.OK {
return 0
}
if n, ok := statusErrno[status]; ok {
return -n
}
return -eIO
}

View File

@@ -0,0 +1,49 @@
package winfsp
import (
"syscall"
"testing"
"github.com/seaweedfs/go-fuse/v2/fuse"
)
// The numbers have to match what cgofuse decodes on the far side, regardless
// of how the host platform spells its own errno constants. That numbering is
// MSVC's, which parts company with Linux above 35.
func TestToErrnoUsesCgofuseNumbering(t *testing.T) {
cases := []struct {
status fuse.Status
want int
}{
{fuse.OK, 0},
{fuse.ENOENT, -2},
{fuse.EIO, -5},
{fuse.EACCES, -13},
{fuse.EINVAL, -22},
{fuse.ENOSYS, -40},
{fuse.Status(syscall.EEXIST), -17},
{fuse.Status(syscall.ENOSPC), -28},
{fuse.Status(syscall.ENOTEMPTY), -41},
}
for _, c := range cases {
if got := toErrno(c.status); got != c.want {
t.Errorf("toErrno(%v) = %d, want %d", c.status, got, c.want)
}
}
}
// An unmapped failure must not pass its raw number through: on Windows those
// are APPLICATION_ERROR offsets that mean something else entirely.
func TestToErrnoUnknownBecomesEIO(t *testing.T) {
if got := toErrno(fuse.Status(1 << 20)); got != -eIO {
t.Errorf("toErrno(unknown) = %d, want %d", got, -eIO)
}
}
func TestToErrnoNeverReturnsPositive(t *testing.T) {
for _, status := range []fuse.Status{fuse.ENOENT, fuse.EPERM, fuse.EBUSY, fuse.Status(4242)} {
if got := toErrno(status); got > 0 {
t.Errorf("toErrno(%v) = %d, want <= 0", status, got)
}
}
}

View File

@@ -0,0 +1,87 @@
package winfsp
import (
"syscall"
"testing"
cgofuse "github.com/winfsp/cgofuse/fuse"
)
// The errno values are spelled out so the table stays portable and testable on
// any runner. This pins them to what cgofuse actually decodes, so a divergence
// is a failing test rather than an operation reporting an unrelated error.
func TestErrnoValuesMatchCgofuse(t *testing.T) {
for _, c := range []struct {
name string
ours int
want int
}{
{"EPERM", ePERM, cgofuse.EPERM},
{"ENOENT", eNOENT, cgofuse.ENOENT},
{"EINTR", eINTR, cgofuse.EINTR},
{"EIO", eIO, cgofuse.EIO},
{"ENXIO", eNXIO, cgofuse.ENXIO},
{"EBADF", eBADF, cgofuse.EBADF},
{"EAGAIN", eAGAIN, cgofuse.EAGAIN},
{"ENOMEM", eNOMEM, cgofuse.ENOMEM},
{"EACCES", eACCES, cgofuse.EACCES},
{"EBUSY", eBUSY, cgofuse.EBUSY},
{"EEXIST", eEXIST, cgofuse.EEXIST},
{"EXDEV", eXDEV, cgofuse.EXDEV},
{"ENODEV", eNODEV, cgofuse.ENODEV},
{"ENOTDIR", eNOTDIR, cgofuse.ENOTDIR},
{"EISDIR", eISDIR, cgofuse.EISDIR},
{"EINVAL", eINVAL, cgofuse.EINVAL},
{"ENFILE", eNFILE, cgofuse.ENFILE},
{"EMFILE", eMFILE, cgofuse.EMFILE},
{"EFBIG", eFBIG, cgofuse.EFBIG},
{"ENOSPC", eNOSPC, cgofuse.ENOSPC},
{"ESPIPE", eSPIPE, cgofuse.ESPIPE},
{"EROFS", eROFS, cgofuse.EROFS},
{"EMLINK", eMLINK, cgofuse.EMLINK},
{"EPIPE", ePIPE, cgofuse.EPIPE},
{"ERANGE", eRANGE, cgofuse.ERANGE},
{"ENAMETOOLONG", eNAMETOOLONG, cgofuse.ENAMETOOLONG},
{"ENOSYS", eNOSYS, cgofuse.ENOSYS},
{"ENOTEMPTY", eNOTEMPTY, cgofuse.ENOTEMPTY},
{"ELOOP", eLOOP, cgofuse.ELOOP},
{"ENODATA", eNODATA, cgofuse.ENODATA},
{"ENOTSUP", eNOTSUP, cgofuse.ENOTSUP},
} {
if c.ours != c.want {
t.Errorf("%s = %d, cgofuse uses %d", c.name, c.ours, c.want)
}
}
}
// translateOpenFlags relies on cgofuse using MSVC's numbering while the raw
// filesystem reads Go's. Pin that, because a swap between O_EXCL and O_TRUNC
// would turn "fail if it exists" into "truncate it".
func TestOpenFlagTranslation(t *testing.T) {
for _, c := range []struct {
name string
in int
want uint32
}{
{"O_EXCL", cgofuse.O_EXCL, uint32(syscall.O_EXCL)},
{"O_TRUNC", cgofuse.O_TRUNC, uint32(syscall.O_TRUNC)},
{"O_CREAT", cgofuse.O_CREAT, uint32(syscall.O_CREAT)},
{"O_APPEND", cgofuse.O_APPEND, uint32(syscall.O_APPEND)},
} {
if got := translateOpenFlags(c.in); got != c.want {
t.Errorf("translateOpenFlags(%s=%#x) = %#x, want %#x", c.name, c.in, got, c.want)
}
}
if got := translateOpenFlags(cgofuse.O_RDWR); got != uint32(syscall.O_RDWR) {
t.Errorf("access mode not preserved: %#x", got)
}
}
// Setxattr passes cgofuse's flags straight to the raw filesystem, which is
// only correct while the two number them identically. The other side is
// pinned by TestXattrFlagValues in weed/mount, so editing either alone fails.
func TestXattrFlagValues(t *testing.T) {
if cgofuse.XATTR_CREATE != 1 || cgofuse.XATTR_REPLACE != 2 {
t.Fatalf("cgofuse xattr flags moved: create=%d replace=%d", cgofuse.XATTR_CREATE, cgofuse.XATTR_REPLACE)
}
}

View File

@@ -0,0 +1,855 @@
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
)
// 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)
// Taken before the lookups: a purge racing this walk - a rename or unlink
// landing between a Lookup and its insert - makes what was just resolved
// the very thing the purge removed.
gen := w.paths.snapshot()
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, gen)
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 to an inode reference the caller owns, for
// the open paths that hold it for the life of a handle: a cached entry is
// stolen, anything else is looked up directly so the reference never passes
// through the cache - an open must not depend on an insert surviving the
// purges racing it. 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)
if key == "" {
return rootInode, fuse.OK
}
if stolen, ok := w.paths.steal(key); ok {
return stolen, fuse.OK
}
parent, name, status := w.resolveParent(path)
if status != fuse.OK {
return 0, status
}
var out fuse.EntryOut
if status := w.wfs.Lookup(never, ptr(w.caller(parent)), name, &out); status != fuse.OK {
return 0, status
}
return out.NodeId, fuse.OK
}
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, w.paths.snapshot())
}
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, gen := cacheKey(path), w.paths.snapshot(); attr != nil && w.stillNames(key, inode) {
w.paths.insert(key, inode, *attr, gen)
} 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
}

View File

@@ -0,0 +1,155 @@
package winfsp
import (
"fmt"
"strconv"
"strings"
"time"
cgofuse "github.com/winfsp/cgofuse/fuse"
"github.com/seaweedfs/seaweedfs/weed/mount"
)
// Options are the WinFsp-specific knobs the mount command passes through.
type Options struct {
// VolumeName labels the drive in Explorer.
VolumeName string
// Uid and Gid are reported for every entry. WinFsp overrides what is
// reported anyway (see the uid=-1 option below), so these are what gets
// written to the filer and read by every other client.
Uid uint32
Gid uint32
// CacheTimeout bounds how long resolved paths and attributes may be
// served from the adapter's cache, and how long WinFsp may serve cached
// directory listings. Metadata events shorten it by purging; nothing
// else invalidates these caches.
CacheTimeout time.Duration
// ReadOnly rejects every modification. WinFsp has no "ro" option — it
// discards the flag and leaves the volume writable — so the refusal has
// to happen in the operations themselves.
ReadOnly bool
// Debug turns on cgofuse's operation trace.
Debug bool
// ExtraOptions are passed through to WinFsp as -o arguments, after the
// defaults, so they win when they name the same option.
ExtraOptions []string
}
// Host is a WinFsp mount that has not been started yet.
type Host struct {
host *cgofuse.FileSystemHost
fs *WinFS
options Options
}
// New wires wfs up to WinFsp. Nothing is mounted until Serve.
func New(wfs *mount.WFS, options Options) *Host {
fs := NewWinFS(wfs, options.Uid, options.Gid, options.ReadOnly, options.CacheTimeout)
host := cgofuse.NewFileSystemHost(fs)
host.SetCapReaddirPlus(true)
host.SetUseIno(true)
return &Host{host: host, fs: fs, options: options}
}
// Notify wires the mount's metadata events to Windows. Called once before
// Serve; the host has to exist first, which is why it is not done in New.
func (h *Host) Notify(wfs *mount.WFS) {
n := &notifier{host: h.host, fs: h.fs, mountRoot: wfs.MountRoot()}
wfs.SetEntryChangeListener(n.notify)
}
// Serve attaches the filesystem at mountPoint, which is a drive letter ("S:"),
// a directory that does not yet exist, or a \\server\share UNC path. It blocks
// until the filesystem is unmounted.
func (h *Host) Serve(mountPoint string) error {
prefix, err := VolumePrefix(mountPoint)
if err != nil {
return err
}
opts := []string{
"-o", "volname=" + h.volumeName(),
"-o", "uid=-1",
"-o", "gid=-1",
// Only an infinite FileInfoTimeout lets the Windows cache manager
// cache file data; at any finite value every application read and
// write is a synchronous trip into this process at whatever size the
// application issued. Remote changes stay visible because every
// applied metadata event goes through Notify, which purges the file's
// cached pages along with its attributes.
//
// KeepFileCache is deliberately absent: it would keep the cache alive
// past cleanup, deferring the close — and with it the flush that
// persists a written file — until Windows reclaims the memory.
"-o", "FileInfoTimeout=-1",
// FlushOnCleanup is absent for the same reason: it makes every
// handle's cleanup flush, and those flushes race the unlinks of
// delete-on-close. The flush stays at close, which WinFsp runs after
// CloseHandle has returned; the mount persists entries eagerly at
// create instead, so nothing that reads through the filer depends on
// when the flush runs.
}
if h.options.CacheTimeout > 0 {
ms := strconv.FormatInt(h.options.CacheTimeout.Milliseconds(), 10)
// These would silently inherit the infinite FileInfoTimeout.
opts = append(opts,
"-o", "DirInfoTimeout="+ms,
"-o", "VolumeInfoTimeout="+ms,
"-o", "EaTimeout="+ms,
)
}
if prefix != "" {
// The mount point itself becomes the network prefix; what WinFsp
// still wants a mount point for is the drive letter it gives the
// mounting session, and "*" lets it pick a free one.
opts = append(opts, "-o", "VolumePrefix="+prefix)
mountPoint = "*"
}
if h.options.Debug {
opts = append(opts, "-d")
}
for _, extra := range h.options.ExtraOptions {
opts = append(opts, "-o", extra)
}
if err := h.mount(mountPoint, opts); err != nil {
return err
}
return nil
}
// mount turns a refusal into an error. cgofuse panics rather than returning
// when winfsp-x64.dll is missing, which is the most likely reason for a
// failure here and the one worth naming.
func (h *Host) mount(mountPoint string, opts []string) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("mounting %s failed (%v); is WinFsp installed?", mountPoint, r)
}
}()
if !h.host.Mount(mountPoint, opts) {
return fmt.Errorf("WinFsp refused to mount %s; check that WinFsp is installed and the mount point is free", mountPoint)
}
return nil
}
// Unmount detaches the filesystem, releasing a blocked Serve.
func (h *Host) Unmount() bool {
return h.host.Unmount()
}
// volumeName keeps the label parseable: WinFsp splits options on commas, so a
// label carrying one would be cut short and take the rest of the option string
// with it.
func (h *Host) volumeName() string {
name := strings.ReplaceAll(h.options.VolumeName, ",", "+")
if name == "" {
return "SeaweedFS"
}
return name
}

View File

@@ -0,0 +1,32 @@
package winfsp
import (
"fmt"
"strings"
)
// VolumePrefix converts a \\server\share mount point into WinFsp's
// VolumePrefix option, which registers the mount as a network file system.
// That registration is what makes the mount reachable from every logon
// session by its UNC path — a drive letter lives inside the session that
// created it, which is why a service mount is invisible to the users logged
// on at the desktop. Handed through as a plain mount point instead, the UNC
// path would be taken for a directory on an actual remote server and the
// mount would fail.
//
// Mount points that are not UNC paths return "", including the \\?\ and \\.\
// device forms, which WinFsp interprets itself.
func VolumePrefix(mountPoint string) (string, error) {
normalized := strings.ReplaceAll(mountPoint, "/", `\`)
if !strings.HasPrefix(normalized, `\\`) {
return "", nil
}
if strings.HasPrefix(normalized, `\\?\`) || strings.HasPrefix(normalized, `\\.\`) {
return "", nil
}
parts := strings.Split(strings.TrimRight(normalized[2:], `\`), `\`)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return "", fmt.Errorf(`a network mount point must be \\server\share, got %s`, mountPoint)
}
return `\` + parts[0] + `\` + parts[1], nil
}

View File

@@ -0,0 +1,37 @@
package winfsp
import "testing"
func TestVolumePrefix(t *testing.T) {
tests := []struct {
name string
mountPoint string
prefix string
wantErr bool
}{
{name: "drive letter", mountPoint: `S:`, prefix: ""},
{name: "directory", mountPoint: `C:\mnt\weed`, prefix: ""},
{name: "relative directory", mountPoint: `mnt\weed`, prefix: ""},
{name: "unc", mountPoint: `\\seaweedfs\share`, prefix: `\seaweedfs\share`},
{name: "unc trailing separator", mountPoint: `\\seaweedfs\share\`, prefix: `\seaweedfs\share`},
{name: "unc forward slashes", mountPoint: `//seaweedfs/share`, prefix: `\seaweedfs\share`},
{name: "unc dotted server", mountPoint: `\\fs.example.com\share`, prefix: `\fs.example.com\share`},
{name: "mountmgr drive", mountPoint: `\\.\S:`, prefix: ""},
{name: "extended drive", mountPoint: `\\?\S:`, prefix: ""},
{name: "server only", mountPoint: `\\seaweedfs`, wantErr: true},
{name: "too deep", mountPoint: `\\seaweedfs\share\dir`, wantErr: true},
{name: "empty server", mountPoint: `\\\share`, wantErr: true},
{name: "empty share", mountPoint: `\\seaweedfs\\`, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prefix, err := VolumePrefix(tt.mountPoint)
if (err != nil) != tt.wantErr {
t.Fatalf("VolumePrefix(%q) error = %v, wantErr %v", tt.mountPoint, err, tt.wantErr)
}
if prefix != tt.prefix {
t.Fatalf("VolumePrefix(%q) = %q, want %q", tt.mountPoint, prefix, tt.prefix)
}
})
}
}

View File

@@ -0,0 +1,25 @@
package winfsp
import (
"strings"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// relativeToMount rebases a filer path onto the mount root. Metadata events
// cover the whole subscription, so a path outside this mount is not ours to
// report, and a sibling whose name merely starts with the root is not inside
// it either.
func relativeToMount(root, path util.FullPath) (string, bool) {
full, prefix := string(path), string(root)
if prefix == "" || prefix == "/" {
return full, full != ""
}
if full == prefix {
return "/", true
}
if !strings.HasPrefix(full, prefix+"/") {
return "", false
}
return full[len(prefix):], true
}

View File

@@ -0,0 +1,34 @@
package winfsp
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// Event paths are absolute on the filer and cover the whole subscription, so
// rebasing them decides both what Windows is told and what is none of its
// business.
func TestRelativeToMount(t *testing.T) {
cases := []struct {
root string
path string
want string
ok bool
}{
{"/", "/a/b.txt", "/a/b.txt", true},
{"", "/a/b.txt", "/a/b.txt", true},
{"/buckets/data", "/buckets/data/a/b.txt", "/a/b.txt", true},
{"/buckets/data", "/buckets/data", "/", true},
{"/buckets/data", "/buckets/other/b.txt", "", false},
// A sibling whose name merely starts with the root must not match.
{"/buckets/data", "/buckets/data2/b.txt", "", false},
{"/buckets/data", "/elsewhere", "", false},
}
for _, c := range cases {
got, ok := relativeToMount(util.FullPath(c.root), util.FullPath(c.path))
if ok != c.ok || (ok && got != c.want) {
t.Errorf("root %q path %q = (%q, %v), want (%q, %v)", c.root, c.path, got, ok, c.want, c.ok)
}
}
}

View File

@@ -0,0 +1,63 @@
package winfsp
import (
cgofuse "github.com/winfsp/cgofuse/fuse"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// notifier turns the mount's metadata events into the notifications Windows
// listens for. Nothing invalidates a Windows client's cache from this side, so
// without it a change made by another mount, the S3 gateway or the filer API
// stays invisible until the user refreshes by hand.
type notifier struct {
host *cgofuse.FileSystemHost
fs *WinFS
mountRoot util.FullPath
}
// notify reports one applied event. Windows wants the path relative to the
// mount, and an action describing what happened to it; the adapter's own path
// cache is out of date for the same path.
func (n *notifier) notify(invalidation meta_cache.EntryInvalidation) {
if n.host == nil {
return
}
// A rename arrives as two invalidations, one vacating the old path and one
// describing the new, so reporting RenamedTo here as well would send the
// destination twice — and as a create even when a directory moved.
if path, ok := relativeToMount(n.mountRoot, invalidation.Path); ok {
isDirectory := invalidation.WasDirectory || invalidation.Entry.GetIsDirectory()
n.fs.invalidatePath(path, isDirectory)
n.send(path, n.action(invalidation))
}
}
func (n *notifier) send(path string, action uint32) {
if !n.host.Notify(path, action) {
// A rejected notification only costs a stale view until the client
// looks again, so it is not worth failing an operation over.
glog.V(4).Infof("winfsp notify %s action %d rejected", path, action)
}
}
// action picks what to tell Windows happened. It cannot distinguish a created
// entry from a modified one, and Windows treats an unexpected create on an
// existing name as a refresh, so create is the safe report.
func (n *notifier) action(invalidation meta_cache.EntryInvalidation) uint32 {
if invalidation.Deleted || invalidation.Entry == nil {
// The entry is gone, so only WasDirectory still says what it was, and
// Windows watches directory and file removals through different
// filters.
if invalidation.WasDirectory {
return cgofuse.NOTIFY_RMDIR
}
return cgofuse.NOTIFY_UNLINK
}
if invalidation.Entry.GetIsDirectory() {
return cgofuse.NOTIFY_MKDIR
}
return cgofuse.NOTIFY_CREATE
}

33
weed/mount/winfsp/path.go Normal file
View File

@@ -0,0 +1,33 @@
package winfsp
import "strings"
// splitPath breaks a WinFsp path into the components to walk. WinFsp normally
// hands out forward slashes, but callers reach the mount with either separator,
// and empty or "." components have to drop out rather than become a lookup for
// a name that does not exist.
func splitPath(path string) []string {
trimmed := strings.Trim(strings.ReplaceAll(path, `\`, "/"), "/")
if trimmed == "" {
return nil
}
parts := make([]string, 0, strings.Count(trimmed, "/")+1)
for _, name := range strings.Split(trimmed, "/") {
if name == "" || name == "." {
continue
}
parts = append(parts, name)
}
return parts
}
// splitParent separates the components leading up to the final name, which the
// create and delete operations need apart. ok is false for the root, which has
// no parent to operate in.
func splitParent(path string) (parent []string, name string, ok bool) {
parts := splitPath(path)
if len(parts) == 0 {
return nil, "", false
}
return parts[:len(parts)-1], parts[len(parts)-1], true
}

View File

@@ -0,0 +1,78 @@
package winfsp
import (
"strings"
"testing"
)
func TestSplitPath(t *testing.T) {
cases := []struct {
in string
want []string
}{
{"/", nil},
{"", nil},
{"//", nil},
{".", nil},
{"/foo", []string{"foo"}},
{"foo", []string{"foo"}},
{"/foo/", []string{"foo"}},
{"/foo/bar", []string{"foo", "bar"}},
{"/foo//bar", []string{"foo", "bar"}},
{"/foo/./bar", []string{"foo", "bar"}},
{`\foo\bar`, []string{"foo", "bar"}},
{`/foo\bar`, []string{"foo", "bar"}},
{"/foo bar/baz qux", []string{"foo bar", "baz qux"}},
{"/eñe/日本", []string{"eñe", "日本"}},
// ".." is a name like any other here; the filer resolves it, and
// treating it structurally would let a path escape the mount root.
{"/foo/../bar", []string{"foo", "..", "bar"}},
}
for _, c := range cases {
got := splitPath(c.in)
if len(got) != len(c.want) {
t.Errorf("splitPath(%q) = %v, want %v", c.in, got, c.want)
continue
}
for i := range got {
if got[i] != c.want[i] {
t.Errorf("splitPath(%q) = %v, want %v", c.in, got, c.want)
break
}
}
}
}
func TestSplitParent(t *testing.T) {
cases := []struct {
in string
wantParent string
wantName string
wantOK bool
}{
{"/", "", "", false},
{"", "", "", false},
{"/foo", "", "foo", true},
{"/foo/bar", "foo", "bar", true},
{"/foo/bar/baz", "foo/bar", "baz", true},
{`\foo\bar`, "foo", "bar", true},
{"/foo/bar/", "foo", "bar", true},
}
for _, c := range cases {
parent, name, ok := splitParent(c.in)
if ok != c.wantOK || name != c.wantName || strings.Join(parent, "/") != c.wantParent {
t.Errorf("splitParent(%q) = (%q, %q, %v), want (%q, %q, %v)",
c.in, strings.Join(parent, "/"), name, ok, c.wantParent, c.wantName, c.wantOK)
}
}
}
// The root has no parent, so operations that need one must be rejected rather
// than silently acting on the root itself.
func TestSplitParentRejectsRoot(t *testing.T) {
for _, root := range []string{"/", "", "//", `\`} {
if _, _, ok := splitParent(root); ok {
t.Errorf("splitParent(%q) accepted the root", root)
}
}
}

View File

@@ -0,0 +1,233 @@
package winfsp
import (
"strings"
"sync"
"time"
"github.com/seaweedfs/go-fuse/v2/fuse"
)
// pathCache stands in for the dentry and attribute caches the kernel provides
// on the unix mounts. WinFsp addresses every operation by path, so without it
// each operation walks the whole path through Lookup again, and in a directory
// the filer has not listed yet every one of those lookups is a filer round
// trip.
//
// The cache owns one lookup reference per entry, the way the kernel holds one
// until it sends FORGET. An evicted reference sits out one sweep in the
// graveyard before it is returned, so an operation that resolved just before
// the eviction is not left holding a reclaimed inode.
type pathCache struct {
ttl time.Duration
forget func(inode uint64)
mu sync.Mutex
entries map[string]*pathCacheEntry
// Two graveyard generations: an appended reference always survives the
// sweep of the call that appended it, so a caller still using the inode -
// a walk mid-resolution, an operation that looked it up just before - is
// never holding a reference the same call just returned.
graveyard []uint64
prevGraveyard []uint64
lastSweep time.Time
// gen counts purges. A resolve holds no lock across its Lookup RPC, so a
// purge can run between the RPC and the insert; the insert then carries
// exactly the name the purge removed and has to be discarded. The recent
// purges are kept by key so only a purge that covers the inserted name
// discards it: unrelated churn must not starve resolveAndSteal into EIO.
gen uint64
recentPurges []purgeRecord
}
type purgeRecord struct {
key string
prefix bool
}
// maxRecentPurges bounds the purges remembered for the covers check. A resolve
// older than the window is discarded without one, which only costs a retry.
const maxRecentPurges = 128
func (r purgeRecord) covers(key string) bool {
if r.key == key {
return true
}
if !r.prefix {
return false
}
// An empty key with prefix is the whole cache: purge clears every entry
// for it, so it has to cover every in-flight insert too.
return r.key == "" || strings.HasPrefix(key, r.key+"/")
}
// purgedSince reports whether any purge after gen covers key.
func (c *pathCache) purgedSince(gen uint64, key string) bool {
dropped := c.gen - gen
if dropped == 0 {
return false
}
if dropped > uint64(len(c.recentPurges)) {
return true
}
for _, r := range c.recentPurges[uint64(len(c.recentPurges))-dropped:] {
if r.covers(key) {
return true
}
}
return false
}
type pathCacheEntry struct {
inode uint64
attr fuse.Attr
expires time.Time
}
// maxCachedPaths bounds the references parked here. Overflow clears the whole
// cache rather than tracking recency: entries expire within ttl anyway, so
// exact eviction order buys nothing.
const maxCachedPaths = 64 << 10
func newPathCache(ttl time.Duration, forget func(inode uint64)) *pathCache {
return &pathCache{
ttl: ttl,
forget: forget,
entries: map[string]*pathCacheEntry{},
lastSweep: time.Now(),
}
}
// cacheKey canonicalises a WinFsp path. The root maps to "", which is never
// cached: its inode is fixed and holds no reference.
func cacheKey(path string) string {
return strings.Join(splitPath(path), "/")
}
// lookup reports the cached inode and attributes for key. The entry stays in
// the cache; the inode remains valid at least one sweep past its expiry.
func (c *pathCache) lookup(key string) (inode uint64, attr fuse.Attr, ok bool) {
if key == "" {
return 0, fuse.Attr{}, false
}
c.mu.Lock()
defer c.mu.Unlock()
entry, found := c.entries[key]
if !found || time.Now().After(entry.expires) {
return 0, fuse.Attr{}, false
}
return entry.inode, entry.attr, true
}
// snapshot returns the purge generation. A caller about to resolve outside
// the lock passes it back to insert, which discards the entry if any purge ran
// in between.
func (c *pathCache) snapshot() uint64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.gen
}
// insert takes ownership of one lookup reference on inode, in every outcome:
// an entry a covering purge outdated goes to the graveyard instead of the map,
// so the caller may keep using the inode for at least one sweep either way.
func (c *pathCache) insert(key string, inode uint64, attr fuse.Attr, gen uint64) {
if key == "" {
c.forget(inode)
return
}
var pending []uint64
c.mu.Lock()
if c.purgedSince(gen, key) {
c.graveyard = append(c.graveyard, inode)
pending = c.sweepLocked()
c.mu.Unlock()
c.forgetAll(pending)
return
}
if existing, found := c.entries[key]; found {
c.graveyard = append(c.graveyard, existing.inode)
} else if len(c.entries) >= maxCachedPaths {
for _, entry := range c.entries {
c.graveyard = append(c.graveyard, entry.inode)
}
c.entries = map[string]*pathCacheEntry{}
}
c.entries[key] = &pathCacheEntry{inode: inode, attr: attr, expires: time.Now().Add(c.ttl)}
pending = c.sweepLocked()
c.mu.Unlock()
c.forgetAll(pending)
}
// steal removes key and hands its reference to the caller.
func (c *pathCache) steal(key string) (inode uint64, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
entry, found := c.entries[key]
if !found {
return 0, false
}
delete(c.entries, key)
return entry.inode, true
}
// purge drops key, and everything under it when prefix is set, which a rename
// or removal of a directory needs: the children's cached paths name entries
// that are no longer there.
func (c *pathCache) purge(key string, prefix bool) {
var pending []uint64
c.mu.Lock()
// Recorded even when the map holds nothing: the point is as much the
// in-flight resolve about to insert this very name.
c.gen++
c.recentPurges = append(c.recentPurges, purgeRecord{key: key, prefix: prefix})
if len(c.recentPurges) > maxRecentPurges {
c.recentPurges = append(c.recentPurges[:0], c.recentPurges[len(c.recentPurges)-maxRecentPurges:]...)
}
if entry, found := c.entries[key]; found {
c.graveyard = append(c.graveyard, entry.inode)
delete(c.entries, key)
}
if prefix {
under := key + "/"
for k, entry := range c.entries {
if key == "" || strings.HasPrefix(k, under) {
c.graveyard = append(c.graveyard, entry.inode)
delete(c.entries, k)
}
}
}
pending = c.sweepLocked()
c.mu.Unlock()
c.forgetAll(pending)
}
// sweepLocked returns the generation before last for the caller to forget
// outside the lock, and retires the current one. Sweeps run at most once per
// ttl and an appended reference sits out the sweep of its own call, so every
// reference rests for at least one full ttl after its last possible use.
func (c *pathCache) sweepLocked() []uint64 {
now := time.Now()
if now.Sub(c.lastSweep) < c.ttl {
return nil
}
c.lastSweep = now
pending := c.prevGraveyard
c.prevGraveyard = c.graveyard
c.graveyard = nil
for key, entry := range c.entries {
if now.After(entry.expires) {
// Returned by the next sweep, a full ttl away: served from the map
// until a moment ago, so someone may still be holding it.
c.prevGraveyard = append(c.prevGraveyard, entry.inode)
delete(c.entries, key)
}
}
return pending
}
func (c *pathCache) forgetAll(inodes []uint64) {
for _, inode := range inodes {
c.forget(inode)
}
}

View File

@@ -0,0 +1,269 @@
package winfsp
import (
"fmt"
"sync"
"testing"
"time"
"github.com/seaweedfs/go-fuse/v2/fuse"
)
type forgetRecorder struct {
mu sync.Mutex
inodes []uint64
}
func (r *forgetRecorder) forget(inode uint64) {
r.mu.Lock()
defer r.mu.Unlock()
r.inodes = append(r.inodes, inode)
}
func (r *forgetRecorder) forgotten() []uint64 {
r.mu.Lock()
defer r.mu.Unlock()
return append([]uint64(nil), r.inodes...)
}
func TestPathCacheLookupAndExpiry(t *testing.T) {
rec := &forgetRecorder{}
c := newPathCache(20*time.Millisecond, rec.forget)
c.insert("a/b", 7, fuse.Attr{Ino: 7, Size: 42}, c.snapshot())
inode, attr, ok := c.lookup("a/b")
if !ok || inode != 7 || attr.Size != 42 {
t.Fatalf("lookup = %d %v %v, want 7 size 42 true", inode, attr.Size, ok)
}
time.Sleep(30 * time.Millisecond)
if _, _, ok := c.lookup("a/b"); ok {
t.Fatal("expired entry still served")
}
// The reference survives at least one sweep past expiry before it is
// forgotten: one insert moves it to the graveyard, the next returns it.
c.insert("x", 8, fuse.Attr{}, c.snapshot())
time.Sleep(30 * time.Millisecond)
c.insert("y", 9, fuse.Attr{}, c.snapshot())
found := false
for _, inode := range rec.forgotten() {
if inode == 7 {
found = true
}
}
if !found {
t.Fatalf("expired reference never forgotten; forgot %v", rec.forgotten())
}
}
func TestPathCacheStealTransfersOwnership(t *testing.T) {
rec := &forgetRecorder{}
c := newPathCache(20*time.Millisecond, rec.forget)
c.insert("a", 5, fuse.Attr{}, c.snapshot())
inode, ok := c.steal("a")
if !ok || inode != 5 {
t.Fatalf("steal = %d %v, want 5 true", inode, ok)
}
if _, ok := c.steal("a"); ok {
t.Fatal("second steal succeeded")
}
// Drive several sweeps; the stolen reference must never be forgotten.
for i := 0; i < 4; i++ {
time.Sleep(25 * time.Millisecond)
c.insert("churn", uint64(100+i), fuse.Attr{}, c.snapshot())
}
for _, inode := range rec.forgotten() {
if inode == 5 {
t.Fatal("stolen reference was forgotten by the cache")
}
}
}
func TestPathCacheReplaceReturnsOldReference(t *testing.T) {
rec := &forgetRecorder{}
c := newPathCache(20*time.Millisecond, rec.forget)
c.insert("a", 5, fuse.Attr{}, c.snapshot())
c.insert("a", 6, fuse.Attr{}, c.snapshot())
if inode, _, ok := c.lookup("a"); !ok || inode != 6 {
t.Fatalf("lookup after replace = %d %v, want 6 true", inode, ok)
}
for i := 0; i < 3; i++ {
time.Sleep(25 * time.Millisecond)
c.insert("churn", uint64(100+i), fuse.Attr{}, c.snapshot())
}
found := false
for _, inode := range rec.forgotten() {
if inode == 5 {
found = true
}
if inode == 6 && !found {
t.Fatal("live reference forgotten before the replaced one")
}
}
if !found {
t.Fatalf("replaced reference never forgotten; forgot %v", rec.forgotten())
}
}
func TestPathCachePurgePrefix(t *testing.T) {
rec := &forgetRecorder{}
c := newPathCache(time.Minute, rec.forget)
c.insert("dir", 2, fuse.Attr{}, c.snapshot())
c.insert("dir/a", 3, fuse.Attr{}, c.snapshot())
c.insert("dir/a/b", 4, fuse.Attr{}, c.snapshot())
c.insert("dirt", 5, fuse.Attr{}, c.snapshot())
c.purge("dir", true)
if _, _, ok := c.lookup("dir"); ok {
t.Fatal("purged key still cached")
}
if _, _, ok := c.lookup("dir/a/b"); ok {
t.Fatal("purged subtree still cached")
}
// A sibling that only shares the name as a string prefix stays.
if _, _, ok := c.lookup("dirt"); !ok {
t.Fatal("sibling was purged with the subtree")
}
}
func TestPathCacheRootNeverCached(t *testing.T) {
rec := &forgetRecorder{}
c := newPathCache(time.Minute, rec.forget)
c.insert("", 9, fuse.Attr{}, c.snapshot())
if _, _, ok := c.lookup(""); ok {
t.Fatal("root was cached")
}
if got := rec.forgotten(); len(got) != 1 || got[0] != 9 {
t.Fatalf("root reference not returned immediately: %v", got)
}
}
func TestCacheKey(t *testing.T) {
for path, want := range map[string]string{
`/a/b`: "a/b",
`\a\b`: "a/b",
`a//b/`: "a/b",
`/`: "",
``: "",
`/a/./b`: "a/b",
} {
if got := cacheKey(path); got != want {
t.Errorf("cacheKey(%q) = %q, want %q", path, got, want)
}
}
}
// A resolve runs its Lookup outside the cache lock, so a purge can land
// between the RPC and the insert. An insert a purge covered must then be
// discarded: it carries exactly the name the purge removed, and caching it
// would serve a deleted entry for a full ttl. This is how a rename's source
// briefly came back from the dead on the Windows mount.
func TestPathCacheInsertAfterCoveringPurgeIsDiscarded(t *testing.T) {
c := newPathCache(time.Minute, func(uint64) {})
gen := c.snapshot()
c.purge("dir/src", false)
c.insert("dir/src", 42, fuse.Attr{}, gen)
if _, _, ok := c.lookup("dir/src"); ok {
t.Fatal("purged name served from an insert that started before the purge")
}
}
// A purge of a directory covers the resolves in flight under it.
func TestPathCacheInsertUnderPurgedPrefixIsDiscarded(t *testing.T) {
c := newPathCache(time.Minute, func(uint64) {})
gen := c.snapshot()
c.purge("dir", true)
c.insert("dir/child", 7, fuse.Attr{}, gen)
if _, _, ok := c.lookup("dir/child"); ok {
t.Fatal("purged prefix served a child from a stale insert")
}
}
// A purge of something else entirely must not discard the insert: an open
// retries resolve-then-steal only a few times before giving up with EIO, so
// unrelated churn starving every insert would fail opens of untouched paths.
func TestPathCacheUnrelatedPurgeDoesNotDiscard(t *testing.T) {
c := newPathCache(time.Minute, func(uint64) {})
gen := c.snapshot()
c.purge("elsewhere", true)
c.purge("dir/srcling", false)
c.insert("dir/src", 9, fuse.Attr{Size: 11}, gen)
inode, attr, ok := c.lookup("dir/src")
if !ok || inode != 9 || attr.Size != 11 {
t.Fatalf("lookup = %d,%d,%v, want the inserted entry", inode, attr.Size, ok)
}
}
// Purges beyond the remembered window cannot be checked by key, so the insert
// is discarded rather than trusted.
func TestPathCacheInsertOlderThanPurgeWindowIsDiscarded(t *testing.T) {
c := newPathCache(time.Minute, func(uint64) {})
gen := c.snapshot()
for i := 0; i <= maxRecentPurges; i++ {
c.purge(fmt.Sprintf("unrelated/%d", i), false)
}
c.insert("dir/src", 3, fuse.Attr{}, gen)
if _, _, ok := c.lookup("dir/src"); ok {
t.Fatal("insert older than the purge window was trusted")
}
}
// The discarded insert still owns a lookup reference, which has to come back
// through the graveyard rather than leak - and not in the same call: the
// walker is still using the inode.
func TestPathCacheDiscardedInsertRestsBeforeForget(t *testing.T) {
rec := &forgetRecorder{}
c := newPathCache(10*time.Millisecond, rec.forget)
gen := c.snapshot()
c.purge("dir/src", false)
// Make a sweep due, so the discarding insert itself sweeps: the reference
// it just parked must not come back out of that same call.
time.Sleep(15 * time.Millisecond)
c.insert("dir/src", 42, fuse.Attr{}, gen)
for _, inode := range rec.forgotten() {
if inode == 42 {
t.Fatal("discarded insert forgotten in the same call; the walker still holds it")
}
}
for i := 0; i < 3; i++ {
time.Sleep(15 * time.Millisecond)
c.purge("churn", false)
}
found := false
for _, inode := range rec.forgotten() {
if inode == 42 {
found = true
}
}
if !found {
t.Fatalf("discarded insert leaked its lookup reference; forgot %v", rec.forgotten())
}
}
// A purge of the whole cache - the root, prefix set - covers every in-flight
// insert, exactly as it removed every entry.
func TestPathCacheRootPurgeCoversEveryInsert(t *testing.T) {
c := newPathCache(time.Minute, func(uint64) {})
gen := c.snapshot()
c.purge("", true)
c.insert("dir/src", 42, fuse.Attr{}, gen)
if _, _, ok := c.lookup("dir/src"); ok {
t.Fatal("root purge did not cover an in-flight insert")
}
}

View File

@@ -0,0 +1,86 @@
package winfsp
import (
"bytes"
"github.com/seaweedfs/go-fuse/v2/fuse"
)
// xattrBufferSize bounds one attribute value and one name listing. The mount
// stores extended attributes in the entry itself, so they are small by
// construction; this is the ceiling WinFsp will accept in a single reply.
const xattrBufferSize = 64 * 1024
func (w *WinFS) Getxattr(path string, name string) (int, []byte) {
inode, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status), nil
}
dest := make([]byte, xattrBufferSize)
size, status := w.wfs.GetXAttr(never, ptr(w.caller(inode)), name, dest)
if status != fuse.OK {
return toErrno(status), nil
}
return 0, dest[:size]
}
func (w *WinFS) Setxattr(path string, name string, value []byte, flags int) int {
if w.denied() {
return -eROFS
}
inode, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
// cgofuse and the raw filesystem number XATTR_CREATE and XATTR_REPLACE the
// same, so the flags pass straight through; TestXattrFlagValues pins that.
in := &fuse.SetXAttrIn{InHeader: w.caller(inode)}
in.Flags = uint32(flags)
in.Size = uint32(len(value))
return toErrno(w.wfs.SetXAttr(never, in, name, value))
}
func (w *WinFS) Removexattr(path string, name string) int {
if w.denied() {
return -eROFS
}
inode, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
return toErrno(w.wfs.RemoveXAttr(never, ptr(w.caller(inode)), name))
}
func (w *WinFS) Listxattr(path string, fill func(name string) bool) int {
inode, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
dest := make([]byte, xattrBufferSize)
size, status := w.wfs.ListXAttr(never, ptr(w.caller(inode)), dest)
if status != fuse.OK {
return toErrno(status)
}
for _, name := range splitXattrNames(dest[:size]) {
if !fill(name) {
break
}
}
return 0
}
// splitXattrNames unpacks the NUL-separated listing the raw filesystem
// produces. A trailing NUL terminates the last name rather than starting an
// empty one.
func splitXattrNames(packed []byte) []string {
var names []string
for _, name := range bytes.Split(packed, []byte{0}) {
if len(name) > 0 {
names = append(names, string(name))
}
}
return names
}