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
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:
118
weed/mount/winfsp/errno.go
Normal file
118
weed/mount/winfsp/errno.go
Normal 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
|
||||
}
|
||||
49
weed/mount/winfsp/errno_test.go
Normal file
49
weed/mount/winfsp/errno_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
87
weed/mount/winfsp/errno_windows_test.go
Normal file
87
weed/mount/winfsp/errno_windows_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
855
weed/mount/winfsp/fs_windows.go
Normal file
855
weed/mount/winfsp/fs_windows.go
Normal 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
|
||||
}
|
||||
155
weed/mount/winfsp/host_windows.go
Normal file
155
weed/mount/winfsp/host_windows.go
Normal 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 := ¬ifier{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
|
||||
}
|
||||
32
weed/mount/winfsp/mountpoint.go
Normal file
32
weed/mount/winfsp/mountpoint.go
Normal 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
|
||||
}
|
||||
37
weed/mount/winfsp/mountpoint_test.go
Normal file
37
weed/mount/winfsp/mountpoint_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
25
weed/mount/winfsp/notify.go
Normal file
25
weed/mount/winfsp/notify.go
Normal 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
|
||||
}
|
||||
34
weed/mount/winfsp/notify_test.go
Normal file
34
weed/mount/winfsp/notify_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
63
weed/mount/winfsp/notify_windows.go
Normal file
63
weed/mount/winfsp/notify_windows.go
Normal 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
33
weed/mount/winfsp/path.go
Normal 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
|
||||
}
|
||||
78
weed/mount/winfsp/path_test.go
Normal file
78
weed/mount/winfsp/path_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
233
weed/mount/winfsp/pathcache.go
Normal file
233
weed/mount/winfsp/pathcache.go
Normal 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)
|
||||
}
|
||||
}
|
||||
269
weed/mount/winfsp/pathcache_test.go
Normal file
269
weed/mount/winfsp/pathcache_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
86
weed/mount/winfsp/xattr_windows.go
Normal file
86
weed/mount/winfsp/xattr_windows.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user