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
856 lines
25 KiB
Go
856 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
|
|
)
|
|
|
|
// 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
|
|
}
|