├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── backend.go ├── buffer.go ├── cmd └── fuse-abort │ ├── .gitignore │ ├── internal │ └── mountinfo │ │ ├── .gitignore │ │ ├── fuzz │ │ ├── fuzz.go │ │ ├── mountinfo.go │ │ ├── mountinfo_test.go │ │ ├── testdata │ │ └── fuzz │ │ │ └── corpus │ │ │ ├── crashers.mountinfo │ │ │ ├── escaped.mountinfo │ │ │ └── real.mountinfo │ │ └── tools.go │ └── main.go ├── debug.go ├── doc ├── .gitignore ├── README.md ├── mount-linux-error-init.seq ├── mount-linux-error-init.seq.png ├── mount-linux.seq ├── mount-linux.seq.png ├── mount-osx-error-init.seq ├── mount-osx-error-init.seq.png ├── mount-osx.seq ├── mount-osx.seq.png ├── mount-sequence.md └── writing-docs.md ├── error_darwin.go ├── error_freebsd.go ├── error_linux.go ├── error_std.go ├── examples ├── clockfs │ └── clockfs.go ├── hellofs │ └── hello.go └── loopback │ ├── LICENSE │ ├── README.md │ ├── darwin.go │ ├── darwin_386.go │ ├── darwin_else.go │ ├── linux-64bit.go │ ├── linux.go │ ├── linux_386.go │ ├── loog │ └── log.go │ ├── loopback.go │ └── main.go ├── fs ├── bench │ ├── bench_create_test.go │ ├── bench_lookup_test.go │ ├── bench_readwrite_test.go │ ├── doc.go │ └── helpers_test.go ├── fstestutil │ ├── checkdir.go │ ├── debug.go │ ├── doc.go │ ├── mounted.go │ ├── mountinfo.go │ ├── mountinfo_darwin.go │ ├── mountinfo_freebsd.go │ ├── mountinfo_linux.go │ ├── record │ │ ├── buffer.go │ │ ├── record.go │ │ └── wait.go │ ├── spawntest │ │ ├── example_test.go │ │ ├── httpjson │ │ │ ├── client.go │ │ │ ├── doc.go │ │ │ ├── musteof.go │ │ │ └── server.go │ │ └── spawntest.go │ └── testfs.go ├── helpers_test.go ├── serve.go ├── serve_darwin_test.go ├── serve_freebsd_test.go ├── serve_linux_test.go ├── serve_test.go └── tree.go ├── fuse.go ├── fuse_darwin.go ├── fuse_freebsd.go ├── fuse_kernel.go ├── fuse_kernel_darwin.go ├── fuse_kernel_freebsd.go ├── fuse_kernel_linux.go ├── fuse_kernel_std.go ├── fuse_kernel_test.go ├── fuse_linux.go ├── fuse_test.go ├── fuseutil └── fuseutil.go ├── go.mod ├── go.sum ├── init.go ├── mount.go ├── mount_darwin.go ├── mount_freebsd.go ├── mount_linux.go ├── options.go ├── options_daemon_timeout_test.go ├── options_darwin.go ├── options_freebsd.go ├── options_helper_test.go ├── options_linux.go ├── options_nocomma_test.go ├── options_test.go ├── protocol.go ├── unmount.go ├── unmount_linux.go └── unmount_std.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go filter=gofmt 2 | *.cgo filter=gofmt 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .#* 3 | ## the next line needs to start with a backslash to avoid looking like 4 | ## a comment 5 | \#*# 6 | .*.swp 7 | 8 | *.test 9 | 10 | /clockfs 11 | /hellofs 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, 2023 Matt Joiner . 2 | Copyright (c) 2013-2019 Tommi Virtanen. 3 | Copyright (c) 2009, 2011, 2012 The Go Authors. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following disclaimer 14 | in the documentation and/or other materials provided with the 15 | distribution. 16 | * Neither the name of Google Inc. nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | 33 | 34 | The following included software components have additional copyright 35 | notices and license terms that may differ from the above. 36 | 37 | 38 | File fuse.go: 39 | 40 | // Adapted from Plan 9 from User Space's src/cmd/9pfuse/fuse.c, 41 | // which carries this notice: 42 | // 43 | // The files in this directory are subject to the following license. 44 | // 45 | // The author of this software is Russ Cox. 46 | // 47 | // Copyright (c) 2006 Russ Cox 48 | // 49 | // Permission to use, copy, modify, and distribute this software for any 50 | // purpose without fee is hereby granted, provided that this entire notice 51 | // is included in all copies of any software which is or includes a copy 52 | // or modification of this software and in all copies of the supporting 53 | // documentation for such software. 54 | // 55 | // THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED 56 | // WARRANTY. IN PARTICULAR, THE AUTHOR MAKES NO REPRESENTATION OR WARRANTY 57 | // OF ANY KIND CONCERNING THE MERCHANTABILITY OF THIS SOFTWARE OR ITS 58 | // FITNESS FOR ANY PARTICULAR PURPOSE. 59 | 60 | 61 | File fuse_kernel.go: 62 | 63 | // Derived from FUSE's fuse_kernel.h 64 | /* 65 | This file defines the kernel interface of FUSE 66 | Copyright (C) 2001-2007 Miklos Szeredi 67 | 68 | 69 | This -- and only this -- header file may also be distributed under 70 | the terms of the BSD Licence as follows: 71 | 72 | Copyright (C) 2001-2007 Miklos Szeredi. All rights reserved. 73 | 74 | Redistribution and use in source and binary forms, with or without 75 | modification, are permitted provided that the following conditions 76 | are met: 77 | 1. Redistributions of source code must retain the above copyright 78 | notice, this list of conditions and the following disclaimer. 79 | 2. Redistributions in binary form must reproduce the above copyright 80 | notice, this list of conditions and the following disclaimer in the 81 | documentation and/or other materials provided with the distribution. 82 | 83 | THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND 84 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 85 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 86 | ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE 87 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 88 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 89 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 90 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 91 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 92 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 93 | SUCH DAMAGE. 94 | */ 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | github.com/anacrolix/fuse 2 | ========================= 3 | 4 | This module supports implementing FUSE (Filesystems in Userspace) in Go. It supports MacFUSE 3.3+, 4, and FUSE-T, on MacOS, and regular FUSE on Linux and FreeBSD. 5 | 6 | [`github.com/anacrolix/fuse`](https://github.com/anacrolix/fuse) is a fork of [`github.com/zegl/fuse`](https://github.com/zegl/fuse), which is a fork of [`bazil.org/fuse`](https://bazil.org/fuse). 7 | 8 | `bazil.org/fuse` dropped support for FUSE on Mac when OSXFUSE [stopped being an open source project](https://github.com/bazil/fuse/issues/224). 9 | 10 | `github.com/zegl/fuse` added support for MacFUSE 4, and restored support for MacFUSE 3.3 and newer. 11 | 12 | `github.com/anacrolix/fuse` fixes imports and module paths so you can import this module without using Go workspaces or go.mod replace directives. It also adds support for [FUSE-T](https://www.fuse-t.org/), and Mac M1. 13 | -------------------------------------------------------------------------------- /backend.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | type Backend string 9 | 10 | const ( 11 | fuseTBackend = "FUSE-T" 12 | osxfuseBackend = "OSXFUSE" 13 | ) 14 | 15 | func (be Backend) IsFuseT() bool { 16 | return be == fuseTBackend 17 | } 18 | 19 | func (be Backend) IsUnset() bool { 20 | return be == "" 21 | } 22 | 23 | var forcedBackend Backend 24 | 25 | func initForcedBackend() { 26 | forcedBackend = getForcedBackend() 27 | } 28 | 29 | func getForcedBackend() (ret Backend) { 30 | return Backend(strings.ToUpper(strings.TrimSpace(os.Getenv("FUSE_FORCE_BACKEND")))) 31 | } 32 | 33 | // Extra state to be managed per backend. 34 | type backendState interface { 35 | Drop() 36 | } 37 | 38 | // FUSE-T requires we hold on to some extra file descriptors for the duration of the connection. 39 | type fuseTBackendState struct { 40 | extraFiles []*os.File 41 | } 42 | 43 | func (bes fuseTBackendState) Drop() { 44 | for _, f := range bes.extraFiles { 45 | f.Close() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /buffer.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import "unsafe" 4 | 5 | // buffer provides a mechanism for constructing a message from 6 | // multiple segments. 7 | type buffer []byte 8 | 9 | // alloc allocates size bytes and returns a pointer to the new 10 | // segment. 11 | func (w *buffer) alloc(size uintptr) unsafe.Pointer { 12 | s := int(size) 13 | if len(*w)+s > cap(*w) { 14 | old := *w 15 | *w = make([]byte, len(*w), 2*cap(*w)+s) 16 | copy(*w, old) 17 | } 18 | l := len(*w) 19 | *w = (*w)[:l+s] 20 | return unsafe.Pointer(&(*w)[l]) 21 | } 22 | 23 | // reset clears out the contents of the buffer. 24 | func (w *buffer) reset() { 25 | for i := range (*w)[:cap(*w)] { 26 | (*w)[i] = 0 27 | } 28 | *w = (*w)[:0] 29 | } 30 | 31 | func newBuffer(extra uintptr) buffer { 32 | const hdrSize = unsafe.Sizeof(outHeader{}) 33 | buf := make(buffer, hdrSize, hdrSize+extra) 34 | return buf 35 | } 36 | -------------------------------------------------------------------------------- /cmd/fuse-abort/.gitignore: -------------------------------------------------------------------------------- 1 | /fuse-abort 2 | -------------------------------------------------------------------------------- /cmd/fuse-abort/internal/mountinfo/.gitignore: -------------------------------------------------------------------------------- 1 | /mountinfo-fuzz.zip 2 | /testdata/fuzz/crashers/ 3 | /testdata/fuzz/suppressions/ 4 | /testdata/fuzz/corpus/* 5 | !/testdata/fuzz/corpus/*.mountinfo 6 | -------------------------------------------------------------------------------- /cmd/fuse-abort/internal/mountinfo/fuzz: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | go run github.com/dvyukov/go-fuzz/go-fuzz-build 5 | exec go run github.com/dvyukov/go-fuzz/go-fuzz -workdir=testdata/fuzz 6 | -------------------------------------------------------------------------------- /cmd/fuse-abort/internal/mountinfo/fuzz.go: -------------------------------------------------------------------------------- 1 | //go:build gofuzz 2 | // +build gofuzz 3 | 4 | package mountinfo 5 | 6 | func Fuzz(data []byte) int { 7 | if _, err := parse(data); err != nil { 8 | return 0 9 | } 10 | return 1 11 | } 12 | -------------------------------------------------------------------------------- /cmd/fuse-abort/internal/mountinfo/mountinfo.go: -------------------------------------------------------------------------------- 1 | package mountinfo 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strconv" 10 | ) 11 | 12 | const DefaultPath = "/proc/self/mountinfo" 13 | 14 | func Open(p string) (*Reader, error) { 15 | f, err := os.Open(p) 16 | if err != nil { 17 | return nil, err 18 | } 19 | rr := &Reader{ 20 | closer: f, 21 | scanner: bufio.NewScanner(f), 22 | } 23 | return rr, nil 24 | } 25 | 26 | type Reader struct { 27 | closer io.Closer 28 | scanner *bufio.Scanner 29 | } 30 | 31 | var _ io.Closer = (*Reader)(nil) 32 | 33 | func (r *Reader) Close() error { 34 | return r.closer.Close() 35 | } 36 | 37 | // unescape backslash-prefixed octal 38 | func unescape(in []byte) (string, error) { 39 | buf := make([]byte, 0, len(in)) 40 | for len(in) > 0 { 41 | if in[0] == '\\' { 42 | if len(in) < 4 { 43 | return "", fmt.Errorf("truncated octal sequence: %q", in[1:]) 44 | } 45 | octal := string(in[1:4]) 46 | in = in[4:] 47 | num, err := strconv.ParseUint(octal, 8, 8) 48 | if err != nil { 49 | return "", err 50 | } 51 | buf = append(buf, byte(num)) 52 | continue 53 | } 54 | 55 | buf = append(buf, in[0]) 56 | in = in[1:] 57 | } 58 | return string(buf), nil 59 | } 60 | 61 | func (r *Reader) Next() (*Mount, error) { 62 | if !r.scanner.Scan() { 63 | err := r.scanner.Err() 64 | if err != nil { 65 | return nil, err 66 | } 67 | return nil, io.EOF 68 | } 69 | 70 | info, err := parse(r.scanner.Bytes()) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return info, nil 75 | } 76 | 77 | func parse(line []byte) (*Mount, error) { 78 | // https://www.kernel.org/doc/Documentation/filesystems/proc.txt 79 | // 80 | // mountinfo fields are space-separated, with octal escapes 81 | // 82 | // id parentId maj:min path_inside mountpoint mount_options [optional fields...] - fstype /dev/sdblah super_block_options 83 | // 84 | // we care about maj:min, mountpoint, fstype 85 | 86 | fields := bytes.Split(line, []byte{' '}) 87 | const minFields = 7 88 | if len(fields) < minFields { 89 | return nil, fmt.Errorf("cannot parse mountinfo entry: %q", line) 90 | } 91 | 92 | majmin := fields[2] 93 | idx := bytes.IndexByte(majmin, ':') 94 | if idx == -1 { 95 | return nil, fmt.Errorf("cannot parse mountinfo entry, weird major:minor: %q", line) 96 | } 97 | major := string(majmin[:idx]) 98 | minor := string(majmin[idx+1:]) 99 | 100 | mountpoint, err := unescape(fields[4]) 101 | if err != nil { 102 | return nil, fmt.Errorf("cannot parse mountinfo entry, invalid escape in mountpoint: %q", line) 103 | } 104 | 105 | rest := fields[6:] 106 | 107 | // skip optional fields 108 | for { 109 | if len(rest) == 0 { 110 | return nil, fmt.Errorf("cannot parse mountinfo entry, no optional field terminator: %q", line) 111 | } 112 | cur := rest[0] 113 | rest = rest[1:] 114 | if bytes.Equal(cur, []byte{'-'}) { 115 | break 116 | } 117 | } 118 | 119 | if len(rest) == 0 { 120 | return nil, fmt.Errorf("cannot parse mountinfo entry, no fstype: %q", line) 121 | } 122 | fstype, err := unescape(rest[0]) 123 | if err != nil { 124 | return nil, fmt.Errorf("cannot parse mountinfo entry, invalid escape in fstype: %q", line) 125 | } 126 | 127 | i := &Mount{ 128 | Major: major, 129 | Minor: minor, 130 | Mountpoint: mountpoint, 131 | FSType: fstype, 132 | } 133 | return i, nil 134 | } 135 | 136 | type Mount struct { 137 | Major string 138 | Minor string 139 | Mountpoint string 140 | FSType string 141 | } 142 | -------------------------------------------------------------------------------- /cmd/fuse-abort/internal/mountinfo/mountinfo_test.go: -------------------------------------------------------------------------------- 1 | package mountinfo_test 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "testing" 7 | 8 | "github.com/anacrolix/fuse/cmd/fuse-abort/internal/mountinfo" 9 | ) 10 | 11 | func TestOpenError(t *testing.T) { 12 | r, err := mountinfo.Open("testdata/does-not-exist") 13 | if err == nil { 14 | r.Close() 15 | t.Fatal("expected an error") 16 | } 17 | if !os.IsNotExist(err) { 18 | t.Fatalf("expected a not-exists error: %v", err) 19 | } 20 | } 21 | 22 | func TestReal(t *testing.T) { 23 | r, err := mountinfo.Open("testdata/fuzz/corpus/real.mountinfo") 24 | if err != nil { 25 | t.Fatalf("cannot open mountinfo: %v", err) 26 | } 27 | defer func() { 28 | if err := r.Close(); err != nil { 29 | t.Errorf("close: %v", err) 30 | } 31 | }() 32 | 33 | success := false 34 | for { 35 | info, err := r.Next() 36 | if err == io.EOF { 37 | break 38 | } 39 | if err != nil { 40 | t.Fatalf("reading mountinfo: %v", err) 41 | } 42 | if info.Mountpoint != "/home/tv/tmp/fusetest128387706" { 43 | t.Logf("skip %#v\n", info) 44 | continue 45 | } 46 | t.Logf("found %#v\n", info) 47 | success = true 48 | if g, e := info.Major, "0"; g != e { 49 | t.Errorf("wrong major number: %q != %q", g, e) 50 | } 51 | if g, e := info.Minor, "139"; g != e { 52 | t.Errorf("wrong minor number: %q != %q", g, e) 53 | } 54 | if g, e := info.FSType, "fuse"; g != e { 55 | t.Errorf("wrong FS type: %q != %q", g, e) 56 | } 57 | } 58 | if !success { 59 | t.Error("did not see mount") 60 | } 61 | } 62 | 63 | func TestEscape(t *testing.T) { 64 | r, err := mountinfo.Open("testdata/fuzz/corpus/escaped.mountinfo") 65 | if err != nil { 66 | t.Fatalf("cannot open mountinfo: %v", err) 67 | } 68 | defer func() { 69 | if err := r.Close(); err != nil { 70 | t.Errorf("close: %v", err) 71 | } 72 | }() 73 | 74 | info, err := r.Next() 75 | if err != nil { 76 | t.Fatalf("reading mountinfo: %v", err) 77 | } 78 | t.Logf("got %#v\n", info) 79 | if g, e := info.Mountpoint, "/xyz\x53zy"; g != e { 80 | t.Errorf("wrong mountpoint: %q != %q", g, e) 81 | } 82 | if g, e := info.FSType, "foo\x01bar"; g != e { 83 | t.Errorf("wrong FS type: %q != %q", g, e) 84 | } 85 | } 86 | 87 | func TestCrashers(t *testing.T) { 88 | r, err := mountinfo.Open("testdata/fuzz/corpus/crashers.mountinfo") 89 | if err != nil { 90 | t.Fatalf("cannot open mountinfo: %v", err) 91 | } 92 | defer func() { 93 | if err := r.Close(); err != nil { 94 | t.Errorf("close: %v", err) 95 | } 96 | }() 97 | 98 | for { 99 | _, err := r.Next() 100 | if err == io.EOF { 101 | break 102 | } 103 | // we don't care about errors, just that we don't crash 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /cmd/fuse-abort/internal/mountinfo/testdata/fuzz/corpus/crashers.mountinfo: -------------------------------------------------------------------------------- 1 | : - 2 | -------------------------------------------------------------------------------- /cmd/fuse-abort/internal/mountinfo/testdata/fuzz/corpus/escaped.mountinfo: -------------------------------------------------------------------------------- 1 | 268 48 0:139 / /xyz\123zy rw,nosuid,nodev,relatime shared:94 - foo\001bar /dev/fuse rw,user_id=1000,group_id=1000 2 | -------------------------------------------------------------------------------- /cmd/fuse-abort/internal/mountinfo/testdata/fuzz/corpus/real.mountinfo: -------------------------------------------------------------------------------- 1 | 19 24 0:18 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw 2 | 20 24 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw 3 | 21 24 0:6 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=32673056k,nr_inodes=8168264,mode=755 4 | 22 21 0:14 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 5 | 23 24 0:19 / /run rw,nosuid,noexec,relatime shared:5 - tmpfs tmpfs rw,size=6538356k,mode=755 6 | 24 0 0:20 /@ / rw,relatime shared:1 - btrfs /dev/mapper/sda5_crypt rw,ssd,space_cache,metadata_ratio=4,subvolid=256,subvol=/@ 7 | 25 19 0:12 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:8 - securityfs securityfs rw 8 | 26 21 0:23 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw 9 | 27 23 0:24 / /run/lock rw,nosuid,nodev,noexec,relatime shared:6 - tmpfs tmpfs rw,size=5120k 10 | 28 19 0:25 / /sys/fs/cgroup rw shared:9 - tmpfs tmpfs rw,mode=755 11 | 29 28 0:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:10 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd 12 | 30 19 0:27 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:11 - pstore pstore rw 13 | 31 28 0:28 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,blkio 14 | 32 28 0:29 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,net_cls,net_prio 15 | 33 28 0:30 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,cpuset,clone_children 16 | 34 28 0:31 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb 17 | 35 28 0:32 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,freezer 18 | 36 28 0:33 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,memory 19 | 37 28 0:34 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event 20 | 38 28 0:35 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:20 - cgroup cgroup rw,cpu,cpuacct 21 | 39 28 0:36 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,devices 22 | 40 20 0:37 / /proc/sys/fs/binfmt_misc rw,relatime shared:22 - autofs systemd-1 rw,fd=31,pgrp=1,timeout=0,minproto=5,maxproto=5,direct 23 | 41 19 0:7 / /sys/kernel/debug rw,relatime shared:23 - debugfs debugfs rw 24 | 42 21 0:38 / /dev/hugepages rw,relatime shared:24 - hugetlbfs hugetlbfs rw 25 | 43 21 0:17 / /dev/mqueue rw,relatime shared:25 - mqueue mqueue rw 26 | 44 19 0:39 / /sys/fs/fuse/connections rw,relatime shared:26 - fusectl fusectl rw 27 | 50 24 8:1 / /boot rw,relatime shared:27 - ext4 /dev/sda1 rw,data=ordered 28 | 48 24 0:20 /@home /home rw,relatime shared:29 - btrfs /dev/mapper/sda5_crypt rw,ssd,space_cache,metadata_ratio=4,subvolid=258,subvol=/@home 29 | 95 23 0:45 / /run/cgmanager/fs rw,relatime shared:74 - tmpfs cgmfs rw,size=100k,mode=755 30 | 173 40 0:46 / /proc/sys/fs/binfmt_misc rw,relatime shared:76 - binfmt_misc binfmt_misc rw 31 | 243 23 0:51 / /run/user/1000 rw,nosuid,nodev,relatime shared:160 - tmpfs tmpfs rw,size=6538356k,mode=700,uid=1000,gid=1000 32 | 203 243 0:50 / /run/user/1000/gvfs rw,nosuid,nodev,relatime shared:199 - fuse.gvfsd-fuse gvfsd-fuse rw,user_id=1000,group_id=1000 33 | 268 48 0:139 / /home/tv/tmp/fusetest128387706 rw,nosuid,nodev,relatime shared:94 - fuse /dev/fuse rw,user_id=1000,group_id=1000 34 | -------------------------------------------------------------------------------- /cmd/fuse-abort/internal/mountinfo/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | import ( 7 | _ "github.com/dvyukov/go-fuzz/go-fuzz" 8 | _ "github.com/dvyukov/go-fuzz/go-fuzz-build" 9 | ) 10 | -------------------------------------------------------------------------------- /cmd/fuse-abort/main.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | // Forcibly abort a FUSE filesystem mounted at the given path. 4 | // 5 | // This is only supported on Linux. 6 | package main 7 | 8 | import ( 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "io" 13 | "log" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | "syscall" 18 | 19 | "github.com/anacrolix/fuse" 20 | "github.com/anacrolix/fuse/cmd/fuse-abort/internal/mountinfo" 21 | ) 22 | 23 | // When developing a FUSE filesystem, it's pretty common to end up 24 | // with broken mount points, where the FUSE server process is either 25 | // no longer running, or is not responsive. 26 | // 27 | // The usual `fusermount -u` / `umount` commands do things like stat 28 | // the mountpoint, causing filesystem requests. A hung filesystem 29 | // won't answer them. 30 | // 31 | // The way out of this conundrum is to sever the kernel FUSE 32 | // connection. This process is woefully underdocumented, but basically 33 | // we need to find a "connection identifier" and then use `sysfs` to 34 | // tell the FUSE kernelspace to abort the connection. 35 | // 36 | // The special sauce is knowing that the minor number of a device node 37 | // for the mountpoint is this identifier. That and some careful 38 | // parsing of a file listing all the mounts. 39 | // 40 | // https://www.kernel.org/doc/Documentation/filesystems/fuse.txt 41 | // https://sourceforge.net/p/fuse/mailman/message/31426925/ 42 | 43 | // findFUSEMounts returns a mapping of all the known mounts in the 44 | // current namespace. For FUSE mounts, the value will be the 45 | // connection ID. Non-FUSE mounts store an empty string, to 46 | // differentiate error messages. 47 | func findFUSEMounts() (map[string]string, error) { 48 | r := map[string]string{} 49 | 50 | mounts, err := mountinfo.Open(mountinfo.DefaultPath) 51 | if err != nil { 52 | return nil, fmt.Errorf("cannot open mountinfo: %v", err) 53 | } 54 | defer mounts.Close() 55 | for { 56 | info, err := mounts.Next() 57 | if err == io.EOF { 58 | break 59 | } 60 | if err != nil { 61 | return nil, fmt.Errorf("parsing mountinfo: %v", err) 62 | } 63 | 64 | if info.FSType != "fuse" && !strings.HasPrefix(info.FSType, "fuse.") { 65 | r[info.Mountpoint] = "" 66 | continue 67 | } 68 | if info.Major != "0" { 69 | return nil, fmt.Errorf("FUSE mount has weird device major number: %v:%v: %v", info.Major, info.Minor, info.Mountpoint) 70 | } 71 | if _, ok := r[info.Mountpoint]; ok { 72 | return nil, fmt.Errorf("mountpoint seen seen twice in mountinfo: %v", info.Mountpoint) 73 | } 74 | r[info.Mountpoint] = info.Minor 75 | } 76 | return r, nil 77 | } 78 | 79 | func abort(id string) error { 80 | p := filepath.Join("/sys/fs/fuse/connections", id, "abort") 81 | f, err := os.OpenFile(p, os.O_WRONLY, 0600) 82 | if errors.Is(err, os.ErrNotExist) { 83 | // nothing to abort, consider that a success because we might 84 | // have just raced against an unmount 85 | return nil 86 | } 87 | if err != nil { 88 | return err 89 | } 90 | defer f.Close() 91 | if _, err := f.WriteString("1\n"); err != nil { 92 | return err 93 | } 94 | if err := f.Close(); err != nil { 95 | return err 96 | } 97 | f = nil 98 | return nil 99 | } 100 | 101 | func pruneEmptyDir(p string) error { 102 | // we want an rmdir and not a generic delete like 103 | // os.Remove; the node underlying the mountpoint might not 104 | // be a directory, and we really want to only prune 105 | // directories 106 | if err := syscall.Rmdir(p); err != nil { 107 | switch err { 108 | case syscall.ENOTEMPTY, syscall.ENOTDIR: 109 | // underlying node wasn't an empty dir; ignore 110 | case syscall.ENOENT: 111 | // someone else removed it for us; ignore 112 | default: 113 | err = &os.PathError{ 114 | Op: "rmdir", 115 | Path: p, 116 | Err: err, 117 | } 118 | return err 119 | } 120 | } 121 | return nil 122 | } 123 | 124 | var errWarnings = errors.New("encountered warnings") 125 | 126 | func run(prune bool, mountpoints []string) error { 127 | success := true 128 | // make an explicit effort to process mountpoints in command line 129 | // order, even if mountinfo is not in that order 130 | mounts, err := findFUSEMounts() 131 | if err != nil { 132 | return err 133 | } 134 | for _, mountpoint := range mountpoints { 135 | p, err := filepath.Abs(mountpoint) 136 | if err != nil { 137 | log.Printf("cannot make path absolute: %s: %v", mountpoint, err) 138 | success = false 139 | continue 140 | } 141 | id, ok := mounts[p] 142 | if !ok { 143 | log.Printf("mountpoint not found: %v", p) 144 | success = false 145 | continue 146 | } 147 | if id == "" { 148 | log.Printf("not a FUSE mount: %v", p) 149 | success = false 150 | continue 151 | } 152 | if err := abort(id); err != nil { 153 | return fmt.Errorf("cannot abort: %v is connection %v: %v", p, id, err) 154 | } 155 | if err := fuse.Unmount(p); err != nil { 156 | log.Printf("cannot unmount: %v", err) 157 | success = false 158 | continue 159 | } 160 | if prune { 161 | if err := pruneEmptyDir(p); err != nil { 162 | log.Printf("cannot prune mountpoint: %v", err) 163 | success = false 164 | } 165 | } 166 | } 167 | 168 | if !success { 169 | return errWarnings 170 | } 171 | return nil 172 | } 173 | 174 | var prog = filepath.Base(os.Args[0]) 175 | 176 | func usage() { 177 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", prog) 178 | fmt.Fprintf(flag.CommandLine.Output(), " %s MOUNTPOINT..\n", prog) 179 | fmt.Fprintf(flag.CommandLine.Output(), "\n") 180 | fmt.Fprintf(flag.CommandLine.Output(), "Forcibly aborts a FUSE filesystem mounted at the given path.\n") 181 | fmt.Fprintf(flag.CommandLine.Output(), "\n") 182 | flag.PrintDefaults() 183 | } 184 | 185 | func main() { 186 | log.SetFlags(0) 187 | log.SetPrefix(prog + ": ") 188 | 189 | var prune bool 190 | flag.BoolVar(&prune, "p", false, "prune empty mountpoints after unmounting") 191 | 192 | flag.Usage = usage 193 | flag.Parse() 194 | if flag.NArg() == 0 { 195 | flag.Usage() 196 | os.Exit(2) 197 | } 198 | 199 | if err := run(prune, flag.Args()); err != nil { 200 | if err == errWarnings { 201 | // they've already been logged 202 | os.Exit(1) 203 | } 204 | log.Fatal(err) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import ( 4 | "runtime" 5 | ) 6 | 7 | func stack() string { 8 | buf := make([]byte, 1024) 9 | return string(buf[:runtime.Stack(buf, false)]) 10 | } 11 | 12 | func nop(msg interface{}) {} 13 | 14 | // Debug is called to output debug messages, including protocol 15 | // traces. The default behavior is to do nothing. 16 | // 17 | // The messages have human-friendly string representations and are 18 | // safe to marshal to JSON. 19 | // 20 | // Implementations must not retain msg. 21 | var Debug func(msg interface{}) = nop 22 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | /*.seq.svg 2 | 3 | # not ignoring *.seq.png; we want those committed to the repo 4 | # for embedding on Github 5 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # bazil.org/fuse documentation 2 | 3 | See also API docs at http://godoc.org/bazil.org/fuse 4 | 5 | - [The mount sequence](mount-sequence.md) 6 | - [Writing documentation](writing-docs.md) 7 | -------------------------------------------------------------------------------- /doc/mount-linux-error-init.seq: -------------------------------------------------------------------------------- 1 | seqdiag { 2 | app; 3 | fuse [label="bazil.org/fuse"]; 4 | fusermount; 5 | kernel; 6 | mounts; 7 | 8 | app; 9 | fuse [label="bazil.org/fuse"]; 10 | fusermount; 11 | kernel; 12 | mounts; 13 | 14 | app -> fuse [label="Mount"]; 15 | fuse -> fusermount [label="spawn, pass socketpair fd"]; 16 | fusermount -> kernel [label="open /dev/fuse"]; 17 | fusermount -> kernel [label="mount(2)"]; 18 | kernel ->> mounts [label="mount is visible"]; 19 | fusermount <-- kernel [label="mount(2) returns"]; 20 | fuse <<-- fusermount [diagonal, label="exit, receive /dev/fuse fd", leftnote="on Linux, successful exit here\nmeans the mount has happened,\nthough InitRequest might not have yet"]; 21 | app <-- fuse [label="Mount returns\nConn.Ready is already closed"]; 22 | 23 | app -> fuse [label="fs.Serve"]; 24 | fuse => kernel [label="read /dev/fuse fd", note="starts with InitRequest"]; 25 | fuse -> app [label="Init"]; 26 | fuse <-- app [color=red]; 27 | fuse -> kernel [label="write /dev/fuse fd", color=red]; 28 | kernel -> kernel [label="set connection\nstate to error", color=red]; 29 | fuse <-- kernel; 30 | ... conn.MountError == nil, so it is still mounted ... 31 | ... call conn.Close to clean up ... 32 | } 33 | -------------------------------------------------------------------------------- /doc/mount-linux-error-init.seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anacrolix/fuse/aeb550c91d7a625e3fabf57b6a476f6f3dadc18e/doc/mount-linux-error-init.seq.png -------------------------------------------------------------------------------- /doc/mount-linux.seq: -------------------------------------------------------------------------------- 1 | seqdiag { 2 | // seqdiag -T svg -o doc/mount-osx.svg doc/mount-osx.seq 3 | app; 4 | fuse [label="bazil.org/fuse"]; 5 | fusermount; 6 | kernel; 7 | mounts; 8 | 9 | app -> fuse [label="Mount"]; 10 | fuse -> fusermount [label="spawn, pass socketpair fd"]; 11 | fusermount -> kernel [label="open /dev/fuse"]; 12 | fusermount -> kernel [label="mount(2)"]; 13 | kernel ->> mounts [label="mount is visible"]; 14 | fusermount <-- kernel [label="mount(2) returns"]; 15 | fuse <<-- fusermount [diagonal, label="exit, receive /dev/fuse fd", leftnote="on Linux, successful exit here\nmeans the mount has happened,\nthough InitRequest might not have yet"]; 16 | app <-- fuse [label="Mount returns\nConn.Ready is already closed", rightnote="InitRequest and StatfsRequest\nmay or may not be seen\nbefore Conn.Ready,\ndepending on platform"]; 17 | 18 | fuse => kernel [label="read /dev/fuse fd: initRequest"]; 19 | fuse => kernel [label="write /dev/fuse fd: initResponse"]; 20 | 21 | app <-- fuse [label="Mount returns"]; 22 | 23 | app -> fuse [label="fs.Serve"]; 24 | fuse => kernel [label="read /dev/fuse fd", note="starts with InitRequest"]; 25 | fuse => app [label="FS/Node/Handle methods"]; 26 | fuse => kernel [label="write /dev/fuse fd"]; 27 | ... repeat ... 28 | 29 | ... shutting down ... 30 | app -> fuse [label="Unmount"]; 31 | fuse -> fusermount [label="fusermount -u"]; 32 | fusermount -> kernel; 33 | kernel <<-- mounts; 34 | fusermount <-- kernel; 35 | fuse <<-- fusermount [diagonal]; 36 | app <-- fuse [label="Unmount returns"]; 37 | 38 | // actually triggers before above 39 | fuse <<-- kernel [diagonal, label="/dev/fuse EOF"]; 40 | app <-- fuse [label="fs.Serve returns"]; 41 | 42 | app -> fuse [label="conn.Close"]; 43 | fuse -> kernel [label="close /dev/fuse fd"]; 44 | fuse <-- kernel; 45 | app <-- fuse; 46 | } 47 | -------------------------------------------------------------------------------- /doc/mount-linux.seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anacrolix/fuse/aeb550c91d7a625e3fabf57b6a476f6f3dadc18e/doc/mount-linux.seq.png -------------------------------------------------------------------------------- /doc/mount-osx-error-init.seq: -------------------------------------------------------------------------------- 1 | seqdiag { 2 | app; 3 | fuse [label="bazil.org/fuse"]; 4 | wait [label="callMount\nhelper goroutine"]; 5 | mount_osxfusefs; 6 | kernel; 7 | 8 | app -> fuse [label="Mount"]; 9 | fuse -> kernel [label="open /dev/osxfuseN"]; 10 | fuse -> mount_osxfusefs [label="spawn, pass fd"]; 11 | fuse -> wait [label="goroutine", note="blocks on cmd.Wait"]; 12 | app <-- fuse [label="Mount returns"]; 13 | 14 | mount_osxfusefs -> kernel [label="mount(2)"]; 15 | 16 | app -> fuse [label="fs.Serve"]; 17 | fuse => kernel [label="read /dev/osxfuseN fd", note="starts with InitRequest,\nalso seen before mount exits:\ntwo StatfsRequest calls"]; 18 | fuse -> app [label="Init"]; 19 | fuse <-- app [color=red]; 20 | fuse -> kernel [label="write /dev/osxfuseN fd", color=red]; 21 | fuse <-- kernel; 22 | 23 | mount_osxfusefs <-- kernel [label="mount(2) returns", color=red]; 24 | wait <<-- mount_osxfusefs [diagonal, label="exit", color=red]; 25 | app <<-- wait [diagonal, label="mount has failed,\nclose Conn.Ready", color=red]; 26 | 27 | // actually triggers before above 28 | fuse <<-- kernel [diagonal, label="/dev/osxfuseN EOF"]; 29 | app <-- fuse [label="fs.Serve returns"]; 30 | ... conn.MountError != nil, so it was was never mounted ... 31 | ... call conn.Close to clean up ... 32 | } 33 | -------------------------------------------------------------------------------- /doc/mount-osx-error-init.seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anacrolix/fuse/aeb550c91d7a625e3fabf57b6a476f6f3dadc18e/doc/mount-osx-error-init.seq.png -------------------------------------------------------------------------------- /doc/mount-osx.seq: -------------------------------------------------------------------------------- 1 | seqdiag { 2 | // seqdiag -T svg -o doc/mount-osx.svg doc/mount-osx.seq 3 | app; 4 | fuse [label="bazil.org/fuse"]; 5 | wait [label="callMount\nhelper goroutine"]; 6 | mount_osxfusefs; 7 | kernel; 8 | mounts; 9 | 10 | app -> fuse [label="Mount"]; 11 | fuse -> kernel [label="open /dev/osxfuseN"]; 12 | fuse -> mount_osxfusefs [label="spawn, pass fd"]; 13 | fuse -> wait [label="goroutine", note="blocks on cmd.Wait"]; 14 | app <-- fuse [label="Mount returns"]; 15 | 16 | mount_osxfusefs -> kernel [label="mount(2)"]; 17 | 18 | app -> fuse [label="fs.Serve"]; 19 | fuse => kernel [label="read /dev/osxfuseN fd", note="starts with InitRequest,\nalso seen before mount exits:\ntwo StatfsRequest calls"]; 20 | fuse => app [label="FS/Node/Handle methods"]; 21 | fuse => kernel [label="write /dev/osxfuseN fd"]; 22 | ... repeat ... 23 | 24 | kernel ->> mounts [label="mount is visible"]; 25 | mount_osxfusefs <-- kernel [label="mount(2) returns"]; 26 | wait <<-- mount_osxfusefs [diagonal, label="exit", leftnote="on OS X, successful exit\nhere means we finally know\nthe mount has happened\n(can't trust InitRequest,\nkernel might have timed out\nwaiting for InitResponse)"]; 27 | 28 | app <<-- wait [diagonal, label="mount is ready,\nclose Conn.Ready", rightnote="InitRequest and StatfsRequest\nmay or may not be seen\nbefore Conn.Ready,\ndepending on platform"]; 29 | 30 | ... shutting down ... 31 | app -> fuse [label="Unmount"]; 32 | fuse -> kernel [label="umount(2)"]; 33 | kernel <<-- mounts; 34 | fuse <-- kernel; 35 | app <-- fuse [label="Unmount returns"]; 36 | 37 | // actually triggers before above 38 | fuse <<-- kernel [diagonal, label="/dev/osxfuseN EOF"]; 39 | app <-- fuse [label="fs.Serve returns"]; 40 | 41 | app -> fuse [label="conn.Close"]; 42 | fuse -> kernel [label="close /dev/osxfuseN"]; 43 | fuse <-- kernel; 44 | app <-- fuse; 45 | } 46 | -------------------------------------------------------------------------------- /doc/mount-osx.seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anacrolix/fuse/aeb550c91d7a625e3fabf57b6a476f6f3dadc18e/doc/mount-osx.seq.png -------------------------------------------------------------------------------- /doc/mount-sequence.md: -------------------------------------------------------------------------------- 1 | # The mount sequence 2 | 3 | FUSE mounting is a little bit tricky. There's a userspace helper tool 4 | that performs the handshake with the kernel, and then steps out of the 5 | way. This helper behaves differently on different platforms, forcing a 6 | more complex API on us. 7 | 8 | ## Successful runs 9 | 10 | On Linux, the mount is immediate and file system accesses wait until 11 | the requests are served. 12 | 13 | ![Diagram of Linux FUSE mount sequence](mount-linux.seq.png) 14 | 15 | On OS X, the mount becomes visible only after `InitRequest` (and maybe 16 | more) have been served. 17 | 18 | ![Diagram of OSXFUSE mount sequence](mount-osx.seq.png) 19 | 20 | 21 | ## Errors 22 | 23 | Let's see what happens if `InitRequest` gets an error response. On 24 | Linux, the mountpoint is there but all operations will fail: 25 | 26 | ![Diagram of Linux error handling](mount-linux-error-init.seq.png) 27 | 28 | On OS X, the mount never happened: 29 | ======= 30 | Let's see what happens if `initRequest` gets an error response. 31 | The mountpoint is temporarily there but all operations will fail: 32 | 33 | ![Diagram of OS X error handling](mount-osx-error-init.seq.png) 34 | -------------------------------------------------------------------------------- /doc/writing-docs.md: -------------------------------------------------------------------------------- 1 | # Writing documentation 2 | 3 | ## Sequence diagrams 4 | 5 | The sequence diagrams are generated with `seqdiag`: 6 | http://blockdiag.com/en/seqdiag/index.html 7 | 8 | An easy way to work on them is to automatically update the generated 9 | files with https://github.com/cespare/reflex : 10 | 11 | reflex -g 'doc/[^.]*.seq' -- seqdiag -T svg -o '{}.svg' '{}' & 12 | 13 | reflex -g 'doc/[^.]*.seq' -- seqdiag -T png -o '{}.png' '{}' & 14 | 15 | The markdown files refer to PNG images because of Github limitations, 16 | but the SVG is generally more pleasant to view. 17 | -------------------------------------------------------------------------------- /error_darwin.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import ( 4 | "syscall" 5 | ) 6 | 7 | const ( 8 | ENOATTR = Errno(syscall.ENOATTR) 9 | ) 10 | 11 | const ( 12 | errNoXattr = ENOATTR 13 | ) 14 | 15 | func init() { 16 | errnoNames[errNoXattr] = "ENOATTR" 17 | } 18 | -------------------------------------------------------------------------------- /error_freebsd.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import "syscall" 4 | 5 | const ( 6 | ENOATTR = Errno(syscall.ENOATTR) 7 | ) 8 | 9 | const ( 10 | errNoXattr = ENOATTR 11 | ) 12 | 13 | func init() { 14 | errnoNames[errNoXattr] = "ENOATTR" 15 | } 16 | -------------------------------------------------------------------------------- /error_linux.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import ( 4 | "syscall" 5 | ) 6 | 7 | const ( 8 | ENODATA = Errno(syscall.ENODATA) 9 | ) 10 | 11 | const ( 12 | errNoXattr = ENODATA 13 | ) 14 | 15 | func init() { 16 | errnoNames[errNoXattr] = "ENODATA" 17 | } 18 | -------------------------------------------------------------------------------- /error_std.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | // There is very little commonality in extended attribute errors 4 | // across platforms. 5 | // 6 | // getxattr return value for "extended attribute does not exist" is 7 | // ENOATTR on OS X, and ENODATA on Linux and apparently at least 8 | // NetBSD. There may be a #define ENOATTR on Linux too, but the value 9 | // is ENODATA in the actual syscalls. FreeBSD and OpenBSD have no 10 | // ENODATA, only ENOATTR. ENOATTR is not in any of the standards, 11 | // ENODATA exists but is only used for STREAMs. 12 | // 13 | // Each platform will define it a errNoXattr constant, and this file 14 | // will enforce that it implements the right interfaces and hide the 15 | // implementation. 16 | // 17 | // https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/getxattr.2.html 18 | // http://mail-index.netbsd.org/tech-kern/2012/04/30/msg013090.html 19 | // http://mail-index.netbsd.org/tech-kern/2012/04/30/msg013097.html 20 | // http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/errno.h.html 21 | // http://www.freebsd.org/cgi/man.cgi?query=extattr_get_file&sektion=2 22 | // http://nixdoc.net/man-pages/openbsd/man2/extattr_get_file.2.html 23 | 24 | // ErrNoXattr is a platform-independent error value meaning the 25 | // extended attribute was not found. It can be used to respond to 26 | // GetxattrRequest and such. 27 | const ErrNoXattr = errNoXattr 28 | 29 | var _ error = ErrNoXattr 30 | var _ Errno = ErrNoXattr 31 | var _ ErrorNumber = ErrNoXattr 32 | -------------------------------------------------------------------------------- /examples/clockfs/clockfs.go: -------------------------------------------------------------------------------- 1 | // Clockfs implements a file system with the current time in a file. 2 | // It was written to demonstrate kernel cache invalidation. 3 | package main 4 | 5 | import ( 6 | "context" 7 | "flag" 8 | "fmt" 9 | "log" 10 | "os" 11 | "sync/atomic" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/anacrolix/fuse" 16 | "github.com/anacrolix/fuse/fs" 17 | _ "github.com/anacrolix/fuse/fs/fstestutil" 18 | "github.com/anacrolix/fuse/fuseutil" 19 | ) 20 | 21 | func usage() { 22 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 23 | fmt.Fprintf(os.Stderr, " %s MOUNTPOINT\n", os.Args[0]) 24 | flag.PrintDefaults() 25 | } 26 | 27 | func run(mountpoint string) error { 28 | c, err := fuse.Mount( 29 | mountpoint, 30 | fuse.FSName("clock"), 31 | fuse.Subtype("clockfsfs"), 32 | fuse.LocalVolume(), 33 | fuse.VolumeName("Clock filesystem"), 34 | ) 35 | if err != nil { 36 | return err 37 | } 38 | defer c.Close() 39 | 40 | srv := fs.New(c, nil) 41 | filesys := &FS{ 42 | // We pre-create the clock node so that it's always the same 43 | // object returned from all the Lookups. You could carefully 44 | // track its lifetime between Lookup&Forget, and have the 45 | // ticking & invalidation happen only when active, but let's 46 | // keep this example simple. 47 | clockFile: &File{ 48 | fuse: srv, 49 | }, 50 | } 51 | filesys.clockFile.tick() 52 | // This goroutine never exits. That's fine for this example. 53 | go filesys.clockFile.update() 54 | if err := srv.Serve(filesys); err != nil { 55 | return err 56 | } 57 | 58 | // Check if the mount process has an error to report. 59 | <-c.Ready 60 | if err := c.MountError; err != nil { 61 | return err 62 | } 63 | return nil 64 | } 65 | 66 | func main() { 67 | flag.Usage = usage 68 | flag.Parse() 69 | 70 | if flag.NArg() != 1 { 71 | usage() 72 | os.Exit(2) 73 | } 74 | mountpoint := flag.Arg(0) 75 | 76 | if err := run(mountpoint); err != nil { 77 | log.Fatal(err) 78 | } 79 | } 80 | 81 | type FS struct { 82 | clockFile *File 83 | } 84 | 85 | var _ fs.FS = (*FS)(nil) 86 | 87 | func (f *FS) Root() (fs.Node, error) { 88 | return &Dir{fs: f}, nil 89 | } 90 | 91 | // Dir implements both Node and Handle for the root directory. 92 | type Dir struct { 93 | fs *FS 94 | } 95 | 96 | var _ fs.Node = (*Dir)(nil) 97 | 98 | func (d *Dir) Attr(ctx context.Context, a *fuse.Attr) error { 99 | a.Inode = 1 100 | a.Mode = os.ModeDir | 0o555 101 | return nil 102 | } 103 | 104 | var _ fs.NodeStringLookuper = (*Dir)(nil) 105 | 106 | func (d *Dir) Lookup(ctx context.Context, name string) (fs.Node, error) { 107 | if name == "clock" { 108 | return d.fs.clockFile, nil 109 | } 110 | return nil, syscall.ENOENT 111 | } 112 | 113 | var dirDirs = []fuse.Dirent{ 114 | {Inode: 2, Name: "clock", Type: fuse.DT_File}, 115 | } 116 | 117 | var _ fs.HandleReadDirAller = (*Dir)(nil) 118 | 119 | func (d *Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { 120 | return dirDirs, nil 121 | } 122 | 123 | type File struct { 124 | fuse *fs.Server 125 | content atomic.Value 126 | count uint64 127 | } 128 | 129 | var _ fs.Node = (*File)(nil) 130 | 131 | func (f *File) Attr(ctx context.Context, a *fuse.Attr) error { 132 | a.Inode = 2 133 | a.Mode = 0o444 134 | t := f.content.Load().(string) 135 | a.Size = uint64(len(t)) 136 | return nil 137 | } 138 | 139 | var _ fs.NodeOpener = (*File)(nil) 140 | 141 | func (f *File) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) { 142 | if !req.Flags.IsReadOnly() { 143 | return nil, fuse.Errno(syscall.EACCES) 144 | } 145 | resp.Flags |= fuse.OpenKeepCache 146 | return f, nil 147 | } 148 | 149 | var _ fs.Handle = (*File)(nil) 150 | 151 | var _ fs.HandleReader = (*File)(nil) 152 | 153 | func (f *File) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { 154 | t := f.content.Load().(string) 155 | fuseutil.HandleRead(req, resp, []byte(t)) 156 | return nil 157 | } 158 | 159 | func (f *File) tick() { 160 | // Intentionally a variable-length format, to demonstrate size changes. 161 | f.count++ 162 | s := fmt.Sprintf("%d\t%s\n", f.count, time.Now()) 163 | f.content.Store(s) 164 | 165 | // For simplicity, this example tries to send invalidate 166 | // notifications even when the kernel does not hold a reference to 167 | // the node, so be extra sure to ignore ErrNotCached. 168 | if err := f.fuse.InvalidateNodeData(f); err != nil && err != fuse.ErrNotCached { 169 | log.Printf("invalidate error: %v", err) 170 | } 171 | } 172 | 173 | func (f *File) update() { 174 | tick := time.NewTicker(1 * time.Second) 175 | defer tick.Stop() 176 | for range tick.C { 177 | f.tick() 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /examples/hellofs/hello.go: -------------------------------------------------------------------------------- 1 | // Hellofs implements a simple "hello world" file system. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "syscall" 11 | 12 | "github.com/anacrolix/fuse" 13 | "github.com/anacrolix/fuse/fs" 14 | _ "github.com/anacrolix/fuse/fs/fstestutil" 15 | ) 16 | 17 | func usage() { 18 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 19 | fmt.Fprintf(os.Stderr, " %s MOUNTPOINT\n", os.Args[0]) 20 | flag.PrintDefaults() 21 | } 22 | 23 | func main() { 24 | flag.Usage = usage 25 | flag.Parse() 26 | 27 | if flag.NArg() != 1 { 28 | usage() 29 | os.Exit(2) 30 | } 31 | mountpoint := flag.Arg(0) 32 | 33 | c, err := fuse.Mount( 34 | mountpoint, 35 | fuse.FSName("helloworld"), 36 | fuse.Subtype("hellofs"), 37 | fuse.LocalVolume(), 38 | fuse.VolumeName("Hello world!"), 39 | ) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | defer c.Close() 44 | 45 | err = fs.Serve(c, FS{}) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | // check if the mount process has an error to report 51 | <-c.Ready 52 | if err := c.MountError; err != nil { 53 | log.Fatal(err) 54 | } 55 | } 56 | 57 | // FS implements the hello world file system. 58 | type FS struct{} 59 | 60 | func (FS) Root() (fs.Node, error) { 61 | return Dir{}, nil 62 | } 63 | 64 | // Dir implements both Node and Handle for the root directory. 65 | type Dir struct{} 66 | 67 | func (Dir) Attr(ctx context.Context, a *fuse.Attr) error { 68 | a.Inode = 1 69 | a.Mode = os.ModeDir | 0o555 70 | return nil 71 | } 72 | 73 | func (Dir) Lookup(ctx context.Context, name string) (fs.Node, error) { 74 | if name == "hello" { 75 | return File{}, nil 76 | } 77 | return nil, syscall.ENOENT 78 | } 79 | 80 | var dirDirs = []fuse.Dirent{ 81 | {Inode: 2, Name: "hello", Type: fuse.DT_File}, 82 | } 83 | 84 | func (Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { 85 | return dirDirs, nil 86 | } 87 | 88 | // File implements both Node and Handle for the hello file. 89 | type File struct{} 90 | 91 | const greeting = "hello, world\n" 92 | 93 | func (File) Attr(ctx context.Context, a *fuse.Attr) error { 94 | a.Inode = 2 95 | a.Mode = 0o444 96 | a.Size = uint64(len(greeting)) 97 | return nil 98 | } 99 | 100 | func (File) ReadAll(ctx context.Context) ([]byte, error) { 101 | return []byte(greeting), nil 102 | } 103 | -------------------------------------------------------------------------------- /examples/loopback/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Keybase 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /examples/loopback/README.md: -------------------------------------------------------------------------------- 1 | # loopback 2 | A loopback filesystem using bazil.org/fuse 3 | -------------------------------------------------------------------------------- /examples/loopback/darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin 6 | // +build darwin 7 | 8 | package main 9 | 10 | import ( 11 | "context" 12 | "log" 13 | "os" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/anacrolix/fuse" 18 | ) 19 | 20 | func fillAttrWithFileInfo(a *fuse.Attr, fi os.FileInfo) { 21 | s := fi.Sys().(*syscall.Stat_t) 22 | a.Valid = attrValidDuration 23 | a.Inode = s.Ino 24 | a.Size = uint64(s.Size) 25 | a.Blocks = uint64(s.Blocks) 26 | a.Atime = time.Unix(s.Atimespec.Unix()) 27 | a.Mtime = time.Unix(s.Mtimespec.Unix()) 28 | a.Ctime = time.Unix(s.Ctimespec.Unix()) 29 | a.Crtime = time.Unix(s.Birthtimespec.Unix()) 30 | a.Mode = fi.Mode() 31 | a.Nlink = uint32(s.Nlink) 32 | a.Uid = s.Uid 33 | a.Gid = s.Gid 34 | a.Flags = s.Flags 35 | a.BlockSize = uint32(s.Blksize) 36 | } 37 | 38 | func (n *Node) setattrPlatformSpecific(ctx context.Context, 39 | req *fuse.SetattrRequest, resp *fuse.SetattrResponse) (err error) { 40 | if req.Valid.Flags() { 41 | log.Printf("Flags: %x", req.Flags) 42 | if err = syscall.Chflags(n.realPath, int(req.Flags)); err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /examples/loopback/darwin_386.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin && 386 6 | // +build darwin,386 7 | 8 | package main 9 | 10 | import ( 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | func tToTv(t time.Time) (tv syscall.Timeval) { 16 | tv.Sec = int32(t.Unix()) 17 | tv.Usec = int32(t.UnixNano() % time.Second.Nanoseconds() / 18 | time.Microsecond.Nanoseconds()) 19 | return tv 20 | } 21 | -------------------------------------------------------------------------------- /examples/loopback/darwin_else.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin && !386 6 | // +build darwin,!386 7 | 8 | package main 9 | 10 | import ( 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | func tToTv(t time.Time) (tv syscall.Timeval) { 16 | tv.Sec = int64(t.Unix()) 17 | tv.Usec = int32(t.UnixNano() % time.Second.Nanoseconds() / 18 | time.Microsecond.Nanoseconds()) 19 | return tv 20 | } 21 | -------------------------------------------------------------------------------- /examples/loopback/linux-64bit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build linux && !386 6 | // +build linux,!386 7 | 8 | package main 9 | 10 | import ( 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | func tToTv(t time.Time) (tv syscall.Timeval) { 16 | tv.Sec = int64(t.Unix()) 17 | tv.Usec = int64(t.UnixNano() % time.Second.Nanoseconds() / 18 | time.Microsecond.Nanoseconds()) 19 | return tv 20 | } 21 | -------------------------------------------------------------------------------- /examples/loopback/linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build linux 6 | // +build linux 7 | 8 | package main 9 | 10 | import ( 11 | "context" 12 | "os" 13 | "syscall" 14 | "time" 15 | 16 | "github.com/anacrolix/fuse" 17 | ) 18 | 19 | func fillAttrWithFileInfo(a *fuse.Attr, fi os.FileInfo) { 20 | s := fi.Sys().(*syscall.Stat_t) 21 | a.Valid = attrValidDuration 22 | a.Inode = s.Ino 23 | a.Size = uint64(s.Size) 24 | a.Blocks = uint64(s.Blocks) 25 | a.Atime = time.Unix(s.Atim.Unix()) 26 | a.Mtime = time.Unix(s.Mtim.Unix()) 27 | a.Ctime = time.Unix(s.Ctim.Unix()) 28 | a.Mode = fi.Mode() 29 | a.Nlink = uint32(s.Nlink) 30 | a.Uid = s.Uid 31 | a.Gid = s.Gid 32 | a.BlockSize = uint32(s.Blksize) 33 | } 34 | 35 | func (n *Node) setattrPlatformSpecific(ctx context.Context, 36 | req *fuse.SetattrRequest, resp *fuse.SetattrResponse) (err error) { 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /examples/loopback/linux_386.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build linux && 386 6 | // +build linux,386 7 | 8 | package main 9 | 10 | import ( 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | func tToTv(t time.Time) (tv syscall.Timeval) { 16 | tv.Sec = int32(t.Unix()) 17 | tv.Usec = int32(t.UnixNano() % time.Second.Nanoseconds() / 18 | time.Microsecond.Nanoseconds()) 19 | return tv 20 | } 21 | -------------------------------------------------------------------------------- /examples/loopback/loog/log.go: -------------------------------------------------------------------------------- 1 | package loog 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/anacrolix/fuse" 9 | ) 10 | 11 | var sink io.WriteCloser 12 | 13 | func SetSink(file string) { 14 | f, err := os.Create(file) 15 | if err != nil { 16 | panic(err) 17 | } 18 | sink = f 19 | } 20 | 21 | func LogAttr(p string, a *fuse.Attr, err error) { 22 | fmt.Fprintf(sink, "Attr valid=%s Inode=%x size=%d blocks=%d mode=%o nlink=%d uid=%d gid=%d rdev=%x flags=%o blocksize=%d error=%v path=%s\n", 23 | a.Valid, 24 | a.Inode, 25 | a.Size, 26 | a.Blocks, 27 | // a.Atime 28 | // a.Mtime 29 | // a.Ctime 30 | // a.Crtime 31 | a.Mode, 32 | a.Nlink, 33 | a.Uid, 34 | a.Gid, 35 | a.Rdev, 36 | a.Flags, 37 | a.BlockSize, 38 | err, p) 39 | } 40 | -------------------------------------------------------------------------------- /examples/loopback/loopback.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build linux || darwin 6 | // +build linux darwin 7 | 8 | package main 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "io/ioutil" 14 | "log" 15 | "os" 16 | "path/filepath" 17 | "sort" 18 | "sync" 19 | "syscall" 20 | "time" 21 | 22 | "github.com/anacrolix/fuse" 23 | "github.com/anacrolix/fuse/fs" 24 | ) 25 | 26 | const ( 27 | attrValidDuration = time.Second 28 | ) 29 | 30 | func translateError(err error) error { 31 | switch { 32 | case os.IsNotExist(err): 33 | return fuse.ENOENT 34 | case os.IsExist(err): 35 | return fuse.EEXIST 36 | case os.IsPermission(err): 37 | return fuse.EPERM 38 | default: 39 | return err 40 | } 41 | } 42 | 43 | // FS is the filesystem root 44 | type FS struct { 45 | rootPath string 46 | 47 | xlock sync.RWMutex 48 | xattrs map[string]map[string][]byte 49 | 50 | nlock sync.Mutex 51 | nodes map[string][]*Node // realPath -> nodes 52 | } 53 | 54 | func newFS(rootPath string) *FS { 55 | return &FS{ 56 | rootPath: rootPath, 57 | xattrs: make(map[string]map[string][]byte), 58 | nodes: make(map[string][]*Node), 59 | } 60 | } 61 | 62 | func (f *FS) newNode(n *Node) { 63 | rp := n.getRealPath() 64 | 65 | f.nlock.Lock() 66 | defer f.nlock.Unlock() 67 | f.nodes[rp] = append(f.nodes[rp], n) 68 | } 69 | 70 | func (f *FS) nodeRenamed(oldPath string, newPath string) { 71 | f.nlock.Lock() 72 | defer f.nlock.Unlock() 73 | f.nodes[newPath] = append(f.nodes[newPath], f.nodes[oldPath]...) 74 | delete(f.nodes, oldPath) 75 | for _, n := range f.nodes[newPath] { 76 | n.updateRealPath(newPath) 77 | } 78 | } 79 | 80 | func (f *FS) forgetNode(n *Node) { 81 | f.nlock.Lock() 82 | defer f.nlock.Unlock() 83 | nodes, ok := f.nodes[n.realPath] 84 | if !ok { 85 | return 86 | } 87 | 88 | found := -1 89 | for i, node := range nodes { 90 | if node == n { 91 | found = i 92 | break 93 | } 94 | } 95 | 96 | if found > -1 { 97 | nodes = append(nodes[:found], nodes[found+1:]...) 98 | } 99 | if len(nodes) == 0 { 100 | delete(f.nodes, n.realPath) 101 | } else { 102 | f.nodes[n.realPath] = nodes 103 | } 104 | } 105 | 106 | // Root implements fs.FS interface for *FS 107 | func (f *FS) Root() (n fs.Node, err error) { 108 | time.Sleep(latency) 109 | defer func() { log.Printf("FS.Root(): %#+v error=%v", n, err) }() 110 | nn := &Node{realPath: f.rootPath, isDir: true, fs: f} 111 | f.newNode(nn) 112 | return nn, nil 113 | } 114 | 115 | var _ fs.FSStatfser = (*FS)(nil) 116 | 117 | // Statfs implements fs.FSStatfser interface for *FS 118 | func (f *FS) Statfs(ctx context.Context, 119 | req *fuse.StatfsRequest, resp *fuse.StatfsResponse) (err error) { 120 | time.Sleep(latency) 121 | defer func() { log.Printf("FS.Statfs(): error=%v", err) }() 122 | var stat syscall.Statfs_t 123 | if err := syscall.Statfs(f.rootPath, &stat); err != nil { 124 | return translateError(err) 125 | } 126 | resp.Blocks = stat.Blocks 127 | resp.Bfree = stat.Bfree 128 | resp.Bavail = stat.Bavail 129 | resp.Files = 0 // TODO 130 | resp.Ffree = stat.Ffree 131 | resp.Bsize = uint32(stat.Bsize) 132 | resp.Namelen = 255 // TODO 133 | resp.Frsize = 8 // TODO 134 | 135 | return nil 136 | } 137 | 138 | // if to is empty, all xattrs on the node is removed 139 | func (f *FS) moveAllxattrs(ctx context.Context, from string, to string) { 140 | f.xlock.Lock() 141 | defer f.xlock.Unlock() 142 | if f.xattrs[from] != nil { 143 | if to != "" { 144 | f.xattrs[to] = f.xattrs[from] 145 | } 146 | f.xattrs[from] = nil 147 | } 148 | } 149 | 150 | // Handle represent an open file or directory 151 | type Handle struct { 152 | fs *FS 153 | reopener func() (*os.File, error) 154 | forgetter func() 155 | 156 | f *os.File 157 | } 158 | 159 | var _ fs.HandleFlusher = (*Handle)(nil) 160 | 161 | // Flush implements fs.HandleFlusher interface for *Handle 162 | func (h *Handle) Flush(ctx context.Context, 163 | req *fuse.FlushRequest) (err error) { 164 | time.Sleep(latency) 165 | defer func() { log.Printf("Handle(%s).Flush(): error=%v", h.f.Name(), err) }() 166 | return h.f.Sync() 167 | } 168 | 169 | var _ fs.HandleReadAller = (*Handle)(nil) 170 | 171 | // ReadAll implements fs.HandleReadAller interface for *Handle 172 | func (h *Handle) ReadAll(ctx context.Context) (d []byte, err error) { 173 | time.Sleep(latency) 174 | defer func() { 175 | log.Printf("Handle(%s).ReadAll(): error=%v", 176 | h.f.Name(), err) 177 | }() 178 | return ioutil.ReadAll(h.f) 179 | } 180 | 181 | var _ fs.HandleReadDirAller = (*Handle)(nil) 182 | 183 | // ReadDirAll implements fs.HandleReadDirAller interface for *Handle 184 | func (h *Handle) ReadDirAll(ctx context.Context) ( 185 | dirs []fuse.Dirent, err error) { 186 | time.Sleep(latency) 187 | defer func() { 188 | log.Printf("Handle(%s).ReadDirAll(): %#+v error=%v", 189 | h.f.Name(), dirs, err) 190 | }() 191 | fis, err := h.f.Readdir(0) 192 | if err != nil { 193 | fmt.Printf("ReadDir error: %v", err) 194 | return nil, translateError(err) 195 | } 196 | log.Printf("dirent: %v", fis) 197 | 198 | // Readdir() reads up the entire dir stream but never resets the pointer. 199 | // Consequently, when Readdir is called again on the same *File, it gets 200 | // nothing. As a result, we need to close the file descriptor and re-open it 201 | // so next call would work. 202 | if err = h.f.Close(); err != nil { 203 | return nil, translateError(err) 204 | } 205 | if h.f, err = h.reopener(); err != nil { 206 | return nil, translateError(err) 207 | } 208 | 209 | return getDirentsWithFileInfos(fis), nil 210 | } 211 | 212 | var _ fs.HandleReader = (*Handle)(nil) 213 | 214 | // Read implements fs.HandleReader interface for *Handle 215 | func (h *Handle) Read(ctx context.Context, 216 | req *fuse.ReadRequest, resp *fuse.ReadResponse) (err error) { 217 | time.Sleep(latency) 218 | defer func() { 219 | log.Printf("Handle(%s).Read(): error=%v", 220 | h.f.Name(), err) 221 | }() 222 | 223 | if _, err = h.f.Seek(req.Offset, 0); err != nil { 224 | return translateError(err) 225 | } 226 | resp.Data = make([]byte, req.Size) 227 | n, err := h.f.Read(resp.Data) 228 | resp.Data = resp.Data[:n] 229 | return translateError(err) 230 | } 231 | 232 | var _ fs.HandleReleaser = (*Handle)(nil) 233 | 234 | // Release implements fs.HandleReleaser interface for *Handle 235 | func (h *Handle) Release(ctx context.Context, 236 | req *fuse.ReleaseRequest) (err error) { 237 | time.Sleep(latency) 238 | defer func() { 239 | log.Printf("Handle(%s).Release(): error=%v", 240 | h.f.Name(), err) 241 | }() 242 | if h.forgetter != nil { 243 | h.forgetter() 244 | } 245 | return h.f.Close() 246 | } 247 | 248 | var _ fs.HandleWriter = (*Handle)(nil) 249 | 250 | // Write implements fs.HandleWriter interface for *Handle 251 | func (h *Handle) Write(ctx context.Context, 252 | req *fuse.WriteRequest, resp *fuse.WriteResponse) (err error) { 253 | time.Sleep(latency) 254 | defer func() { 255 | log.Printf("Handle(%s).Write(): error=%v", 256 | h.f.Name(), err) 257 | }() 258 | 259 | if _, err = h.f.Seek(req.Offset, 0); err != nil { 260 | return translateError(err) 261 | } 262 | n, err := h.f.Write(req.Data) 263 | resp.Size = n 264 | return translateError(err) 265 | } 266 | 267 | // Node is the node for both directories and files 268 | type Node struct { 269 | fs *FS 270 | 271 | rpLock sync.RWMutex 272 | realPath string 273 | 274 | isDir bool 275 | 276 | lock sync.RWMutex 277 | flushers map[*Handle]bool 278 | } 279 | 280 | func (n *Node) getRealPath() string { 281 | n.rpLock.RLock() 282 | defer n.rpLock.RUnlock() 283 | return n.realPath 284 | } 285 | 286 | func (n *Node) updateRealPath(realPath string) { 287 | n.rpLock.Lock() 288 | defer n.rpLock.Unlock() 289 | n.realPath = realPath 290 | } 291 | 292 | var _ fs.NodeAccesser = (*Node)(nil) 293 | 294 | // Access implements fs.NodeAccesser interface for *Node 295 | func (n *Node) Access(ctx context.Context, a *fuse.AccessRequest) (err error) { 296 | time.Sleep(latency) 297 | defer func() { 298 | log.Printf("%s.Access(%o): error=%v", n.getRealPath(), a.Mask, err) 299 | }() 300 | fi, err := os.Stat(n.getRealPath()) 301 | if err != nil { 302 | return translateError(err) 303 | } 304 | if a.Mask&uint32(fi.Mode()>>6) != a.Mask { 305 | return fuse.EPERM 306 | } 307 | return nil 308 | } 309 | 310 | // Attr implements fs.Node interface for *Dir 311 | func (n *Node) Attr(ctx context.Context, a *fuse.Attr) (err error) { 312 | time.Sleep(latency) 313 | defer func() { log.Printf("%s.Attr(): %#+v error=%v", n.getRealPath(), a, err) }() 314 | fi, err := os.Stat(n.getRealPath()) 315 | if err != nil { 316 | return translateError(err) 317 | } 318 | 319 | fillAttrWithFileInfo(a, fi) 320 | 321 | return nil 322 | } 323 | 324 | // Lookup implements fs.NodeRequestLookuper interface for *Node 325 | func (n *Node) Lookup(ctx context.Context, 326 | name string) (ret fs.Node, err error) { 327 | time.Sleep(latency) 328 | defer func() { 329 | log.Printf("%s.Lookup(%s): %#+v error=%v", 330 | n.getRealPath(), name, ret, err) 331 | }() 332 | 333 | if !n.isDir { 334 | return nil, fuse.ENOTSUP 335 | } 336 | 337 | p := filepath.Join(n.getRealPath(), name) 338 | fi, err := os.Stat(p) 339 | 340 | err = translateError(err) 341 | if err != nil { 342 | return nil, translateError(err) 343 | } 344 | 345 | var nn *Node 346 | if fi.IsDir() { 347 | nn = &Node{realPath: p, isDir: true, fs: n.fs} 348 | } else { 349 | nn = &Node{realPath: p, isDir: false, fs: n.fs} 350 | } 351 | 352 | n.fs.newNode(nn) 353 | return nn, nil 354 | } 355 | 356 | func getDirentsWithFileInfos(fis []os.FileInfo) (dirs []fuse.Dirent) { 357 | for _, fi := range fis { 358 | stat := fi.Sys().(*syscall.Stat_t) 359 | var tp fuse.DirentType 360 | 361 | switch { 362 | case fi.IsDir(): 363 | tp = fuse.DT_Dir 364 | case fi.Mode().IsRegular(): 365 | tp = fuse.DT_File 366 | default: 367 | fmt.Printf("type %v\n", fi.Mode()) 368 | //panic("unsupported dirent type") 369 | continue 370 | } 371 | 372 | dirs = append(dirs, fuse.Dirent{ 373 | Inode: stat.Ino, 374 | Name: fi.Name(), 375 | Type: tp, 376 | }) 377 | } 378 | 379 | return dirs 380 | } 381 | 382 | func fuseOpenFlagsToOSFlagsAndPerms( 383 | f fuse.OpenFlags) (flag int, perm os.FileMode) { 384 | flag = int(f & fuse.OpenAccessModeMask) 385 | if f&fuse.OpenAppend != 0 { 386 | perm |= os.ModeAppend 387 | } 388 | if f&fuse.OpenCreate != 0 { 389 | flag |= os.O_CREATE 390 | } 391 | if f&fuse.OpenDirectory != 0 { 392 | perm |= os.ModeDir 393 | } 394 | if f&fuse.OpenExclusive != 0 { 395 | perm |= os.ModeExclusive 396 | } 397 | if f&fuse.OpenNonblock != 0 { 398 | log.Printf("fuse.OpenNonblock is set in OpenFlags but ignored") 399 | } 400 | if f&fuse.OpenSync != 0 { 401 | flag |= os.O_SYNC 402 | } 403 | if f&fuse.OpenTruncate != 0 { 404 | flag |= os.O_TRUNC 405 | } 406 | 407 | return flag, perm 408 | } 409 | 410 | func (n *Node) rememberHandle(h *Handle) { 411 | n.lock.Lock() 412 | defer n.lock.Unlock() 413 | if n.flushers == nil { 414 | n.flushers = make(map[*Handle]bool) 415 | } 416 | n.flushers[h] = true 417 | } 418 | 419 | func (n *Node) forgetHandle(h *Handle) { 420 | n.lock.Lock() 421 | defer n.lock.Unlock() 422 | if n.flushers == nil { 423 | return 424 | } 425 | delete(n.flushers, h) 426 | } 427 | 428 | var _ fs.NodeOpener = (*Node)(nil) 429 | 430 | // Open implements fs.NodeOpener interface for *Node 431 | func (n *Node) Open(ctx context.Context, 432 | req *fuse.OpenRequest, resp *fuse.OpenResponse) (h fs.Handle, err error) { 433 | time.Sleep(latency) 434 | flags, perm := fuseOpenFlagsToOSFlagsAndPerms(req.Flags) 435 | defer func() { 436 | log.Printf("%s.Open(): %o %o error=%v", 437 | n.getRealPath(), flags, perm, err) 438 | }() 439 | 440 | opener := func() (*os.File, error) { 441 | return os.OpenFile(n.getRealPath(), flags, perm) 442 | } 443 | 444 | f, err := opener() 445 | if err != nil { 446 | return nil, translateError(err) 447 | } 448 | 449 | handle := &Handle{fs: n.fs, f: f, reopener: opener} 450 | n.rememberHandle(handle) 451 | handle.forgetter = func() { 452 | n.forgetHandle(handle) 453 | } 454 | return handle, nil 455 | } 456 | 457 | var _ fs.NodeCreater = (*Node)(nil) 458 | 459 | // Create implements fs.NodeCreater interface for *Node 460 | func (n *Node) Create( 461 | ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) ( 462 | fsn fs.Node, fsh fs.Handle, err error) { 463 | time.Sleep(latency) 464 | flags, _ := fuseOpenFlagsToOSFlagsAndPerms(req.Flags) 465 | name := filepath.Join(n.getRealPath(), req.Name) 466 | defer func() { 467 | log.Printf("%s.Create(%s): %o %o error=%v", 468 | n.getRealPath(), name, flags, req.Mode, err) 469 | }() 470 | 471 | opener := func() (f *os.File, err error) { 472 | return os.OpenFile(name, flags, req.Mode) 473 | } 474 | 475 | f, err := opener() 476 | if err != nil { 477 | return nil, nil, translateError(err) 478 | } 479 | 480 | h := &Handle{fs: n.fs, f: f, reopener: opener} 481 | 482 | node := &Node{ 483 | realPath: filepath.Join(n.getRealPath(), req.Name), 484 | isDir: req.Mode.IsDir(), 485 | fs: n.fs, 486 | } 487 | node.rememberHandle(h) 488 | h.forgetter = func() { 489 | node.forgetHandle(h) 490 | } 491 | n.fs.newNode(node) 492 | return node, h, nil 493 | } 494 | 495 | var _ fs.NodeMkdirer = (*Node)(nil) 496 | 497 | // Mkdir implements fs.NodeMkdirer interface for *Node 498 | func (n *Node) Mkdir(ctx context.Context, 499 | req *fuse.MkdirRequest) (created fs.Node, err error) { 500 | time.Sleep(latency) 501 | defer func() { log.Printf("%s.Mkdir(%s): error=%v", n.getRealPath(), req.Name, err) }() 502 | name := filepath.Join(n.getRealPath(), req.Name) 503 | if err = os.Mkdir(name, req.Mode); err != nil { 504 | return nil, translateError(err) 505 | } 506 | nn := &Node{realPath: name, isDir: true, fs: n.fs} 507 | n.fs.newNode(nn) 508 | return nn, nil 509 | } 510 | 511 | var _ fs.NodeRemover = (*Node)(nil) 512 | 513 | // Remove implements fs.NodeRemover interface for *Node 514 | func (n *Node) Remove(ctx context.Context, req *fuse.RemoveRequest) (err error) { 515 | time.Sleep(latency) 516 | name := filepath.Join(n.getRealPath(), req.Name) 517 | defer func() { log.Printf("%s.Remove(%s): error=%v", n.getRealPath(), name, err) }() 518 | defer func() { 519 | if err == nil { 520 | n.fs.moveAllxattrs(ctx, name, "") 521 | } 522 | }() 523 | return os.Remove(name) 524 | } 525 | 526 | var _ fs.NodeFsyncer = (*Node)(nil) 527 | 528 | // Fsync implements fs.NodeFsyncer interface for *Node 529 | func (n *Node) Fsync(ctx context.Context, req *fuse.FsyncRequest) (err error) { 530 | time.Sleep(latency) 531 | defer func() { log.Printf("%s.Fsync(): error=%v", n.getRealPath(), err) }() 532 | n.lock.RLock() 533 | defer n.lock.RUnlock() 534 | for h := range n.flushers { 535 | return h.f.Sync() 536 | } 537 | return fuse.EIO 538 | } 539 | 540 | var _ fs.NodeSetattrer = (*Node)(nil) 541 | 542 | // Setattr implements fs.NodeSetattrer interface for *Node 543 | func (n *Node) Setattr(ctx context.Context, 544 | req *fuse.SetattrRequest, resp *fuse.SetattrResponse) (err error) { 545 | time.Sleep(latency) 546 | defer func() { 547 | log.Printf("%s.Setattr(valid=%x): error=%v", n.getRealPath(), req.Valid, err) 548 | }() 549 | if req.Valid.Size() { 550 | if err = syscall.Truncate(n.getRealPath(), int64(req.Size)); err != nil { 551 | return translateError(err) 552 | } 553 | } 554 | 555 | if req.Valid.Mtime() { 556 | var tvs [2]syscall.Timeval 557 | if !req.Valid.Atime() { 558 | tvs[0] = tToTv(time.Now()) 559 | } else { 560 | tvs[0] = tToTv(req.Atime) 561 | } 562 | tvs[1] = tToTv(req.Mtime) 563 | } 564 | 565 | if req.Valid.Handle() { 566 | log.Printf("%s.Setattr(): unhandled request: req.Valid.Handle() == true", 567 | n.getRealPath()) 568 | } 569 | 570 | if req.Valid.Mode() { 571 | if err = os.Chmod(n.getRealPath(), req.Mode); err != nil { 572 | return translateError(err) 573 | } 574 | } 575 | 576 | if req.Valid.Uid() || req.Valid.Gid() { 577 | if req.Valid.Uid() && req.Valid.Gid() { 578 | if err = os.Chown(n.getRealPath(), int(req.Uid), int(req.Gid)); err != nil { 579 | return translateError(err) 580 | } 581 | } 582 | fi, err := os.Stat(n.getRealPath()) 583 | if err != nil { 584 | return translateError(err) 585 | } 586 | s := fi.Sys().(*syscall.Stat_t) 587 | if req.Valid.Uid() { 588 | if err = os.Chown(n.getRealPath(), int(req.Uid), int(s.Gid)); err != nil { 589 | return translateError(err) 590 | } 591 | } else { 592 | if err = os.Chown(n.getRealPath(), int(s.Uid), int(req.Gid)); err != nil { 593 | return translateError(err) 594 | } 595 | } 596 | } 597 | 598 | if err = n.setattrPlatformSpecific(ctx, req, resp); err != nil { 599 | return translateError(err) 600 | } 601 | 602 | fi, err := os.Stat(n.getRealPath()) 603 | if err != nil { 604 | return translateError(err) 605 | } 606 | 607 | fillAttrWithFileInfo(&resp.Attr, fi) 608 | 609 | return nil 610 | } 611 | 612 | var _ fs.NodeRenamer = (*Node)(nil) 613 | 614 | // Rename implements fs.NodeRenamer interface for *Node 615 | func (n *Node) Rename(ctx context.Context, 616 | req *fuse.RenameRequest, newDir fs.Node) (err error) { 617 | time.Sleep(latency) 618 | np := filepath.Join(newDir.(*Node).getRealPath(), req.NewName) 619 | op := filepath.Join(n.getRealPath(), req.OldName) 620 | defer func() { 621 | log.Printf("%s.Rename(%s->%s): error=%v", 622 | n.getRealPath(), op, np, err) 623 | }() 624 | defer func() { 625 | if err == nil { 626 | n.fs.moveAllxattrs(ctx, op, np) 627 | n.fs.nodeRenamed(op, np) 628 | } 629 | }() 630 | return os.Rename(op, np) 631 | } 632 | 633 | var _ fs.NodeGetxattrer = (*Node)(nil) 634 | 635 | // Getxattr implements fs.Getxattrer interface for *Node 636 | func (n *Node) Getxattr(ctx context.Context, 637 | req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) (err error) { 638 | time.Sleep(latency) 639 | if !inMemoryXattr { 640 | return fuse.ENOTSUP 641 | } 642 | 643 | defer func() { 644 | log.Printf("%s.Getxattr(%s): error=%v", n.getRealPath(), req.Name, err) 645 | }() 646 | n.fs.xlock.RLock() 647 | defer n.fs.xlock.RUnlock() 648 | if x := n.fs.xattrs[n.getRealPath()]; x != nil { 649 | 650 | var ok bool 651 | resp.Xattr, ok = x[req.Name] 652 | if ok { 653 | return nil 654 | } 655 | } 656 | return fuse.ErrNoXattr 657 | } 658 | 659 | var _ fs.NodeListxattrer = (*Node)(nil) 660 | 661 | // Listxattr implements fs.Listxattrer interface for *Node 662 | func (n *Node) Listxattr(ctx context.Context, 663 | req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) (err error) { 664 | time.Sleep(latency) 665 | if !inMemoryXattr { 666 | return fuse.ENOTSUP 667 | } 668 | 669 | defer func() { 670 | log.Printf("%s.Listxattr(%d,%d): error=%v", 671 | n.getRealPath(), req.Position, req.Size, err) 672 | }() 673 | n.fs.xlock.RLock() 674 | defer n.fs.xlock.RUnlock() 675 | if x := n.fs.xattrs[n.getRealPath()]; x != nil { 676 | names := make([]string, 0) 677 | for k := range x { 678 | names = append(names, k) 679 | } 680 | sort.Strings(names) 681 | 682 | if int(req.Position) >= len(names) { 683 | return nil 684 | } 685 | names = names[int(req.Position):] 686 | 687 | s := int(req.Size) 688 | if s == 0 || s > len(names) { 689 | s = len(names) 690 | } 691 | if s > 0 { 692 | resp.Append(names[:s]...) 693 | } 694 | } 695 | 696 | return nil 697 | } 698 | 699 | var _ fs.NodeSetxattrer = (*Node)(nil) 700 | 701 | // Setxattr implements fs.Setxattrer interface for *Node 702 | func (n *Node) Setxattr(ctx context.Context, 703 | req *fuse.SetxattrRequest) (err error) { 704 | time.Sleep(latency) 705 | if !inMemoryXattr { 706 | return fuse.ENOTSUP 707 | } 708 | 709 | defer func() { 710 | log.Printf("%s.Setxattr(%s): error=%v", n.getRealPath(), req.Name, err) 711 | }() 712 | n.fs.xlock.Lock() 713 | defer n.fs.xlock.Unlock() 714 | if n.fs.xattrs[n.getRealPath()] == nil { 715 | n.fs.xattrs[n.getRealPath()] = make(map[string][]byte) 716 | } 717 | buf := make([]byte, len(req.Xattr)) 718 | copy(buf, req.Xattr) 719 | 720 | n.fs.xattrs[n.getRealPath()][req.Name] = buf 721 | return nil 722 | } 723 | 724 | var _ fs.NodeRemovexattrer = (*Node)(nil) 725 | 726 | // Removexattr implements fs.Removexattrer interface for *Node 727 | func (n *Node) Removexattr(ctx context.Context, 728 | req *fuse.RemovexattrRequest) (err error) { 729 | time.Sleep(latency) 730 | if !inMemoryXattr { 731 | return fuse.ENOTSUP 732 | } 733 | 734 | defer func() { 735 | log.Printf("%s.Removexattr(%s): error=%v", n.getRealPath(), req.Name, err) 736 | }() 737 | n.fs.xlock.Lock() 738 | defer n.fs.xlock.Unlock() 739 | 740 | name := req.Name 741 | 742 | if x := n.fs.xattrs[n.getRealPath()]; x != nil { 743 | var ok bool 744 | _, ok = x[name] 745 | if ok { 746 | delete(x, name) 747 | return nil 748 | } 749 | } 750 | return fuse.ErrNoXattr 751 | } 752 | 753 | var _ fs.NodeForgetter = (*Node)(nil) 754 | 755 | // Forget implements fs.NodeForgetter interface for *Node 756 | func (n *Node) Forget() { 757 | n.fs.forgetNode(n) 758 | } 759 | -------------------------------------------------------------------------------- /examples/loopback/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build linux || darwin 6 | // +build linux darwin 7 | 8 | package main 9 | 10 | import ( 11 | "flag" 12 | "fmt" 13 | "log" 14 | "os" 15 | "time" 16 | 17 | "github.com/anacrolix/fuse" 18 | "github.com/anacrolix/fuse/fs" 19 | _ "github.com/anacrolix/fuse/fs/fstestutil" 20 | ) 21 | 22 | var ( 23 | inMemoryXattr bool 24 | latency time.Duration 25 | ) 26 | 27 | /* 28 | func init() { 29 | flag.BoolVar(&inMemoryXattr, "in-memory-xattr", false, 30 | "use an in-memory implementation for xattr. Otherwise,\n"+ 31 | "fall back to the ._ file approach provided by osxfuse.") 32 | flag.DurationVar(&latency, "latency", 0, 33 | "add an artificial latency to every fuse handler on every call") 34 | }*/ 35 | 36 | func usage() { 37 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 38 | fmt.Fprintf(os.Stderr, " %s ROOT MOUNTPOINT\n", os.Args[0]) 39 | flag.PrintDefaults() 40 | } 41 | 42 | func main() { 43 | flag.Usage = usage 44 | flag.Parse() 45 | 46 | if flag.NArg() != 2 { 47 | usage() 48 | os.Exit(2) 49 | } 50 | mountpoint := flag.Arg(1) 51 | 52 | c, err := fuse.Mount( 53 | mountpoint, 54 | fuse.FSName("loopback"), 55 | fuse.Subtype("loopback-fs"), 56 | fuse.VolumeName("goLoopback"), 57 | //fuse.AllowRoot(), 58 | ) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | defer c.Close() 63 | 64 | err = fs.Serve(c, newFS(flag.Arg(0))) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | 69 | // check if the mount process has an error to report 70 | <-c.Ready 71 | if err := c.MountError; err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | log.Println("mounted!") 76 | 77 | } 78 | -------------------------------------------------------------------------------- /fs/bench/bench_create_test.go: -------------------------------------------------------------------------------- 1 | package bench_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "sync" 10 | "testing" 11 | 12 | "github.com/anacrolix/fuse" 13 | "github.com/anacrolix/fuse/fs" 14 | "github.com/anacrolix/fuse/fs/fstestutil" 15 | "github.com/anacrolix/fuse/fs/fstestutil/spawntest/httpjson" 16 | ) 17 | 18 | type dummyFile struct { 19 | fstestutil.File 20 | } 21 | 22 | type benchCreateDir struct { 23 | fstestutil.Dir 24 | } 25 | 26 | var _ fs.NodeCreater = (*benchCreateDir)(nil) 27 | 28 | func (f *benchCreateDir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) { 29 | child := &dummyFile{} 30 | return child, child, nil 31 | } 32 | 33 | type benchCreateHelp struct { 34 | mu sync.Mutex 35 | n int 36 | names []string 37 | } 38 | 39 | func (b *benchCreateHelp) ServeHTTP(w http.ResponseWriter, req *http.Request) { 40 | switch req.URL.Path { 41 | case "/init": 42 | httpjson.ServePOST(b.doInit).ServeHTTP(w, req) 43 | case "/bench": 44 | httpjson.ServePOST(b.doBench).ServeHTTP(w, req) 45 | default: 46 | http.NotFound(w, req) 47 | } 48 | } 49 | 50 | type benchCreateInitRequest struct { 51 | Dir string 52 | N int 53 | } 54 | 55 | func (b *benchCreateHelp) doInit(ctx context.Context, req benchCreateInitRequest) (*struct{}, error) { 56 | b.mu.Lock() 57 | defer b.mu.Unlock() 58 | // prepare file names to decrease test overhead 59 | names := make([]string, 0, req.N) 60 | for i := 0; i < req.N; i++ { 61 | // zero-padded so cost stays the same on every iteration 62 | names = append(names, req.Dir+"/"+fmt.Sprintf("%08x", i)) 63 | } 64 | b.n = req.N 65 | b.names = names 66 | return &struct{}{}, nil 67 | } 68 | 69 | func (b *benchCreateHelp) doBench(ctx context.Context, _ struct{}) (*struct{}, error) { 70 | for i := 0; i < b.n; i++ { 71 | f, err := os.Create(b.names[i]) 72 | if err != nil { 73 | log.Fatalf("Create: %v", err) 74 | } 75 | f.Close() 76 | } 77 | return &struct{}{}, nil 78 | } 79 | 80 | var benchCreateHelper = helpers.Register("benchCreate", &benchCreateHelp{}) 81 | 82 | func BenchmarkCreate(b *testing.B) { 83 | ctx, cancel := context.WithCancel(context.Background()) 84 | defer cancel() 85 | f := &benchCreateDir{} 86 | mnt, err := fstestutil.MountedT(b, fstestutil.SimpleFS{f}, nil) 87 | if err != nil { 88 | b.Fatal(err) 89 | } 90 | defer mnt.Close() 91 | control := benchCreateHelper.Spawn(ctx, b) 92 | defer control.Close() 93 | 94 | req := benchCreateInitRequest{ 95 | Dir: mnt.Dir, 96 | N: b.N, 97 | } 98 | var nothing struct{} 99 | if err := control.JSON("/init").Call(ctx, req, ¬hing); err != nil { 100 | b.Fatalf("calling helper: %v", err) 101 | } 102 | b.ResetTimer() 103 | 104 | if err := control.JSON("/bench").Call(ctx, struct{}{}, ¬hing); err != nil { 105 | b.Fatalf("calling helper: %v", err) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /fs/bench/bench_lookup_test.go: -------------------------------------------------------------------------------- 1 | package bench_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "syscall" 8 | "testing" 9 | 10 | "github.com/anacrolix/fuse" 11 | "github.com/anacrolix/fuse/fs" 12 | "github.com/anacrolix/fuse/fs/fstestutil" 13 | "github.com/anacrolix/fuse/fs/fstestutil/spawntest/httpjson" 14 | ) 15 | 16 | type benchLookupDir struct { 17 | fstestutil.Dir 18 | } 19 | 20 | var _ fs.NodeRequestLookuper = (*benchLookupDir)(nil) 21 | 22 | func (f *benchLookupDir) Lookup(ctx context.Context, req *fuse.LookupRequest, resp *fuse.LookupResponse) (fs.Node, error) { 23 | return nil, syscall.ENOENT 24 | } 25 | 26 | type benchLookupRequest struct { 27 | Path string 28 | N int 29 | } 30 | 31 | func doBenchLookup(ctx context.Context, req benchLookupRequest) (*struct{}, error) { 32 | for i := 0; i < req.N; i++ { 33 | if _, err := os.Stat(req.Path); !os.IsNotExist(err) { 34 | return nil, fmt.Errorf("Stat: wrong error: %v", err) 35 | } 36 | } 37 | return &struct{}{}, nil 38 | } 39 | 40 | var benchLookupHelper = helpers.Register("benchLookup", httpjson.ServePOST(doBenchLookup)) 41 | 42 | func BenchmarkLookup(b *testing.B) { 43 | ctx, cancel := context.WithCancel(context.Background()) 44 | defer cancel() 45 | f := &benchLookupDir{} 46 | mnt, err := fstestutil.MountedT(b, fstestutil.SimpleFS{f}, nil) 47 | if err != nil { 48 | b.Fatal(err) 49 | } 50 | defer mnt.Close() 51 | control := benchLookupHelper.Spawn(ctx, b) 52 | defer control.Close() 53 | 54 | name := mnt.Dir + "/does-not-exist" 55 | req := benchLookupRequest{ 56 | Path: name, 57 | N: b.N, 58 | } 59 | var nothing struct{} 60 | b.ResetTimer() 61 | 62 | if err := control.JSON("/").Call(ctx, req, ¬hing); err != nil { 63 | b.Fatalf("calling helper: %v", err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /fs/bench/bench_readwrite_test.go: -------------------------------------------------------------------------------- 1 | package bench_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "syscall" 10 | "testing" 11 | 12 | "github.com/anacrolix/fuse" 13 | "github.com/anacrolix/fuse/fs" 14 | "github.com/anacrolix/fuse/fs/fstestutil" 15 | "github.com/anacrolix/fuse/fs/fstestutil/spawntest" 16 | "github.com/anacrolix/fuse/fs/fstestutil/spawntest/httpjson" 17 | ) 18 | 19 | type benchConfig struct { 20 | directIO bool 21 | } 22 | 23 | type benchFS struct { 24 | conf *benchConfig 25 | } 26 | 27 | var _ fs.FS = (*benchFS)(nil) 28 | 29 | func (f *benchFS) Root() (fs.Node, error) { 30 | return benchDir{fs: f}, nil 31 | } 32 | 33 | type benchDir struct { 34 | fs *benchFS 35 | } 36 | 37 | var _ fs.Node = benchDir{} 38 | var _ fs.NodeStringLookuper = benchDir{} 39 | var _ fs.Handle = benchDir{} 40 | var _ fs.HandleReadDirAller = benchDir{} 41 | 42 | func (benchDir) Attr(ctx context.Context, a *fuse.Attr) error { 43 | a.Inode = 1 44 | a.Mode = os.ModeDir | 0o555 45 | return nil 46 | } 47 | 48 | func (d benchDir) Lookup(ctx context.Context, name string) (fs.Node, error) { 49 | if name == "bench" { 50 | return benchFile{conf: d.fs.conf}, nil 51 | } 52 | return nil, syscall.ENOENT 53 | } 54 | 55 | func (benchDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { 56 | l := []fuse.Dirent{ 57 | {Inode: 2, Name: "bench", Type: fuse.DT_File}, 58 | } 59 | return l, nil 60 | } 61 | 62 | type benchFile struct { 63 | conf *benchConfig 64 | } 65 | 66 | var _ fs.Node = benchFile{} 67 | var _ fs.NodeOpener = benchFile{} 68 | var _ fs.NodeFsyncer = benchFile{} 69 | var _ fs.Handle = benchFile{} 70 | var _ fs.HandleReader = benchFile{} 71 | var _ fs.HandleWriter = benchFile{} 72 | 73 | func (benchFile) Attr(ctx context.Context, a *fuse.Attr) error { 74 | a.Inode = 2 75 | a.Mode = 0o644 76 | a.Size = 9999999999999999 77 | return nil 78 | } 79 | 80 | func (f benchFile) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) { 81 | if f.conf.directIO { 82 | resp.Flags |= fuse.OpenDirectIO 83 | } 84 | // TODO configurable? 85 | resp.Flags |= fuse.OpenKeepCache 86 | return f, nil 87 | } 88 | 89 | func (benchFile) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { 90 | resp.Data = resp.Data[:cap(resp.Data)] 91 | return nil 92 | } 93 | 94 | func (benchFile) Write(ctx context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) error { 95 | resp.Size = len(req.Data) 96 | return nil 97 | } 98 | 99 | func (benchFile) Fsync(ctx context.Context, req *fuse.FsyncRequest) error { 100 | return nil 101 | } 102 | 103 | type benchReadWriteRequest struct { 104 | Path string 105 | Size int64 106 | N int 107 | } 108 | 109 | func benchmark(helper *spawntest.Helper, conf *benchConfig, size int64) func(*testing.B) { 110 | fn := func(b *testing.B) { 111 | ctx, cancel := context.WithCancel(context.Background()) 112 | defer cancel() 113 | filesys := &benchFS{ 114 | conf: conf, 115 | } 116 | mnt, err := fstestutil.Mounted(filesys, nil, 117 | fuse.MaxReadahead(64*1024*1024), 118 | fuse.AsyncRead(), 119 | fuse.WritebackCache(), 120 | ) 121 | if err != nil { 122 | b.Fatal(err) 123 | } 124 | defer mnt.Close() 125 | control := helper.Spawn(ctx, b) 126 | defer control.Close() 127 | 128 | name := mnt.Dir + "/bench" 129 | req := benchReadWriteRequest{ 130 | Path: name, 131 | Size: size, 132 | N: b.N, 133 | } 134 | var nothing struct{} 135 | b.SetBytes(size) 136 | b.ResetTimer() 137 | 138 | if err := control.JSON("/").Call(ctx, req, ¬hing); err != nil { 139 | b.Fatalf("calling helper: %v", err) 140 | } 141 | } 142 | return fn 143 | } 144 | 145 | func benchmarkSizes(helper *spawntest.Helper, conf *benchConfig) func(*testing.B) { 146 | fn := func(b *testing.B) { 147 | b.Run("100", benchmark(helper, conf, 100)) 148 | b.Run("10MB", benchmark(helper, conf, 10*1024*1024)) 149 | b.Run("100MB", benchmark(helper, conf, 100*1024*1024)) 150 | } 151 | return fn 152 | } 153 | 154 | type zero struct{} 155 | 156 | func (zero) Read(p []byte) (n int, err error) { 157 | return len(p), nil 158 | } 159 | 160 | var Zero io.Reader = zero{} 161 | 162 | func doBenchWrite(ctx context.Context, req benchReadWriteRequest) (*struct{}, error) { 163 | f, err := os.Create(req.Path) 164 | if err != nil { 165 | return nil, err 166 | } 167 | defer f.Close() 168 | 169 | for i := 0; i < req.N; i++ { 170 | if _, err := io.CopyN(f, Zero, req.Size); err != nil { 171 | return nil, err 172 | } 173 | } 174 | return &struct{}{}, nil 175 | } 176 | 177 | var benchWriteHelper = helpers.Register("benchWrite", httpjson.ServePOST(doBenchWrite)) 178 | 179 | func BenchmarkWrite(b *testing.B) { 180 | b.Run("pagecache", benchmarkSizes(benchWriteHelper, &benchConfig{})) 181 | b.Run("direct", benchmarkSizes(benchWriteHelper, &benchConfig{ 182 | directIO: true, 183 | })) 184 | } 185 | 186 | func doBenchWriteSync(ctx context.Context, req benchReadWriteRequest) (*struct{}, error) { 187 | f, err := os.Create(req.Path) 188 | if err != nil { 189 | return nil, err 190 | } 191 | defer f.Close() 192 | 193 | for i := 0; i < req.N; i++ { 194 | if _, err := io.CopyN(f, Zero, req.Size); err != nil { 195 | return nil, err 196 | } 197 | if err := f.Sync(); err != nil { 198 | return nil, err 199 | } 200 | } 201 | return &struct{}{}, nil 202 | } 203 | 204 | var benchWriteSyncHelper = helpers.Register("benchWriteSync", httpjson.ServePOST(doBenchWriteSync)) 205 | 206 | func BenchmarkWriteSync(b *testing.B) { 207 | b.Run("pagecache", benchmarkSizes(benchWriteSyncHelper, &benchConfig{})) 208 | b.Run("direct", benchmarkSizes(benchWriteSyncHelper, &benchConfig{ 209 | directIO: true, 210 | })) 211 | } 212 | 213 | func doBenchRead(ctx context.Context, req benchReadWriteRequest) (*struct{}, error) { 214 | f, err := os.Create(req.Path) 215 | if err != nil { 216 | return nil, err 217 | } 218 | defer f.Close() 219 | 220 | for i := 0; i < req.N; i++ { 221 | n, err := io.CopyN(ioutil.Discard, f, req.Size) 222 | if err != nil { 223 | return nil, err 224 | } 225 | if n != req.Size { 226 | return nil, fmt.Errorf("unexpected size: %d != %d", n, req.Size) 227 | } 228 | } 229 | return &struct{}{}, nil 230 | } 231 | 232 | var benchReadHelper = helpers.Register("benchRead", httpjson.ServePOST(doBenchRead)) 233 | 234 | func BenchmarkRead(b *testing.B) { 235 | b.Run("pagecache", benchmarkSizes(benchReadHelper, &benchConfig{})) 236 | b.Run("direct", benchmarkSizes(benchReadHelper, &benchConfig{ 237 | directIO: true, 238 | })) 239 | } 240 | -------------------------------------------------------------------------------- /fs/bench/doc.go: -------------------------------------------------------------------------------- 1 | // Package bench contains benchmarks. 2 | // 3 | // It is kept in a separate package to avoid conflicting with the 4 | // debug-heavy defaults for the actual tests. 5 | package bench 6 | -------------------------------------------------------------------------------- /fs/bench/helpers_test.go: -------------------------------------------------------------------------------- 1 | package bench_test 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "testing" 7 | 8 | "github.com/anacrolix/fuse/fs/fstestutil/spawntest" 9 | ) 10 | 11 | var helpers spawntest.Registry 12 | 13 | func TestMain(m *testing.M) { 14 | helpers.AddFlag(flag.CommandLine) 15 | flag.Parse() 16 | helpers.RunIfNeeded() 17 | os.Exit(m.Run()) 18 | } 19 | -------------------------------------------------------------------------------- /fs/fstestutil/checkdir.go: -------------------------------------------------------------------------------- 1 | package fstestutil 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | ) 8 | 9 | // FileInfoCheck is a function that validates an os.FileInfo according 10 | // to some criteria. 11 | type FileInfoCheck func(fi os.FileInfo) error 12 | 13 | type checkDirError struct { 14 | missing map[string]struct{} 15 | extra map[string]os.FileMode 16 | } 17 | 18 | func (e *checkDirError) Error() string { 19 | return fmt.Sprintf("wrong directory contents: missing %v, extra %v", e.missing, e.extra) 20 | } 21 | 22 | // CheckDir checks the contents of the directory at path, making sure 23 | // every directory entry listed in want is present. If the check is 24 | // not nil, it must also pass. 25 | // 26 | // If want contains the impossible filename "", unexpected files are 27 | // checked with that. If the key is not in want, unexpected files are 28 | // an error. 29 | // 30 | // Missing entries, that are listed in want but not seen, are an 31 | // error. 32 | func CheckDir(path string, want map[string]FileInfoCheck) error { 33 | problems := &checkDirError{ 34 | missing: make(map[string]struct{}, len(want)), 35 | extra: make(map[string]os.FileMode), 36 | } 37 | for k := range want { 38 | if k == "" { 39 | continue 40 | } 41 | problems.missing[k] = struct{}{} 42 | } 43 | 44 | fis, err := ioutil.ReadDir(path) 45 | if err != nil { 46 | return fmt.Errorf("cannot read directory: %v", err) 47 | } 48 | 49 | for _, fi := range fis { 50 | check, ok := want[fi.Name()] 51 | if !ok { 52 | check, ok = want[""] 53 | } 54 | if !ok { 55 | problems.extra[fi.Name()] = fi.Mode() 56 | continue 57 | } 58 | delete(problems.missing, fi.Name()) 59 | if check != nil { 60 | if err := check(fi); err != nil { 61 | return fmt.Errorf("check failed: %v: %v", fi.Name(), err) 62 | } 63 | } 64 | } 65 | 66 | if len(problems.missing) > 0 || len(problems.extra) > 0 { 67 | return problems 68 | } 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /fs/fstestutil/debug.go: -------------------------------------------------------------------------------- 1 | package fstestutil 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "strconv" 7 | 8 | "github.com/anacrolix/fuse" 9 | ) 10 | 11 | type flagDebug bool 12 | 13 | var debug flagDebug 14 | 15 | var _ flag.Value = &debug 16 | 17 | func (f *flagDebug) IsBoolFlag() bool { 18 | return true 19 | } 20 | 21 | func nop(msg interface{}) {} 22 | 23 | func (f *flagDebug) Set(s string) error { 24 | v, err := strconv.ParseBool(s) 25 | if err != nil { 26 | return err 27 | } 28 | *f = flagDebug(v) 29 | if v { 30 | fuse.Debug = logMsg 31 | } else { 32 | fuse.Debug = nop 33 | } 34 | return nil 35 | } 36 | 37 | func (f *flagDebug) String() string { 38 | return strconv.FormatBool(bool(*f)) 39 | } 40 | 41 | func logMsg(msg interface{}) { 42 | log.Printf("FUSE: %s\n", msg) 43 | } 44 | 45 | func init() { 46 | flag.Var(&debug, "fuse.debug", "log FUSE processing details") 47 | } 48 | 49 | // DebugByDefault changes the default of the `-fuse.debug` flag to 50 | // true. 51 | // 52 | // This package registers a command line flag `-fuse.debug` and when 53 | // run with that flag (and activated inside the tests), logs FUSE 54 | // debug messages. 55 | // 56 | // This is disabled by default, as most callers probably won't care 57 | // about FUSE details. Use DebugByDefault for tests where you'd 58 | // normally be passing `-fuse.debug` all the time anyway. 59 | // 60 | // Call from an init function. 61 | func DebugByDefault() { 62 | f := flag.Lookup("fuse.debug") 63 | f.DefValue = "true" 64 | f.Value.Set(f.DefValue) 65 | } 66 | -------------------------------------------------------------------------------- /fs/fstestutil/doc.go: -------------------------------------------------------------------------------- 1 | package fstestutil // import "github.com/anacrolix/fuse/fs/fstestutil" 2 | -------------------------------------------------------------------------------- /fs/fstestutil/mounted.go: -------------------------------------------------------------------------------- 1 | package fstestutil 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/anacrolix/fuse" 13 | "github.com/anacrolix/fuse/fs" 14 | ) 15 | 16 | // Mount contains information about the mount for the test to use. 17 | type Mount struct { 18 | // Dir is the temporary directory where the filesystem is mounted. 19 | Dir string 20 | 21 | Conn *fuse.Conn 22 | Server *fs.Server 23 | 24 | // Error will receive the return value of Serve. 25 | Error <-chan error 26 | 27 | done <-chan struct{} 28 | closed bool 29 | } 30 | 31 | // Close unmounts the filesystem and waits for fs.Serve to return. Any 32 | // returned error will be stored in Err. It is safe to call Close 33 | // multiple times. 34 | func (mnt *Mount) Close() { 35 | if mnt.closed { 36 | return 37 | } 38 | mnt.closed = true 39 | prev := "" 40 | for tries := 0; tries < 1000; tries++ { 41 | err := fuse.Unmount(mnt.Dir) 42 | if err != nil { 43 | msg := err.Error() 44 | // hide repeating errors 45 | if msg != prev { 46 | // TODO do more than log? 47 | 48 | // silence a very common message we can't do anything 49 | // about, for the first few tries. it'll still show if 50 | // the condition persists. 51 | if !strings.HasSuffix(err.Error(), ": Device or resource busy") || tries > 10 { 52 | log.Printf("unmount error: %v", err) 53 | prev = msg 54 | } 55 | } 56 | time.Sleep(100 * time.Millisecond) 57 | continue 58 | } 59 | break 60 | } 61 | <-mnt.done 62 | mnt.Conn.Close() 63 | os.Remove(mnt.Dir) 64 | } 65 | 66 | func (mnt *Mount) Backend() fuse.Backend { 67 | return mnt.Conn.Backend() 68 | } 69 | 70 | // MountedFunc mounts a filesystem at a temporary directory. The 71 | // filesystem used is constructed by calling a function, to allow 72 | // storing fuse.Conn and fs.Server in the FS. 73 | // 74 | // It also waits until the filesystem is known to be visible (OS X 75 | // workaround). 76 | // 77 | // After successful return, caller must clean up by calling Close. 78 | func MountedFunc(fn func(*Mount) fs.FS, conf *fs.Config, options ...fuse.MountOption) (*Mount, error) { 79 | dir, err := ioutil.TempDir("", "fusetest") 80 | if err != nil { 81 | return nil, err 82 | } 83 | defer func() { 84 | _ = os.Remove(dir) 85 | }() 86 | c, err := fuse.Mount(dir, options...) 87 | if err != nil { 88 | return nil, err 89 | } 90 | server := fs.New(c, conf) 91 | done := make(chan struct{}) 92 | serveErr := make(chan error, 1) 93 | mnt := &Mount{ 94 | Dir: dir, 95 | Conn: c, 96 | Server: server, 97 | Error: serveErr, 98 | done: done, 99 | } 100 | filesys := fn(mnt) 101 | go func() { 102 | defer close(done) 103 | serveErr <- server.Serve(filesys) 104 | }() 105 | 106 | select { 107 | case <-mnt.Conn.Ready: 108 | if err := mnt.Conn.MountError; err != nil { 109 | return nil, err 110 | } 111 | return mnt, nil 112 | case err = <-mnt.Error: 113 | // Serve quit early 114 | if err != nil { 115 | return nil, err 116 | } 117 | //lint:ignore ST1005 uppercase because it's an idenfier 118 | return nil, errors.New("Serve exited early") 119 | } 120 | } 121 | 122 | // Mounted mounts the fuse.Server at a temporary directory. 123 | // 124 | // It also waits until the filesystem is known to be visible (OS X 125 | // workaround). 126 | // 127 | // After successful return, caller must clean up by calling Close. 128 | func Mounted(filesys fs.FS, conf *fs.Config, options ...fuse.MountOption) (*Mount, error) { 129 | fn := func(*Mount) fs.FS { return filesys } 130 | return MountedFunc(fn, conf, options...) 131 | } 132 | 133 | // MountedFuncT mounts a filesystem at a temporary directory, 134 | // directing it's debug log to the testing logger. 135 | // 136 | // See MountedFunc for usage. 137 | // 138 | // The debug log is not enabled by default. Use `-fuse.debug` or call 139 | // DebugByDefault to enable. 140 | func MountedFuncT(t testing.TB, fn func(*Mount) fs.FS, conf *fs.Config, options ...fuse.MountOption) (*Mount, error) { 141 | if conf == nil { 142 | conf = &fs.Config{} 143 | } 144 | if debug && conf.Debug == nil { 145 | conf.Debug = func(msg interface{}) { 146 | t.Helper() 147 | t.Logf("FUSE: %s", msg) 148 | } 149 | } 150 | return MountedFunc(fn, conf, options...) 151 | } 152 | 153 | // MountedT mounts the filesystem at a temporary directory, 154 | // directing it's debug log to the testing logger. 155 | // 156 | // See Mounted for usage. 157 | // 158 | // The debug log is not enabled by default. Use `-fuse.debug` or call 159 | // DebugByDefault to enable. 160 | func MountedT(t testing.TB, filesys fs.FS, conf *fs.Config, options ...fuse.MountOption) (*Mount, error) { 161 | fn := func(*Mount) fs.FS { return filesys } 162 | return MountedFuncT(t, fn, conf, options...) 163 | } 164 | -------------------------------------------------------------------------------- /fs/fstestutil/mountinfo.go: -------------------------------------------------------------------------------- 1 | package fstestutil 2 | 3 | // MountInfo describes a mounted file system. 4 | type MountInfo struct { 5 | FSName string 6 | Type string 7 | } 8 | 9 | // GetMountInfo finds information about the mount at mnt. It is 10 | // intended for use by tests only, and only fetches information 11 | // relevant to the current tests. 12 | func GetMountInfo(mnt string) (*MountInfo, error) { 13 | return getMountInfo(mnt) 14 | } 15 | -------------------------------------------------------------------------------- /fs/fstestutil/mountinfo_darwin.go: -------------------------------------------------------------------------------- 1 | package fstestutil 2 | 3 | import ( 4 | "regexp" 5 | "syscall" 6 | ) 7 | 8 | var re = regexp.MustCompile(`\\(.)`) 9 | 10 | // unescape removes backslash-escaping. The escaped characters are not 11 | // mapped in any way; that is, unescape(`\n` ) == `n`. 12 | func unescape(s string) string { 13 | return re.ReplaceAllString(s, `$1`) 14 | } 15 | 16 | // cstr converts a nil-terminated C string into a Go string 17 | func cstr(ca []int8) string { 18 | s := make([]byte, 0, len(ca)) 19 | for _, c := range ca { 20 | if c == 0x00 { 21 | break 22 | } 23 | s = append(s, byte(c)) 24 | } 25 | return string(s) 26 | } 27 | 28 | func getMountInfo(mnt string) (*MountInfo, error) { 29 | var st syscall.Statfs_t 30 | err := syscall.Statfs(mnt, &st) 31 | if err != nil { 32 | return nil, err 33 | } 34 | i := &MountInfo{ 35 | // osx getmntent(3) fails to un-escape the data, so we do it.. 36 | // this might lead to double-unescaping in the future. fun. 37 | // TestMountOptionFSNameEvilBackslashDouble checks for that. 38 | FSName: unescape(cstr(st.Mntfromname[:])), 39 | } 40 | return i, nil 41 | } 42 | -------------------------------------------------------------------------------- /fs/fstestutil/mountinfo_freebsd.go: -------------------------------------------------------------------------------- 1 | package fstestutil 2 | 3 | import "errors" 4 | 5 | func getMountInfo(mnt string) (*MountInfo, error) { 6 | return nil, errors.New("FreeBSD has no useful mount information") 7 | } 8 | -------------------------------------------------------------------------------- /fs/fstestutil/mountinfo_linux.go: -------------------------------------------------------------------------------- 1 | package fstestutil 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // Linux /proc/mounts shows current mounts. 11 | // Same format as /etc/fstab. Quoting getmntent(3): 12 | // 13 | // Since fields in the mtab and fstab files are separated by whitespace, 14 | // octal escapes are used to represent the four characters space (\040), 15 | // tab (\011), newline (\012) and backslash (\134) in those files when 16 | // they occur in one of the four strings in a mntent structure. 17 | // 18 | // http://linux.die.net/man/3/getmntent 19 | 20 | var fstabUnescape = strings.NewReplacer( 21 | `\040`, "\040", 22 | `\011`, "\011", 23 | `\012`, "\012", 24 | `\134`, "\134", 25 | ) 26 | 27 | var errNotFound = errors.New("mount not found") 28 | 29 | func getMountInfo(mnt string) (*MountInfo, error) { 30 | // TODO delay a little to minimize an undiagnosed race between 31 | // fuse.Conn.Ready and /proc/mounts 32 | // https://github.com/bazil/fuse/issues/228 33 | time.Sleep(10 * time.Millisecond) 34 | data, err := ioutil.ReadFile("/proc/mounts") 35 | if err != nil { 36 | return nil, err 37 | } 38 | for _, line := range strings.Split(string(data), "\n") { 39 | fields := strings.Fields(line) 40 | if len(fields) < 3 { 41 | continue 42 | } 43 | // Fields are: fsname dir type opts freq passno 44 | fsname := fstabUnescape.Replace(fields[0]) 45 | dir := fstabUnescape.Replace(fields[1]) 46 | fstype := fstabUnescape.Replace(fields[2]) 47 | if mnt == dir { 48 | info := &MountInfo{ 49 | FSName: fsname, 50 | Type: fstype, 51 | } 52 | return info, nil 53 | } 54 | } 55 | return nil, errNotFound 56 | } 57 | -------------------------------------------------------------------------------- /fs/fstestutil/record/buffer.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "sync" 7 | ) 8 | 9 | // Buffer is like bytes.Buffer but safe to access from multiple 10 | // goroutines. 11 | type Buffer struct { 12 | mu sync.Mutex 13 | buf bytes.Buffer 14 | } 15 | 16 | var _ io.Writer = (*Buffer)(nil) 17 | 18 | func (b *Buffer) Write(p []byte) (n int, err error) { 19 | b.mu.Lock() 20 | defer b.mu.Unlock() 21 | return b.buf.Write(p) 22 | } 23 | 24 | func (b *Buffer) Bytes() []byte { 25 | b.mu.Lock() 26 | defer b.mu.Unlock() 27 | return b.buf.Bytes() 28 | } 29 | -------------------------------------------------------------------------------- /fs/fstestutil/record/record.go: -------------------------------------------------------------------------------- 1 | package record // import "github.com/anacrolix/fuse/fs/fstestutil/record" 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "sync/atomic" 7 | "syscall" 8 | 9 | "github.com/anacrolix/fuse" 10 | "github.com/anacrolix/fuse/fs" 11 | ) 12 | 13 | // Writes gathers data from FUSE Write calls. 14 | type Writes struct { 15 | buf Buffer 16 | } 17 | 18 | var _ fs.HandleWriter = (*Writes)(nil) 19 | 20 | func (w *Writes) Write(ctx context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) error { 21 | n, err := w.buf.Write(req.Data) 22 | resp.Size = n 23 | if err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | 29 | func (w *Writes) RecordedWriteData() []byte { 30 | return w.buf.Bytes() 31 | } 32 | 33 | // Counter records number of times a thing has occurred. 34 | type Counter struct { 35 | count uint32 36 | } 37 | 38 | func (r *Counter) Inc() { 39 | atomic.AddUint32(&r.count, 1) 40 | } 41 | 42 | func (r *Counter) Count() uint32 { 43 | return atomic.LoadUint32(&r.count) 44 | } 45 | 46 | // MarkRecorder records whether a thing has occurred. 47 | type MarkRecorder struct { 48 | count Counter 49 | } 50 | 51 | func (r *MarkRecorder) Mark() { 52 | r.count.Inc() 53 | } 54 | 55 | func (r *MarkRecorder) Recorded() bool { 56 | return r.count.Count() > 0 57 | } 58 | 59 | // Flushes notes whether a FUSE Flush call has been seen. 60 | type Flushes struct { 61 | rec MarkRecorder 62 | } 63 | 64 | var _ fs.HandleFlusher = (*Flushes)(nil) 65 | 66 | func (r *Flushes) Flush(ctx context.Context, req *fuse.FlushRequest) error { 67 | r.rec.Mark() 68 | return nil 69 | } 70 | 71 | func (r *Flushes) RecordedFlush() bool { 72 | return r.rec.Recorded() 73 | } 74 | 75 | type Recorder struct { 76 | mu sync.Mutex 77 | val interface{} 78 | } 79 | 80 | // Record that we've seen value. A nil value is indistinguishable from 81 | // no value recorded. 82 | func (r *Recorder) Record(value interface{}) { 83 | r.mu.Lock() 84 | r.val = value 85 | r.mu.Unlock() 86 | } 87 | 88 | func (r *Recorder) Recorded() interface{} { 89 | r.mu.Lock() 90 | val := r.val 91 | r.mu.Unlock() 92 | return val 93 | } 94 | 95 | type RequestRecorder struct { 96 | rec Recorder 97 | } 98 | 99 | // Record a fuse.Request, after zeroing header fields that are hard to 100 | // reproduce. 101 | // 102 | // Make sure to record a copy, not the original request. 103 | func (r *RequestRecorder) RecordRequest(req fuse.Request) { 104 | hdr := req.Hdr() 105 | *hdr = fuse.Header{} 106 | r.rec.Record(req) 107 | } 108 | 109 | func (r *RequestRecorder) Recorded() fuse.Request { 110 | val := r.rec.Recorded() 111 | if val == nil { 112 | return nil 113 | } 114 | return val.(fuse.Request) 115 | } 116 | 117 | // Setattrs records a Setattr request and its fields. 118 | type Setattrs struct { 119 | rec RequestRecorder 120 | } 121 | 122 | var _ fs.NodeSetattrer = (*Setattrs)(nil) 123 | 124 | func (r *Setattrs) Setattr(ctx context.Context, req *fuse.SetattrRequest, resp *fuse.SetattrResponse) error { 125 | tmp := *req 126 | r.rec.RecordRequest(&tmp) 127 | return nil 128 | } 129 | 130 | func (r *Setattrs) RecordedSetattr() fuse.SetattrRequest { 131 | val := r.rec.Recorded() 132 | if val == nil { 133 | return fuse.SetattrRequest{} 134 | } 135 | return *(val.(*fuse.SetattrRequest)) 136 | } 137 | 138 | // Fsyncs records an Fsync request and its fields. 139 | type Fsyncs struct { 140 | rec RequestRecorder 141 | } 142 | 143 | var _ fs.NodeFsyncer = (*Fsyncs)(nil) 144 | 145 | func (r *Fsyncs) Fsync(ctx context.Context, req *fuse.FsyncRequest) error { 146 | tmp := *req 147 | r.rec.RecordRequest(&tmp) 148 | return nil 149 | } 150 | 151 | func (r *Fsyncs) RecordedFsync() fuse.FsyncRequest { 152 | val := r.rec.Recorded() 153 | if val == nil { 154 | return fuse.FsyncRequest{} 155 | } 156 | return *(val.(*fuse.FsyncRequest)) 157 | } 158 | 159 | // Mkdirs records a Mkdir request and its fields. 160 | type Mkdirs struct { 161 | rec RequestRecorder 162 | } 163 | 164 | var _ fs.NodeMkdirer = (*Mkdirs)(nil) 165 | 166 | // Mkdir records the request and returns an error. Most callers should 167 | // wrap this call in a function that returns a more useful result. 168 | func (r *Mkdirs) Mkdir(ctx context.Context, req *fuse.MkdirRequest) (fs.Node, error) { 169 | tmp := *req 170 | r.rec.RecordRequest(&tmp) 171 | return nil, syscall.EIO 172 | } 173 | 174 | // RecordedMkdir returns information about the Mkdir request. 175 | // If no request was seen, returns a zero value. 176 | func (r *Mkdirs) RecordedMkdir() fuse.MkdirRequest { 177 | val := r.rec.Recorded() 178 | if val == nil { 179 | return fuse.MkdirRequest{} 180 | } 181 | return *(val.(*fuse.MkdirRequest)) 182 | } 183 | 184 | // Symlinks records a Symlink request and its fields. 185 | type Symlinks struct { 186 | rec RequestRecorder 187 | } 188 | 189 | var _ fs.NodeSymlinker = (*Symlinks)(nil) 190 | 191 | // Symlink records the request and returns an error. Most callers should 192 | // wrap this call in a function that returns a more useful result. 193 | func (r *Symlinks) Symlink(ctx context.Context, req *fuse.SymlinkRequest) (fs.Node, error) { 194 | tmp := *req 195 | r.rec.RecordRequest(&tmp) 196 | return nil, syscall.EIO 197 | } 198 | 199 | // RecordedSymlink returns information about the Symlink request. 200 | // If no request was seen, returns a zero value. 201 | func (r *Symlinks) RecordedSymlink() fuse.SymlinkRequest { 202 | val := r.rec.Recorded() 203 | if val == nil { 204 | return fuse.SymlinkRequest{} 205 | } 206 | return *(val.(*fuse.SymlinkRequest)) 207 | } 208 | 209 | // Links records a Link request and its fields. 210 | type Links struct { 211 | rec RequestRecorder 212 | } 213 | 214 | var _ fs.NodeLinker = (*Links)(nil) 215 | 216 | // Link records the request and returns an error. Most callers should 217 | // wrap this call in a function that returns a more useful result. 218 | func (r *Links) Link(ctx context.Context, req *fuse.LinkRequest, old fs.Node) (fs.Node, error) { 219 | tmp := *req 220 | r.rec.RecordRequest(&tmp) 221 | return nil, syscall.EIO 222 | } 223 | 224 | // RecordedLink returns information about the Link request. 225 | // If no request was seen, returns a zero value. 226 | func (r *Links) RecordedLink() fuse.LinkRequest { 227 | val := r.rec.Recorded() 228 | if val == nil { 229 | return fuse.LinkRequest{} 230 | } 231 | return *(val.(*fuse.LinkRequest)) 232 | } 233 | 234 | // Mknods records a Mknod request and its fields. 235 | type Mknods struct { 236 | rec RequestRecorder 237 | } 238 | 239 | var _ fs.NodeMknoder = (*Mknods)(nil) 240 | 241 | // Mknod records the request and returns an error. Most callers should 242 | // wrap this call in a function that returns a more useful result. 243 | func (r *Mknods) Mknod(ctx context.Context, req *fuse.MknodRequest) (fs.Node, error) { 244 | tmp := *req 245 | r.rec.RecordRequest(&tmp) 246 | return nil, syscall.EIO 247 | } 248 | 249 | // RecordedMknod returns information about the Mknod request. 250 | // If no request was seen, returns a zero value. 251 | func (r *Mknods) RecordedMknod() fuse.MknodRequest { 252 | val := r.rec.Recorded() 253 | if val == nil { 254 | return fuse.MknodRequest{} 255 | } 256 | return *(val.(*fuse.MknodRequest)) 257 | } 258 | 259 | // Opens records a Open request and its fields. 260 | type Opens struct { 261 | rec RequestRecorder 262 | } 263 | 264 | var _ fs.NodeOpener = (*Opens)(nil) 265 | 266 | // Open records the request and returns an error. Most callers should 267 | // wrap this call in a function that returns a more useful result. 268 | func (r *Opens) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) { 269 | tmp := *req 270 | r.rec.RecordRequest(&tmp) 271 | return nil, syscall.EIO 272 | } 273 | 274 | // RecordedOpen returns information about the Open request. 275 | // If no request was seen, returns a zero value. 276 | func (r *Opens) RecordedOpen() fuse.OpenRequest { 277 | val := r.rec.Recorded() 278 | if val == nil { 279 | return fuse.OpenRequest{} 280 | } 281 | return *(val.(*fuse.OpenRequest)) 282 | } 283 | 284 | // Getxattrs records a Getxattr request and its fields. 285 | type Getxattrs struct { 286 | rec RequestRecorder 287 | } 288 | 289 | var _ fs.NodeGetxattrer = (*Getxattrs)(nil) 290 | 291 | // Getxattr records the request and returns an error. Most callers should 292 | // wrap this call in a function that returns a more useful result. 293 | func (r *Getxattrs) Getxattr(ctx context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error { 294 | tmp := *req 295 | r.rec.RecordRequest(&tmp) 296 | return fuse.ErrNoXattr 297 | } 298 | 299 | // RecordedGetxattr returns information about the Getxattr request. 300 | // If no request was seen, returns a zero value. 301 | func (r *Getxattrs) RecordedGetxattr() fuse.GetxattrRequest { 302 | val := r.rec.Recorded() 303 | if val == nil { 304 | return fuse.GetxattrRequest{} 305 | } 306 | return *(val.(*fuse.GetxattrRequest)) 307 | } 308 | 309 | // Listxattrs records a Listxattr request and its fields. 310 | type Listxattrs struct { 311 | rec RequestRecorder 312 | } 313 | 314 | var _ fs.NodeListxattrer = (*Listxattrs)(nil) 315 | 316 | // Listxattr records the request and returns an error. Most callers should 317 | // wrap this call in a function that returns a more useful result. 318 | func (r *Listxattrs) Listxattr(ctx context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error { 319 | tmp := *req 320 | r.rec.RecordRequest(&tmp) 321 | return fuse.ErrNoXattr 322 | } 323 | 324 | // RecordedListxattr returns information about the Listxattr request. 325 | // If no request was seen, returns a zero value. 326 | func (r *Listxattrs) RecordedListxattr() fuse.ListxattrRequest { 327 | val := r.rec.Recorded() 328 | if val == nil { 329 | return fuse.ListxattrRequest{} 330 | } 331 | return *(val.(*fuse.ListxattrRequest)) 332 | } 333 | 334 | // Setxattrs records a Setxattr request and its fields. 335 | type Setxattrs struct { 336 | rec RequestRecorder 337 | } 338 | 339 | var _ fs.NodeSetxattrer = (*Setxattrs)(nil) 340 | 341 | // Setxattr records the request and returns an error. Most callers should 342 | // wrap this call in a function that returns a more useful result. 343 | func (r *Setxattrs) Setxattr(ctx context.Context, req *fuse.SetxattrRequest) error { 344 | tmp := *req 345 | // The byte slice points to memory that will be reused, so make a 346 | // deep copy. 347 | tmp.Xattr = append([]byte(nil), req.Xattr...) 348 | r.rec.RecordRequest(&tmp) 349 | return nil 350 | } 351 | 352 | // RecordedSetxattr returns information about the Setxattr request. 353 | // If no request was seen, returns a zero value. 354 | func (r *Setxattrs) RecordedSetxattr() fuse.SetxattrRequest { 355 | val := r.rec.Recorded() 356 | if val == nil { 357 | return fuse.SetxattrRequest{} 358 | } 359 | return *(val.(*fuse.SetxattrRequest)) 360 | } 361 | 362 | // Removexattrs records a Removexattr request and its fields. 363 | type Removexattrs struct { 364 | rec RequestRecorder 365 | } 366 | 367 | var _ fs.NodeRemovexattrer = (*Removexattrs)(nil) 368 | 369 | // Removexattr records the request and returns an error. Most callers should 370 | // wrap this call in a function that returns a more useful result. 371 | func (r *Removexattrs) Removexattr(ctx context.Context, req *fuse.RemovexattrRequest) error { 372 | tmp := *req 373 | r.rec.RecordRequest(&tmp) 374 | return nil 375 | } 376 | 377 | // RecordedRemovexattr returns information about the Removexattr request. 378 | // If no request was seen, returns a zero value. 379 | func (r *Removexattrs) RecordedRemovexattr() fuse.RemovexattrRequest { 380 | val := r.rec.Recorded() 381 | if val == nil { 382 | return fuse.RemovexattrRequest{} 383 | } 384 | return *(val.(*fuse.RemovexattrRequest)) 385 | } 386 | 387 | // Creates records a Create request and its fields. 388 | type Creates struct { 389 | rec RequestRecorder 390 | } 391 | 392 | var _ fs.NodeCreater = (*Creates)(nil) 393 | 394 | // Create records the request and returns an error. Most callers should 395 | // wrap this call in a function that returns a more useful result. 396 | func (r *Creates) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) { 397 | tmp := *req 398 | r.rec.RecordRequest(&tmp) 399 | return nil, nil, syscall.EIO 400 | } 401 | 402 | // RecordedCreate returns information about the Create request. 403 | // If no request was seen, returns a zero value. 404 | func (r *Creates) RecordedCreate() fuse.CreateRequest { 405 | val := r.rec.Recorded() 406 | if val == nil { 407 | return fuse.CreateRequest{} 408 | } 409 | return *(val.(*fuse.CreateRequest)) 410 | } 411 | -------------------------------------------------------------------------------- /fs/fstestutil/record/wait.go: -------------------------------------------------------------------------------- 1 | package record 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | 8 | "github.com/anacrolix/fuse" 9 | "github.com/anacrolix/fuse/fs" 10 | ) 11 | 12 | // ReleaseWaiter notes whether a FUSE Release call has been seen. 13 | // 14 | // Releases are not guaranteed to happen synchronously with any client 15 | // call, so they must be waited for. 16 | type ReleaseWaiter struct { 17 | once sync.Once 18 | seen chan *fuse.ReleaseRequest 19 | } 20 | 21 | var _ fs.HandleReleaser = (*ReleaseWaiter)(nil) 22 | 23 | func (r *ReleaseWaiter) init() { 24 | r.once.Do(func() { 25 | r.seen = make(chan *fuse.ReleaseRequest, 1) 26 | }) 27 | } 28 | 29 | func (r *ReleaseWaiter) Release(ctx context.Context, req *fuse.ReleaseRequest) error { 30 | r.init() 31 | tmp := *req 32 | hdr := tmp.Hdr() 33 | *hdr = fuse.Header{} 34 | r.seen <- &tmp 35 | close(r.seen) 36 | return nil 37 | } 38 | 39 | // WaitForRelease waits for Release to be called. 40 | // 41 | // With zero duration, wait forever. Otherwise, timeout early 42 | // in a more controlled way than `-test.timeout`. 43 | // 44 | // Returns a sanitized ReleaseRequest and whether a Release was seen. 45 | // Always true if dur==0. 46 | func (r *ReleaseWaiter) WaitForRelease(dur time.Duration) (*fuse.ReleaseRequest, bool) { 47 | r.init() 48 | var timeout <-chan time.Time 49 | if dur > 0 { 50 | timeout = time.After(dur) 51 | } 52 | select { 53 | case req := <-r.seen: 54 | return req, true 55 | case <-timeout: 56 | return nil, false 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /fs/fstestutil/spawntest/example_test.go: -------------------------------------------------------------------------------- 1 | package spawntest_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "os" 8 | "testing" 9 | 10 | "github.com/anacrolix/fuse/fs/fstestutil/spawntest" 11 | "github.com/anacrolix/fuse/fs/fstestutil/spawntest/httpjson" 12 | ) 13 | 14 | var helpers spawntest.Registry 15 | 16 | type addRequest struct { 17 | A uint64 18 | B uint64 19 | } 20 | 21 | type addResult struct { 22 | X uint64 23 | } 24 | 25 | func add(ctx context.Context, req addRequest) (*addResult, error) { 26 | // In real tests, you'd instruct the helper to interact with the 27 | // system-under-test on behalf of the unit test process. For 28 | // brevity, we'll just do the action directly in this example. 29 | x := req.A + req.B 30 | if x < req.A { 31 | return nil, errors.New("overflow") 32 | } 33 | r := &addResult{ 34 | X: x, 35 | } 36 | return r, nil 37 | } 38 | 39 | // The second argument to Register can be any http.Handler. To keep 40 | // state in the helper between calls, you can create a custom type and 41 | // delegate to methods based on http.Request.URL.Path. 42 | var addHelper = helpers.Register("add", httpjson.ServePOST(add)) 43 | 44 | func name_me_TestAdd(t *testing.T) { 45 | ctx, cancel := context.WithCancel(context.Background()) 46 | defer cancel() 47 | 48 | control := addHelper.Spawn(ctx, t) 49 | defer control.Close() 50 | 51 | var got addResult 52 | if err := control.JSON("/").Call(ctx, addRequest{A: 42, B: 13}, &got); err != nil { 53 | t.Fatalf("calling helper: %v", err) 54 | } 55 | if g, e := got.X, uint64(55); g != e { 56 | t.Errorf("wrong add result: %v != %v", g, e) 57 | } 58 | } 59 | 60 | func name_me_TestMain(m *testing.M) { 61 | helpers.AddFlag(flag.CommandLine) 62 | flag.Parse() 63 | helpers.RunIfNeeded() 64 | os.Exit(m.Run()) 65 | } 66 | 67 | func Example() {} 68 | 69 | // Quiet linters. See https://github.com/dominikh/go-tools/issues/675 70 | var _ = name_me_TestAdd 71 | var _ = name_me_TestMain 72 | -------------------------------------------------------------------------------- /fs/fstestutil/spawntest/httpjson/client.go: -------------------------------------------------------------------------------- 1 | package httpjson 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | ) 12 | 13 | // JSON helps make a HTTP request to the resource tree at url with 14 | // JSON request and response bodies. 15 | // 16 | // If client is nil http.DefaultClient will be used. 17 | func JSON(client *http.Client, url string) *Resource { 18 | if client == nil { 19 | client = http.DefaultClient 20 | } 21 | return &Resource{ 22 | http: client, 23 | url: url, 24 | } 25 | } 26 | 27 | // Resource represents a JSON-speaking remote HTTP resource. 28 | type Resource struct { 29 | http *http.Client 30 | url string 31 | } 32 | 33 | // Call a HTTP resource that is expected to return JSON data. 34 | // 35 | // If data is not nil, method is POST and the request body is data 36 | // marshaled into JSON. If data is nil, method is GET. 37 | // 38 | // The response JSON is unmarshaled into dst. 39 | func (c *Resource) Call(ctx context.Context, data interface{}, dst interface{}) error { 40 | method := "GET" 41 | var body io.Reader 42 | if data != nil { 43 | buf, err := json.Marshal(data) 44 | if err != nil { 45 | return err 46 | } 47 | method = "POST" 48 | body = bytes.NewReader(buf) 49 | } 50 | req, err := http.NewRequestWithContext(ctx, method, c.url, body) 51 | if err != nil { 52 | return err 53 | } 54 | if method != "GET" { 55 | req.Header.Set("Content-Type", "application/json") 56 | } 57 | req.Header.Set("Accept", "application/json") 58 | resp, err := c.http.Do(req) 59 | if err != nil { 60 | return err 61 | } 62 | defer resp.Body.Close() 63 | 64 | if resp.StatusCode != http.StatusOK { 65 | buf, err := ioutil.ReadAll(resp.Body) 66 | if err != nil { 67 | buf = []byte("(cannot read error body: " + err.Error() + ")") 68 | } 69 | return fmt.Errorf("http error: %v: %q", resp.Status, bytes.TrimSpace(buf)) 70 | } 71 | 72 | dec := json.NewDecoder(resp.Body) 73 | // add options to func JSON to disable this, if need is strong 74 | // enough; at that point might change api to just take nothing but 75 | // options, use default for client etc. 76 | dec.DisallowUnknownFields() 77 | 78 | if err := dec.Decode(dst); err != nil { 79 | return err 80 | } 81 | if err := mustEOF(dec); err != nil { 82 | return err 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /fs/fstestutil/spawntest/httpjson/doc.go: -------------------------------------------------------------------------------- 1 | // Package httpjson helps transporting JSON over HTTP. 2 | // 3 | // This might get extracted to a standalone repository, if it proves 4 | // useful enough. 5 | package httpjson 6 | -------------------------------------------------------------------------------- /fs/fstestutil/spawntest/httpjson/musteof.go: -------------------------------------------------------------------------------- 1 | package httpjson 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // TrailingDataError is an error that is returned if there is trailing 10 | // data after a JSON message. 11 | type TrailingDataError struct { 12 | Token json.Token 13 | } 14 | 15 | func (t *TrailingDataError) Error() string { 16 | return fmt.Sprintf("invalid character %q after top-level value", t.Token) 17 | } 18 | 19 | func mustEOF(dec *json.Decoder) error { 20 | token, err := dec.Token() 21 | switch err { 22 | case io.EOF: 23 | // expected 24 | return nil 25 | case nil: 26 | return &TrailingDataError{Token: token} 27 | default: 28 | return err 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /fs/fstestutil/spawntest/httpjson/server.go: -------------------------------------------------------------------------------- 1 | package httpjson 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | ) 10 | 11 | // TODO ServeGET 12 | 13 | // ServePOST adapts a function to a http.Handler with easy JSON 14 | // unmarshal & marshal and error reporting. 15 | // 16 | // fn is expected to be of the form 17 | // 18 | // func(context.Context, T1) (T2, error) 19 | // 20 | // or similar with pointers to T1 or T2. 21 | func ServePOST(fn interface{}) http.Handler { 22 | val := reflect.ValueOf(fn) 23 | if val.Kind() != reflect.Func { 24 | panic("JSONHandler was passed a value that is not a function") 25 | } 26 | typ := val.Type() 27 | if typ.NumIn() != 2 { 28 | panic("JSONHandler function must take two arguments") 29 | } 30 | if typ.In(0) != reflect.TypeOf((*context.Context)(nil)).Elem() { 31 | panic("JSONHandler function must take context") 32 | } 33 | if typ.NumOut() != 2 { 34 | panic("JSONHandler function must return two values") 35 | } 36 | if typ.Out(1) != reflect.TypeOf((*error)(nil)).Elem() { 37 | panic("JSONHandler function must return error") 38 | } 39 | 40 | return jsonPOST{ 41 | fnVal: val, 42 | argType: typ.In(1), 43 | retType: typ.Out(0), 44 | } 45 | } 46 | 47 | type jsonPOST struct { 48 | fnVal reflect.Value 49 | argType reflect.Type 50 | retType reflect.Type 51 | } 52 | 53 | var _ http.Handler = jsonPOST{} 54 | 55 | func (j jsonPOST) ServeHTTP(w http.ResponseWriter, req *http.Request) { 56 | if req.Method != "POST" { 57 | w.Header().Set("Allow", "POST") 58 | const code = http.StatusMethodNotAllowed 59 | http.Error(w, http.StatusText(code), code) 60 | return 61 | } 62 | 63 | // TODO do we want to enforce request Content-Type 64 | 65 | // TODO do we want to enforce request Accept 66 | 67 | argVal := reflect.New(j.argType) 68 | arg := argVal.Interface() 69 | dec := json.NewDecoder(req.Body) 70 | dec.DisallowUnknownFields() 71 | if err := dec.Decode(arg); err != nil { 72 | msg := fmt.Sprintf("cannot unmarshal request body as json: %v", err) 73 | http.Error(w, msg, http.StatusBadRequest) 74 | return 75 | } 76 | if err := mustEOF(dec); err != nil { 77 | msg := fmt.Sprintf("cannot unmarshal request body as json: %v", err) 78 | http.Error(w, msg, http.StatusBadRequest) 79 | return 80 | } 81 | 82 | ret := j.fnVal.Call([]reflect.Value{ 83 | reflect.ValueOf(req.Context()), 84 | argVal.Elem(), 85 | }) 86 | 87 | // TODO allow bad request etc status codes 88 | errI := ret[1].Interface() 89 | if errI != nil { 90 | if err := errI.(error); err != nil { 91 | http.Error(w, err.Error(), http.StatusInternalServerError) 92 | return 93 | } 94 | } 95 | 96 | data := ret[0].Interface() 97 | buf, err := json.Marshal(data) 98 | if err != nil { 99 | http.Error(w, err.Error(), http.StatusInternalServerError) 100 | return 101 | } 102 | w.Header().Set("Content-Type", "application/json") 103 | if _, err := w.Write(buf); err != nil { 104 | panic(http.ErrAbortHandler) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /fs/fstestutil/spawntest/spawntest.go: -------------------------------------------------------------------------------- 1 | // Package spawntest helps write tests that use subprocesses. 2 | // 3 | // The subprocess runs a HTTP server on a UNIX domain socket, and the 4 | // test can make HTTP requests to control the behavior of the helper 5 | // subprocess. 6 | // 7 | // Helpers are identified by names they pass to Registry.Register. 8 | // This call should be placed in an init function. The test spawns the 9 | // subprocess by executing the same test binary in a subprocess, 10 | // passing it a special flag that is recognized by TestMain. 11 | // 12 | // This might get extracted to a standalone repository, if it proves 13 | // useful enough. 14 | package spawntest 15 | 16 | import ( 17 | "context" 18 | "errors" 19 | "flag" 20 | "io/ioutil" 21 | "log" 22 | "net" 23 | "net/http" 24 | "net/url" 25 | "os" 26 | "os/exec" 27 | "path/filepath" 28 | "sync" 29 | "testing" 30 | 31 | "github.com/tv42/httpunix" 32 | 33 | "github.com/anacrolix/fuse/fs/fstestutil/spawntest/httpjson" 34 | ) 35 | 36 | // Registry keeps track of helpers. 37 | // 38 | // The zero value is ready to use. 39 | type Registry struct { 40 | mu sync.Mutex 41 | helpers map[string]http.Handler 42 | runName string 43 | runHandler http.Handler 44 | } 45 | 46 | // Register a helper in the registry. 47 | // 48 | // This should be called from a top-level variable assignment. 49 | // 50 | // Register will panic if the name is already registered. 51 | func (r *Registry) Register(name string, h http.Handler) *Helper { 52 | r.mu.Lock() 53 | defer r.mu.Unlock() 54 | if r.helpers == nil { 55 | r.helpers = make(map[string]http.Handler) 56 | } 57 | if _, seen := r.helpers[name]; seen { 58 | panic("spawntest: helper already registered: " + name) 59 | } 60 | r.helpers[name] = h 61 | hh := &Helper{ 62 | name: name, 63 | } 64 | return hh 65 | } 66 | 67 | type helperFlag struct { 68 | r *Registry 69 | } 70 | 71 | var _ flag.Value = helperFlag{} 72 | 73 | func (hf helperFlag) String() string { 74 | if hf.r == nil { 75 | return "" 76 | } 77 | return hf.r.runName 78 | } 79 | 80 | func (hf helperFlag) Set(s string) error { 81 | h, ok := hf.r.helpers[s] 82 | if !ok { 83 | return errors.New("helper not found") 84 | } 85 | hf.r.runName = s 86 | hf.r.runHandler = h 87 | return nil 88 | } 89 | 90 | const flagName = "spawntest.internal.helper" 91 | 92 | // AddFlag adds the command-line flag used to communicate between 93 | // Control and the helper to the flag set. Typically flag.CommandLine 94 | // is used, and this should be called from TestMain before flag.Parse. 95 | func (r *Registry) AddFlag(f *flag.FlagSet) { 96 | v := helperFlag{r: r} 97 | f.Var(v, flagName, "internal use only") 98 | } 99 | 100 | // RunIfNeeded passes execution to the helper if the right 101 | // command-line flag was seen. This should be called from TestMain 102 | // after flag.Parse. If running as the helper, the call will not 103 | // return. 104 | func (r *Registry) RunIfNeeded() { 105 | h := r.runHandler 106 | if h == nil { 107 | return 108 | } 109 | f := os.NewFile(3, "") 110 | l, err := net.FileListener(f) 111 | if err != nil { 112 | log.Fatalf("cannot listen: %v", err) 113 | } 114 | if err := http.Serve(l, h); err != nil { 115 | log.Fatalf("http server error: %v", err) 116 | } 117 | os.Exit(0) 118 | } 119 | 120 | // Helper is the result of registering a helper. It can be used by 121 | // tests to spawn the helper. 122 | type Helper struct { 123 | name string 124 | } 125 | 126 | type transportWithBase struct { 127 | Base *url.URL 128 | Transport http.RoundTripper 129 | } 130 | 131 | var _ http.RoundTripper = (*transportWithBase)(nil) 132 | 133 | func (t *transportWithBase) RoundTrip(req *http.Request) (*http.Response, error) { 134 | ctx := req.Context() 135 | req = req.Clone(ctx) 136 | req.URL = t.Base.ResolveReference(req.URL) 137 | return t.Transport.RoundTrip(req) 138 | } 139 | 140 | func makeHTTPClient(path string) *http.Client { 141 | u := &httpunix.Transport{} 142 | const loc = "helper" 143 | u.RegisterLocation(loc, path) 144 | t := &transportWithBase{ 145 | Base: &url.URL{ 146 | Scheme: httpunix.Scheme, 147 | Host: loc, 148 | }, 149 | Transport: u, 150 | } 151 | client := &http.Client{ 152 | Transport: t, 153 | } 154 | return client 155 | } 156 | 157 | // Spawn the helper. All errors will be reported via t.Logf and fatal 158 | // errors result in t.FailNow. The helper is killed after context 159 | // cancels. 160 | func (h *Helper) Spawn(ctx context.Context, t testing.TB) *Control { 161 | executable, err := os.Executable() 162 | if err != nil { 163 | t.Fatalf("spawntest: cannot find our executable: %v", err) 164 | } 165 | 166 | // could use TB.TempDir() 167 | // https://github.com/golang/go/issues/35998 168 | dir, err := ioutil.TempDir("", "spawntest") 169 | if err != nil { 170 | t.Fatalf("spawnmount.Spawn: cannot make temp dir: %v", err) 171 | } 172 | defer func() { 173 | if dir != "" { 174 | if err := os.RemoveAll(dir); err != nil { 175 | t.Logf("error cleaning temp dir: %v", err) 176 | } 177 | } 178 | }() 179 | 180 | controlPath := filepath.Join(dir, "control") 181 | l, err := net.ListenUnix("unix", &net.UnixAddr{Name: controlPath, Net: "unix"}) 182 | if err != nil { 183 | t.Fatalf("cannot open listener: %v", err) 184 | } 185 | l.SetUnlinkOnClose(false) 186 | defer l.Close() 187 | 188 | lf, err := l.File() 189 | if err != nil { 190 | t.Fatalf("cannot get FD from listener: %v", err) 191 | } 192 | defer lf.Close() 193 | 194 | cmd := exec.Command(executable, "-"+flagName+"="+h.name) 195 | cmd.Stdout = os.Stdout 196 | cmd.Stderr = os.Stderr 197 | cmd.ExtraFiles = []*os.File{lf} 198 | 199 | if err := cmd.Start(); err != nil { 200 | t.Fatalf("spawntest: cannot start helper: %v", err) 201 | } 202 | defer func() { 203 | if cmd != nil { 204 | if err := cmd.Process.Kill(); err != nil { 205 | t.Logf("error killing spawned helper: %v", err) 206 | } 207 | } 208 | }() 209 | 210 | c := &Control{ 211 | t: t, 212 | dir: dir, 213 | cmd: cmd, 214 | http: makeHTTPClient(controlPath), 215 | } 216 | dir = "" 217 | cmd = nil 218 | return c 219 | } 220 | 221 | // Control an instance of a helper running as a subprocess. 222 | type Control struct { 223 | t testing.TB 224 | dir string 225 | cmd *exec.Cmd 226 | http *http.Client 227 | } 228 | 229 | // Close kills the helper and frees resources. 230 | func (c *Control) Close() { 231 | if c.cmd.ProcessState == nil { 232 | // not yet Waited on 233 | c.cmd.Process.Kill() 234 | _ = c.cmd.Wait() 235 | } 236 | 237 | if c.dir != "" { 238 | if err := os.RemoveAll(c.dir); err != nil { 239 | c.t.Logf("error cleaning temp dir: %v", err) 240 | } 241 | c.dir = "" 242 | } 243 | } 244 | 245 | // Signal send a signal to the helper process. 246 | func (c *Control) Signal(sig os.Signal) error { 247 | return c.cmd.Process.Signal(sig) 248 | } 249 | 250 | // HTTP returns a HTTP client that can be used to communicate with the 251 | // helper. URLs passed to this helper should not include scheme or 252 | // host. 253 | func (c *Control) HTTP() *http.Client { 254 | return c.http 255 | } 256 | 257 | // JSON returns a helper to make HTTP requests that pass data as JSON 258 | // to the resource identified by path. Path should not include scheme 259 | // or host. Path can be empty to communicate with the root resource. 260 | func (c *Control) JSON(path string) *httpjson.Resource { 261 | return httpjson.JSON(c.http, path) 262 | } 263 | -------------------------------------------------------------------------------- /fs/fstestutil/testfs.go: -------------------------------------------------------------------------------- 1 | package fstestutil 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "syscall" 7 | 8 | "github.com/anacrolix/fuse" 9 | "github.com/anacrolix/fuse/fs" 10 | ) 11 | 12 | // SimpleFS is a trivial FS that just implements the Root method. 13 | type SimpleFS struct { 14 | Node fs.Node 15 | } 16 | 17 | var _ fs.FS = SimpleFS{} 18 | 19 | func (f SimpleFS) Root() (fs.Node, error) { 20 | return f.Node, nil 21 | } 22 | 23 | // File can be embedded in a struct to make it look like a file. 24 | type File struct{} 25 | 26 | func (f File) Attr(ctx context.Context, a *fuse.Attr) error { 27 | a.Mode = 0o666 28 | return nil 29 | } 30 | 31 | // Dir can be embedded in a struct to make it look like a directory. 32 | type Dir struct{} 33 | 34 | func (f Dir) Attr(ctx context.Context, a *fuse.Attr) error { 35 | a.Mode = os.ModeDir | 0o777 36 | return nil 37 | } 38 | 39 | // ChildMap is a directory with child nodes looked up from a map. 40 | type ChildMap map[string]fs.Node 41 | 42 | var _ fs.Node = (*ChildMap)(nil) 43 | var _ fs.NodeStringLookuper = (*ChildMap)(nil) 44 | 45 | func (f *ChildMap) Attr(ctx context.Context, a *fuse.Attr) error { 46 | a.Mode = os.ModeDir | 0o777 47 | return nil 48 | } 49 | 50 | func (f *ChildMap) Lookup(ctx context.Context, name string) (fs.Node, error) { 51 | child, ok := (*f)[name] 52 | if !ok { 53 | return nil, syscall.ENOENT 54 | } 55 | return child, nil 56 | } 57 | -------------------------------------------------------------------------------- /fs/helpers_test.go: -------------------------------------------------------------------------------- 1 | package fs_test 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestMain(m *testing.M) { 10 | helpers.AddFlag(flag.CommandLine) 11 | flag.Parse() 12 | helpers.RunIfNeeded() 13 | os.Exit(m.Run()) 14 | } 15 | -------------------------------------------------------------------------------- /fs/serve_darwin_test.go: -------------------------------------------------------------------------------- 1 | package fs_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "syscall" 9 | "testing" 10 | 11 | "golang.org/x/sys/unix" 12 | 13 | "github.com/anacrolix/fuse/fs/fstestutil" 14 | "github.com/anacrolix/fuse/fs/fstestutil/spawntest/httpjson" 15 | ) 16 | 17 | func platformStatfs(st *syscall.Statfs_t) *statfsResult { 18 | return &statfsResult{ 19 | Blocks: st.Blocks, 20 | Bfree: st.Bfree, 21 | Bavail: st.Bavail, 22 | Files: st.Files, 23 | Ffree: st.Ffree, 24 | Bsize: int64(st.Iosize), 25 | Namelen: 0, 26 | Frsize: 0, 27 | } 28 | } 29 | 30 | func platformStat(fi os.FileInfo) *statResult { 31 | r := &statResult{ 32 | Mode: fi.Mode(), 33 | } 34 | st := fi.Sys().(*syscall.Stat_t) 35 | r.Ino = st.Ino 36 | r.Nlink = uint64(st.Nlink) 37 | r.UID = st.Uid 38 | r.GID = st.Gid 39 | r.Blksize = int64(st.Blksize) 40 | return r 41 | } 42 | 43 | type exchangeData struct { 44 | fstestutil.File 45 | // this struct cannot be zero size or multiple instances may look identical 46 | _ int 47 | } 48 | 49 | type exchangedataRequest struct { 50 | Path1 string 51 | Path2 string 52 | Options int 53 | WantErrno syscall.Errno 54 | } 55 | 56 | func doExchange(ctx context.Context, req exchangedataRequest) (*struct{}, error) { 57 | if err := unix.Exchangedata(req.Path1, req.Path2, req.Options); !errors.Is(err, req.WantErrno) { 58 | return nil, fmt.Errorf("from error from exchangedata: %v", err) 59 | } 60 | return &struct{}{}, nil 61 | } 62 | 63 | var exchangeHelper = helpers.Register("exchange", httpjson.ServePOST(doExchange)) 64 | 65 | func TestExchangeDataNotSupported(t *testing.T) { 66 | maybeParallel(t) 67 | ctx, cancel := context.WithCancel(context.Background()) 68 | defer cancel() 69 | mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{&fstestutil.ChildMap{ 70 | "one": &exchangeData{}, 71 | "two": &exchangeData{}, 72 | }}, nil) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | defer mnt.Close() 77 | control := exchangeHelper.Spawn(ctx, t) 78 | defer control.Close() 79 | 80 | req := exchangedataRequest{ 81 | Path1: mnt.Dir + "/one", 82 | Path2: mnt.Dir + "/two", 83 | Options: 0, 84 | WantErrno: syscall.ENOTSUP, 85 | } 86 | var nothing struct{} 87 | if err := control.JSON("/").Call(ctx, req, ¬hing); err != nil { 88 | t.Fatalf("calling helper: %v", err) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /fs/serve_freebsd_test.go: -------------------------------------------------------------------------------- 1 | package fs_test 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | ) 7 | 8 | func platformStatfs(st *syscall.Statfs_t) *statfsResult { 9 | return &statfsResult{ 10 | Blocks: st.Blocks, 11 | Bfree: st.Bfree, 12 | Bavail: uint64(st.Bavail), 13 | Files: st.Files, 14 | Ffree: uint64(st.Ffree), 15 | Bsize: int64(st.Iosize), 16 | Namelen: int64(st.Namemax), 17 | Frsize: int64(st.Bsize), 18 | } 19 | } 20 | 21 | func platformStat(fi os.FileInfo) *statResult { 22 | r := &statResult{ 23 | Mode: fi.Mode(), 24 | } 25 | st := fi.Sys().(*syscall.Stat_t) 26 | r.Ino = st.Ino 27 | r.Nlink = st.Nlink 28 | r.UID = st.Uid 29 | r.GID = st.Gid 30 | r.Blksize = int64(st.Blksize) 31 | return r 32 | } 33 | -------------------------------------------------------------------------------- /fs/serve_linux_test.go: -------------------------------------------------------------------------------- 1 | package fs_test 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "syscall" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | func platformStatfs(st *syscall.Statfs_t) *statfsResult { 12 | return &statfsResult{ 13 | Blocks: st.Blocks, 14 | Bfree: st.Bfree, 15 | Bavail: st.Bavail, 16 | Files: st.Files, 17 | Ffree: st.Ffree, 18 | Bsize: int64(st.Bsize), 19 | Namelen: int64(st.Namelen), 20 | Frsize: int64(st.Frsize), 21 | } 22 | } 23 | 24 | func platformStat(fi os.FileInfo) *statResult { 25 | r := &statResult{ 26 | Mode: fi.Mode(), 27 | } 28 | st := fi.Sys().(*syscall.Stat_t) 29 | r.Ino = st.Ino 30 | r.Nlink = uint64(st.Nlink) 31 | r.UID = st.Uid 32 | r.GID = st.Gid 33 | r.Blksize = int64(st.Blksize) 34 | return r 35 | } 36 | 37 | var _lockOFDHelper = helpers.Register("lock-ofd", &lockHelp{ 38 | lockFn: func(fd uintptr, req *lockReq) error { 39 | lk := unix.Flock_t{ 40 | Type: unix.F_WRLCK, 41 | Whence: int16(io.SeekStart), 42 | Start: req.Start, 43 | Len: req.Len, 44 | } 45 | cmd := unix.F_OFD_SETLK 46 | if req.Wait { 47 | cmd = unix.F_OFD_SETLKW 48 | } 49 | return unix.FcntlFlock(fd, cmd, &lk) 50 | }, 51 | unlockFn: func(fd uintptr, req *lockReq) error { 52 | lk := unix.Flock_t{ 53 | Type: unix.F_UNLCK, 54 | Whence: int16(io.SeekStart), 55 | Start: req.Start, 56 | Len: req.Len, 57 | } 58 | cmd := unix.F_OFD_SETLK 59 | if req.Wait { 60 | cmd = unix.F_OFD_SETLKW 61 | } 62 | return unix.FcntlFlock(fd, cmd, &lk) 63 | }, 64 | queryFn: func(fd uintptr, lk *unix.Flock_t) error { 65 | cmd := unix.F_OFD_GETLK 66 | return unix.FcntlFlock(fd, cmd, lk) 67 | }, 68 | }) 69 | 70 | func init() { 71 | lockOFDHelper = _lockOFDHelper 72 | } 73 | -------------------------------------------------------------------------------- /fs/tree.go: -------------------------------------------------------------------------------- 1 | // FUSE directory tree, for servers that wish to use it with the service loop. 2 | 3 | package fs 4 | 5 | import ( 6 | "context" 7 | "os" 8 | pathpkg "path" 9 | "strings" 10 | "syscall" 11 | 12 | "github.com/anacrolix/fuse" 13 | ) 14 | 15 | // A Tree implements a basic read-only directory tree for FUSE. 16 | // The Nodes contained in it may still be writable. 17 | type Tree struct { 18 | tree 19 | } 20 | 21 | func (t *Tree) Root() (Node, error) { 22 | return &t.tree, nil 23 | } 24 | 25 | // Add adds the path to the tree, resolving to the given node. 26 | // If path or a prefix of path has already been added to the tree, 27 | // Add panics. 28 | // 29 | // Add is only safe to call before starting to serve requests. 30 | func (t *Tree) Add(path string, node Node) { 31 | path = pathpkg.Clean("/" + path)[1:] 32 | elems := strings.Split(path, "/") 33 | dir := Node(&t.tree) 34 | for i, elem := range elems { 35 | dt, ok := dir.(*tree) 36 | if !ok { 37 | panic("fuse: Tree.Add for " + strings.Join(elems[:i], "/") + " and " + path) 38 | } 39 | n := dt.lookup(elem) 40 | if n != nil { 41 | if i+1 == len(elems) { 42 | panic("fuse: Tree.Add for " + path + " conflicts with " + elem) 43 | } 44 | dir = n 45 | } else { 46 | if i+1 == len(elems) { 47 | dt.add(elem, node) 48 | } else { 49 | dir = &tree{} 50 | dt.add(elem, dir) 51 | } 52 | } 53 | } 54 | } 55 | 56 | type treeDir struct { 57 | name string 58 | node Node 59 | } 60 | 61 | type tree struct { 62 | dir []treeDir 63 | } 64 | 65 | func (t *tree) lookup(name string) Node { 66 | for _, d := range t.dir { 67 | if d.name == name { 68 | return d.node 69 | } 70 | } 71 | return nil 72 | } 73 | 74 | func (t *tree) add(name string, n Node) { 75 | t.dir = append(t.dir, treeDir{name, n}) 76 | } 77 | 78 | func (t *tree) Attr(ctx context.Context, a *fuse.Attr) error { 79 | a.Mode = os.ModeDir | 0o555 80 | return nil 81 | } 82 | 83 | func (t *tree) Lookup(ctx context.Context, name string) (Node, error) { 84 | n := t.lookup(name) 85 | if n != nil { 86 | return n, nil 87 | } 88 | return nil, syscall.ENOENT 89 | } 90 | 91 | func (t *tree) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { 92 | var out []fuse.Dirent 93 | for _, d := range t.dir { 94 | out = append(out, fuse.Dirent{Name: d.name}) 95 | } 96 | return out, nil 97 | } 98 | -------------------------------------------------------------------------------- /fuse_darwin.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | // Maximum file write size we are prepared to receive from the kernel. 4 | // 5 | // This value has to be >=16MB or OSXFUSE (3.4.0 observed) will 6 | // forcibly close the /dev/fuse file descriptor on a Setxattr with a 7 | // 16MB value. See TestSetxattr16MB and 8 | // https://github.com/bazil/fuse/issues/42 9 | const maxWrite = 16 * 1024 * 1024 10 | -------------------------------------------------------------------------------- /fuse_freebsd.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | // Maximum file write size we are prepared to receive from the kernel. 4 | // 5 | // This number is just a guess. 6 | const maxWrite = 128 * 1024 7 | -------------------------------------------------------------------------------- /fuse_kernel_darwin.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type attr struct { 8 | Ino uint64 9 | Size uint64 10 | Blocks uint64 11 | Atime uint64 12 | Mtime uint64 13 | Ctime uint64 14 | Crtime_ uint64 // OS X only 15 | AtimeNsec uint32 16 | MtimeNsec uint32 17 | CtimeNsec uint32 18 | CrtimeNsec uint32 // OS X only 19 | Mode uint32 20 | Nlink uint32 21 | Uid uint32 22 | Gid uint32 23 | Rdev uint32 24 | Flags_ uint32 // OS X only; see chflags(2) 25 | Blksize uint32 26 | padding uint32 27 | } 28 | 29 | func (a *attr) SetCrtime(s uint64, ns uint32) { 30 | a.Crtime_, a.CrtimeNsec = s, ns 31 | } 32 | 33 | func (a *attr) SetFlags(f uint32) { 34 | a.Flags_ = f 35 | } 36 | 37 | type setattrIn struct { 38 | setattrInCommon 39 | 40 | // OS X only 41 | Bkuptime_ uint64 42 | Chgtime_ uint64 43 | Crtime uint64 44 | BkuptimeNsec uint32 45 | ChgtimeNsec uint32 46 | CrtimeNsec uint32 47 | Flags_ uint32 // see chflags(2) 48 | } 49 | 50 | func (in *setattrIn) BkupTime() time.Time { 51 | return time.Unix(int64(in.Bkuptime_), int64(in.BkuptimeNsec)) 52 | } 53 | 54 | func (in *setattrIn) Chgtime() time.Time { 55 | return time.Unix(int64(in.Chgtime_), int64(in.ChgtimeNsec)) 56 | } 57 | 58 | func (in *setattrIn) Flags() uint32 { 59 | return in.Flags_ 60 | } 61 | 62 | func openFlags(flags uint32) OpenFlags { 63 | return OpenFlags(flags) 64 | } 65 | 66 | type getxattrIn struct { 67 | getxattrInCommon 68 | 69 | // OS X only 70 | Position uint32 71 | Padding uint32 72 | } 73 | 74 | func (g *getxattrIn) position() uint32 { 75 | return g.Position 76 | } 77 | 78 | type setxattrIn struct { 79 | setxattrInCommon 80 | 81 | // OS X only 82 | Position uint32 83 | Padding uint32 84 | } 85 | 86 | func (s *setxattrIn) position() uint32 { 87 | return s.Position 88 | } 89 | -------------------------------------------------------------------------------- /fuse_kernel_freebsd.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import "time" 4 | 5 | type attr struct { 6 | Ino uint64 7 | Size uint64 8 | Blocks uint64 9 | Atime uint64 10 | Mtime uint64 11 | Ctime uint64 12 | AtimeNsec uint32 13 | MtimeNsec uint32 14 | CtimeNsec uint32 15 | Mode uint32 16 | Nlink uint32 17 | Uid uint32 18 | Gid uint32 19 | Rdev uint32 20 | Blksize uint32 21 | padding uint32 22 | } 23 | 24 | func (a *attr) Crtime() time.Time { 25 | return time.Time{} 26 | } 27 | 28 | func (a *attr) SetCrtime(s uint64, ns uint32) { 29 | // ignored on freebsd 30 | } 31 | 32 | func (a *attr) SetFlags(f uint32) { 33 | // ignored on freebsd 34 | } 35 | 36 | type setattrIn struct { 37 | setattrInCommon 38 | } 39 | 40 | func (in *setattrIn) BkupTime() time.Time { 41 | return time.Time{} 42 | } 43 | 44 | func (in *setattrIn) Chgtime() time.Time { 45 | return time.Time{} 46 | } 47 | 48 | func (in *setattrIn) Flags() uint32 { 49 | return 0 50 | } 51 | 52 | func openFlags(flags uint32) OpenFlags { 53 | return OpenFlags(flags) 54 | } 55 | 56 | type getxattrIn struct { 57 | getxattrInCommon 58 | } 59 | 60 | type setxattrIn struct { 61 | setxattrInCommon 62 | } 63 | -------------------------------------------------------------------------------- /fuse_kernel_linux.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import "time" 4 | 5 | type attr struct { 6 | Ino uint64 7 | Size uint64 8 | Blocks uint64 9 | Atime uint64 10 | Mtime uint64 11 | Ctime uint64 12 | AtimeNsec uint32 13 | MtimeNsec uint32 14 | CtimeNsec uint32 15 | Mode uint32 16 | Nlink uint32 17 | Uid uint32 18 | Gid uint32 19 | Rdev uint32 20 | Blksize uint32 21 | _ uint32 22 | } 23 | 24 | func (a *attr) Crtime() time.Time { 25 | return time.Time{} 26 | } 27 | 28 | func (a *attr) SetCrtime(s uint64, ns uint32) { 29 | // Ignored on Linux. 30 | } 31 | 32 | func (a *attr) SetFlags(f uint32) { 33 | // Ignored on Linux. 34 | } 35 | 36 | type setattrIn struct { 37 | setattrInCommon 38 | } 39 | 40 | func (in *setattrIn) BkupTime() time.Time { 41 | return time.Time{} 42 | } 43 | 44 | func (in *setattrIn) Chgtime() time.Time { 45 | return time.Time{} 46 | } 47 | 48 | func (in *setattrIn) Flags() uint32 { 49 | return 0 50 | } 51 | 52 | func openFlags(flags uint32) OpenFlags { 53 | // on amd64, the 32-bit O_LARGEFILE flag is always seen; 54 | // on i386, the flag probably depends on the app 55 | // requesting, but in any case should be utterly 56 | // uninteresting to us here; our kernel protocol messages 57 | // are not directly related to the client app's kernel 58 | // API/ABI 59 | flags &^= 0x8000 60 | 61 | return OpenFlags(flags) 62 | } 63 | 64 | type getxattrIn struct { 65 | getxattrInCommon 66 | } 67 | 68 | type setxattrIn struct { 69 | setxattrInCommon 70 | } 71 | -------------------------------------------------------------------------------- /fuse_kernel_std.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | -------------------------------------------------------------------------------- /fuse_kernel_test.go: -------------------------------------------------------------------------------- 1 | package fuse_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/anacrolix/fuse" 8 | ) 9 | 10 | func TestOpenFlagsAccmodeMaskReadWrite(t *testing.T) { 11 | var f = fuse.OpenFlags(os.O_RDWR | os.O_SYNC) 12 | if g, e := f&fuse.OpenAccessModeMask, fuse.OpenReadWrite; g != e { 13 | t.Fatalf("OpenAccessModeMask behaves wrong: %v: %o != %o", f, g, e) 14 | } 15 | if f.IsReadOnly() { 16 | t.Fatalf("IsReadOnly is wrong: %v", f) 17 | } 18 | if f.IsWriteOnly() { 19 | t.Fatalf("IsWriteOnly is wrong: %v", f) 20 | } 21 | if !f.IsReadWrite() { 22 | t.Fatalf("IsReadWrite is wrong: %v", f) 23 | } 24 | } 25 | 26 | func TestOpenFlagsAccmodeMaskReadOnly(t *testing.T) { 27 | var f = fuse.OpenFlags(os.O_RDONLY | os.O_SYNC) 28 | if g, e := f&fuse.OpenAccessModeMask, fuse.OpenReadOnly; g != e { 29 | t.Fatalf("OpenAccessModeMask behaves wrong: %v: %o != %o", f, g, e) 30 | } 31 | if !f.IsReadOnly() { 32 | t.Fatalf("IsReadOnly is wrong: %v", f) 33 | } 34 | if f.IsWriteOnly() { 35 | t.Fatalf("IsWriteOnly is wrong: %v", f) 36 | } 37 | if f.IsReadWrite() { 38 | t.Fatalf("IsReadWrite is wrong: %v", f) 39 | } 40 | } 41 | 42 | func TestOpenFlagsAccmodeMaskWriteOnly(t *testing.T) { 43 | var f = fuse.OpenFlags(os.O_WRONLY | os.O_SYNC) 44 | if g, e := f&fuse.OpenAccessModeMask, fuse.OpenWriteOnly; g != e { 45 | t.Fatalf("OpenAccessModeMask behaves wrong: %v: %o != %o", f, g, e) 46 | } 47 | if f.IsReadOnly() { 48 | t.Fatalf("IsReadOnly is wrong: %v", f) 49 | } 50 | if !f.IsWriteOnly() { 51 | t.Fatalf("IsWriteOnly is wrong: %v", f) 52 | } 53 | if f.IsReadWrite() { 54 | t.Fatalf("IsReadWrite is wrong: %v", f) 55 | } 56 | } 57 | 58 | func TestOpenFlagsString(t *testing.T) { 59 | var f = fuse.OpenFlags(os.O_RDWR | os.O_SYNC | os.O_APPEND) 60 | if g, e := f.String(), "OpenReadWrite+OpenAppend+OpenSync"; g != e { 61 | t.Fatalf("OpenFlags.String: %q != %q", g, e) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /fuse_linux.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | // Maximum file write size we are prepared to receive from the kernel. 4 | // 5 | // Linux 4.2.0 has been observed to cap this value at 128kB 6 | // (FUSE_MAX_PAGES_PER_REQ=32, 4kB pages). 7 | const maxWrite = 128 * 1024 8 | -------------------------------------------------------------------------------- /fuse_test.go: -------------------------------------------------------------------------------- 1 | package fuse_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "runtime" 7 | "testing" 8 | 9 | _ "github.com/anacrolix/envpprof" 10 | "github.com/anacrolix/fuse" 11 | ) 12 | 13 | func getFeatures(t *testing.T, opts ...fuse.MountOption) fuse.InitFlags { 14 | tmp, err := ioutil.TempDir("", "fusetest") 15 | if err != nil { 16 | t.Fatalf("error creating temp dir: %v", err) 17 | } 18 | defer func() { 19 | if err := os.RemoveAll(tmp); err != nil { 20 | t.Errorf("error cleaning temp dir: %v", err) 21 | } 22 | }() 23 | 24 | defer func() { 25 | err := fuse.Unmount(tmp) 26 | if err != nil { 27 | t.Logf("error unmounting: %v", err) 28 | } 29 | }() 30 | conn, err := fuse.Mount(tmp, opts...) 31 | if err != nil { 32 | t.Fatalf("error mounting: %v", err) 33 | } 34 | defer func() { 35 | if err := conn.Close(); err != nil { 36 | t.Errorf("error closing FUSE connection: %v", err) 37 | } 38 | }() 39 | 40 | return conn.Features() 41 | } 42 | 43 | func TestFeatures(t *testing.T) { 44 | run := func(name string, want, notwant fuse.InitFlags, opts ...fuse.MountOption) { 45 | t.Run(name, func(t *testing.T) { 46 | if runtime.GOOS == "freebsd" { 47 | if want&fuse.InitFlockLocks != 0 { 48 | t.Skip("FreeBSD FUSE does not implement Flock locks") 49 | } 50 | } 51 | got := getFeatures(t, opts...) 52 | t.Logf("features: %v", got) 53 | missing := want &^ got 54 | if missing != 0 { 55 | t.Errorf("missing: %v", missing) 56 | } 57 | extra := got & notwant 58 | if extra != 0 { 59 | t.Errorf("extra: %v", extra) 60 | } 61 | }) 62 | } 63 | 64 | run("Bare", 0, fuse.InitPOSIXLocks|fuse.InitFlockLocks) 65 | run("LockingFlock", fuse.InitFlockLocks, 0, fuse.LockingFlock()) 66 | run("LockingPOSIX", fuse.InitPOSIXLocks, 0, fuse.LockingPOSIX()) 67 | run("AsyncRead", fuse.InitAsyncRead, 0, fuse.AsyncRead()) 68 | run("WritebackCache", fuse.InitWritebackCache, 0, fuse.WritebackCache()) 69 | } 70 | -------------------------------------------------------------------------------- /fuseutil/fuseutil.go: -------------------------------------------------------------------------------- 1 | package fuseutil // import "github.com/anacrolix/fuse/fuseutil" 2 | 3 | import ( 4 | "github.com/anacrolix/fuse" 5 | ) 6 | 7 | // HandleRead handles a read request assuming that data is the entire file content. 8 | // It adjusts the amount returned in resp according to req.Offset and req.Size. 9 | func HandleRead(req *fuse.ReadRequest, resp *fuse.ReadResponse, data []byte) { 10 | if req.Offset >= int64(len(data)) { 11 | data = nil 12 | } else { 13 | data = data[req.Offset:] 14 | } 15 | if len(data) > req.Size { 16 | data = data[:req.Size] 17 | } 18 | n := copy(resp.Data[:req.Size], data) 19 | resp.Data = resp.Data[:n] 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/anacrolix/fuse 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/anacrolix/envpprof v1.3.0 7 | github.com/anacrolix/log v0.14.1 // indirect 8 | github.com/dvyukov/go-fuzz v0.0.0-20220726122315-1d375ef9f9f6 9 | github.com/elazarl/go-bindata-assetfs v1.0.0 // indirect 10 | github.com/stephens2424/writerset v1.0.2 // indirect 11 | github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c 12 | golang.org/x/sys v0.10.0 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= 2 | github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk= 3 | github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0= 4 | github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633 h1:TO3pytMIJ98CO1nYtqbFx/iuTHi4OgIUoE2wNfDdKxw= 5 | github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8= 6 | github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68= 7 | github.com/anacrolix/log v0.14.1 h1:j2FcIpYZ5FbANetUcm5JNu+zUBGADSp/VbjhUPrAY0k= 8 | github.com/anacrolix/log v0.14.1/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY= 9 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/dvyukov/go-fuzz v0.0.0-20220726122315-1d375ef9f9f6 h1:sE4tvxWw01v7K3MAHwKF2UF3xQbgy23PRURntuV1CkU= 14 | github.com/dvyukov/go-fuzz v0.0.0-20220726122315-1d375ef9f9f6/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= 15 | github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk= 16 | github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= 17 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= 18 | github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 19 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 20 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 21 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 22 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 23 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 24 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 25 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 26 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 27 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 28 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 29 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= 33 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 34 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 35 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 36 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 37 | github.com/stephens2424/writerset v1.0.2 h1:znRLgU6g8RS5euYRcy004XeE4W+Tu44kALzy7ghPif8= 38 | github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc= 39 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 41 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 42 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 43 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 44 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 45 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 46 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 47 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 48 | github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ= 49 | github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= 50 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 51 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 52 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 53 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 54 | golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4= 55 | golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 56 | golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 57 | golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4= 58 | golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 59 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 60 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 61 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 62 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 63 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 66 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 72 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 74 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 75 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 76 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 77 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 78 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 79 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 80 | golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 h1:0c3L82FDQ5rt1bjTBlchS8t6RQ6299/+5bWMnRLh+uI= 81 | golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 82 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 84 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 85 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 88 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 89 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 90 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 91 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 92 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 93 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 94 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | // Force init order here. 4 | 5 | func init() { 6 | initForcedBackend() 7 | } 8 | -------------------------------------------------------------------------------- /mount.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "log" 8 | "sync" 9 | ) 10 | 11 | var ( 12 | // ErrOSXFUSENotFound is returned from Mount when the OSXFUSE 13 | // installation is not detected. 14 | // 15 | // Only happens on OS X. Make sure OSXFUSE is installed, or see 16 | // OSXFUSELocations for customization. 17 | ErrOSXFUSENotFound = errors.New("cannot locate OSXFUSE") 18 | ) 19 | 20 | func neverIgnoreLine(line string) bool { 21 | return false 22 | } 23 | 24 | func lineLogger(wg *sync.WaitGroup, prefix string, ignore func(line string) bool, r io.ReadCloser) { 25 | defer wg.Done() 26 | 27 | scanner := bufio.NewScanner(r) 28 | for scanner.Scan() { 29 | line := scanner.Text() 30 | if ignore(line) { 31 | continue 32 | } 33 | log.Printf("%s: %s", prefix, line) 34 | } 35 | if err := scanner.Err(); err != nil { 36 | log.Printf("%s, error reading: %v", prefix, err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /mount_darwin.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strconv" 9 | "syscall" 10 | "unsafe" 11 | ) 12 | 13 | const FUSET_SRV_PATH = "/usr/local/bin/go-nfsv4" 14 | 15 | func fusetBinary() (string, error) { 16 | srv_path := os.Getenv("FUSE_NFSSRV_PATH") 17 | if srv_path == "" { 18 | srv_path = FUSET_SRV_PATH 19 | } 20 | 21 | if _, err := os.Stat(srv_path); err == nil { 22 | return srv_path, nil 23 | } 24 | 25 | return "", fmt.Errorf("FUSE-T not found") 26 | } 27 | 28 | func mountFuseT( 29 | bin string, 30 | mountPoint string, 31 | conf *mountConfig, 32 | ready chan<- struct{}, 33 | errp *error, 34 | ) (_ *os.File, _ fuseTBackendState, err error) { 35 | fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) 36 | if err != nil { 37 | return 38 | } 39 | local := fds[0] 40 | remote := fds[1] 41 | 42 | defer syscall.Close(remote) 43 | 44 | fds, err = syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) 45 | if err != nil { 46 | return 47 | } 48 | 49 | local_mon := fds[0] 50 | remote_mon := fds[1] 51 | 52 | defer syscall.Close(remote_mon) 53 | 54 | args := []string{} 55 | if conf.isReadonly() { 56 | args = append(args, "-r") 57 | } 58 | if conf.fsname() != "" { 59 | args = append(args, "--volname="+conf.fsname()) 60 | } 61 | // TODO: apply more args 62 | 63 | remote_file := os.NewFile(uintptr(remote), "") 64 | remote_mon_file := os.NewFile(uintptr(remote_mon), "") 65 | local_file := os.NewFile(uintptr(local), "") 66 | local_mon_file := os.NewFile(uintptr(local_mon), "") 67 | 68 | args = append(args, fmt.Sprintf("--rwsize=%d", maxWrite)) 69 | args = append(args, mountPoint) 70 | cmd := exec.Command(bin, args...) 71 | cmd.ExtraFiles = []*os.File{remote_file, remote_mon_file} // fd would be (index + 3) 72 | cmd.Stderr = nil 73 | cmd.Stdout = nil 74 | // daemonize 75 | cmd.SysProcAttr = &syscall.SysProcAttr{ 76 | Setsid: true, 77 | } 78 | 79 | envs := []string{} 80 | envs = append(envs, "_FUSE_COMMFD=3") 81 | envs = append(envs, "_FUSE_MONFD=4") 82 | envs = append(envs, "_FUSE_COMMVERS=2") 83 | cmd.Env = append(os.Environ(), envs...) 84 | 85 | syscall.CloseOnExec(local) 86 | syscall.CloseOnExec(local_mon) 87 | 88 | if err = cmd.Start(); err != nil { 89 | return 90 | } 91 | 92 | cmd.Process.Release() 93 | go func() { 94 | _, err := local_mon_file.Write([]byte("mount")) 95 | if err == nil { 96 | var reply [4]byte 97 | _, err = local_mon_file.Read(reply[:]) 98 | if err == nil && reply != [4]byte{} { 99 | err = fmt.Errorf("expected 4 zero bytes, got %v", reply) 100 | } 101 | } 102 | if err != nil { 103 | err = fmt.Errorf("fuse-t mount failed: %w", err) 104 | } 105 | *errp = err 106 | close(ready) 107 | }() 108 | 109 | return local_file, fuseTBackendState{ 110 | // Prevent these files from being GCed. 111 | extraFiles: []*os.File{ 112 | local_mon_file, 113 | }, 114 | }, err 115 | } 116 | 117 | func mount( 118 | mountPoint string, 119 | conf *mountConfig, 120 | ready chan<- struct{}, 121 | errp *error, 122 | ) ( 123 | f *os.File, 124 | be Backend, 125 | bes backendState, 126 | err error, 127 | ) { 128 | if forcedBackend.IsUnset() || forcedBackend.IsFuseT() { 129 | var fuseTBin string 130 | fuseTBin, err = fusetBinary() 131 | if err == nil { 132 | f, bes, err = mountFuseT(fuseTBin, mountPoint, conf, ready, errp) 133 | be = fuseTBackend 134 | return 135 | } 136 | if forcedBackend.IsFuseT() { 137 | return 138 | } 139 | } 140 | 141 | locations := conf.osxfuseLocations 142 | if locations == nil { 143 | locations = []OSXFUSEPaths{ 144 | OSXFUSELocationV4, 145 | OSXFUSELocationV3, 146 | } 147 | } 148 | 149 | var binLocation string 150 | for _, loc := range locations { 151 | if _, err := os.Stat(loc.Mount); os.IsNotExist(err) { 152 | // try the other locations 153 | continue 154 | } 155 | binLocation = loc.Mount 156 | break 157 | } 158 | if binLocation == "" { 159 | err = ErrOSXFUSENotFound 160 | return 161 | } 162 | 163 | local, remote, err := unixgramSocketpair() 164 | if err != nil { 165 | return 166 | } 167 | 168 | defer local.Close() 169 | defer remote.Close() 170 | 171 | cmd := exec.Command(binLocation, 172 | "-o", conf.getOptions(), 173 | 174 | // Tell osxfuse-kext how large our buffer is. It must split 175 | // writes larger than this into multiple writes. 176 | // 177 | // OSXFUSE seems to ignore InitResponse.MaxWrite, and uses 178 | // this instead. 179 | "-o", "iosize="+strconv.FormatUint(maxWrite, 10), 180 | 181 | mountPoint) 182 | 183 | cmd.ExtraFiles = []*os.File{remote} // fd would be (index + 3) 184 | cmd.Env = append(os.Environ(), 185 | "_FUSE_CALL_BY_LIB=", 186 | "_FUSE_DAEMON_PATH="+os.Args[0], 187 | "_FUSE_COMMFD=3", 188 | "_FUSE_COMMVERS=2", 189 | "MOUNT_OSXFUSE_CALL_BY_LIB=", 190 | "MOUNT_OSXFUSE_DAEMON_PATH="+os.Args[0]) 191 | 192 | var out, errOut bytes.Buffer 193 | cmd.Stdout = &out 194 | cmd.Stderr = &errOut 195 | 196 | if err = cmd.Start(); err != nil { 197 | return 198 | } 199 | 200 | fd, err := getConnection(local) 201 | if err != nil { 202 | return 203 | } 204 | 205 | go func() { 206 | // wait inside a goroutine, or otherwise it would block forever for unknown reasons 207 | if err := cmd.Wait(); err != nil { 208 | err = fmt.Errorf("mount_osxfusefs failed: %v. Stderr: %s, Stdout: %s", 209 | err, errOut.String(), out.String()) 210 | *errp = err 211 | } 212 | close(ready) 213 | }() 214 | 215 | dup, err := syscall.Dup(int(fd.Fd())) 216 | if err != nil { 217 | return 218 | } 219 | 220 | syscall.CloseOnExec(int(fd.Fd())) 221 | syscall.CloseOnExec(dup) 222 | 223 | f = os.NewFile(uintptr(dup), "macfuse") 224 | be = osxfuseBackend 225 | return 226 | } 227 | 228 | func unixgramSocketpair() (l, r *os.File, err error) { 229 | fd, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) 230 | if err != nil { 231 | return nil, nil, os.NewSyscallError("socketpair", 232 | err.(syscall.Errno)) 233 | } 234 | l = os.NewFile(uintptr(fd[0]), "socketpair-half1") 235 | r = os.NewFile(uintptr(fd[1]), "socketpair-half2") 236 | return 237 | } 238 | 239 | func getConnection(local *os.File) (*os.File, error) { 240 | var data [4]byte 241 | control := make([]byte, 4*256) 242 | 243 | // n, oobn, recvflags, from, errno - todo: error checking. 244 | _, oobn, _, _, 245 | err := syscall.Recvmsg( 246 | int(local.Fd()), data[:], control[:], 0) 247 | if err != nil { 248 | return nil, err 249 | } 250 | 251 | message := *(*syscall.Cmsghdr)(unsafe.Pointer(&control[0])) 252 | fd := *(*int32)(unsafe.Pointer(uintptr(unsafe.Pointer(&control[0])) + syscall.SizeofCmsghdr)) 253 | 254 | if message.Type != syscall.SCM_RIGHTS { 255 | return nil, fmt.Errorf("getConnection: recvmsg returned wrong control type: %d", message.Type) 256 | } 257 | if oobn <= syscall.SizeofCmsghdr { 258 | return nil, fmt.Errorf("getConnection: too short control message. Length: %d", oobn) 259 | } 260 | if fd < 0 { 261 | return nil, fmt.Errorf("getConnection: fd < 0: %d", fd) 262 | } 263 | 264 | return os.NewFile(uintptr(fd), "macfuse"), nil 265 | } 266 | -------------------------------------------------------------------------------- /mount_freebsd.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | "sync" 10 | "syscall" 11 | ) 12 | 13 | func handleMountFusefsStderr(errCh chan<- error) func(line string) (ignore bool) { 14 | return func(line string) (ignore bool) { 15 | const ( 16 | noMountpointPrefix = `mount_fusefs: ` 17 | noMountpointSuffix = `: No such file or directory` 18 | ) 19 | if strings.HasPrefix(line, noMountpointPrefix) && strings.HasSuffix(line, noMountpointSuffix) { 20 | // re-extract it from the error message in case some layer 21 | // changed the path 22 | mountpoint := line[len(noMountpointPrefix) : len(line)-len(noMountpointSuffix)] 23 | err := &MountpointDoesNotExistError{ 24 | Path: mountpoint, 25 | } 26 | select { 27 | case errCh <- err: 28 | return true 29 | default: 30 | // not the first error; fall back to logging it 31 | return false 32 | } 33 | } 34 | 35 | return false 36 | } 37 | } 38 | 39 | // isBoringMountFusefsError returns whether the Wait error is 40 | // uninteresting; exit status 1 is. 41 | func isBoringMountFusefsError(err error) bool { 42 | if err, ok := err.(*exec.ExitError); ok && err.Exited() { 43 | if status, ok := err.Sys().(syscall.WaitStatus); ok && status.ExitStatus() == 1 { 44 | return true 45 | } 46 | } 47 | return false 48 | } 49 | 50 | func mount(dir string, conf *mountConfig, ready chan<- struct{}, errp *error) (fusefd *os.File, _ Backend, _ backendState, err error) { 51 | for k, v := range conf.options { 52 | if strings.Contains(k, ",") || strings.Contains(v, ",") { 53 | // Silly limitation but the mount helper does not 54 | // understand any escaping. See TestMountOptionCommaError. 55 | err = fmt.Errorf("mount options cannot contain commas on FreeBSD: %q=%q", k, v) 56 | return 57 | } 58 | } 59 | 60 | f, err := os.OpenFile("/dev/fuse", os.O_RDWR, 0o000) 61 | if err != nil { 62 | *errp = err 63 | return 64 | } 65 | 66 | cmd := exec.Command( 67 | "/sbin/mount_fusefs", 68 | "--safe", 69 | "-o", conf.getOptions(), 70 | "3", 71 | dir, 72 | ) 73 | cmd.ExtraFiles = []*os.File{f} 74 | 75 | stdout, err := cmd.StdoutPipe() 76 | if err != nil { 77 | err = fmt.Errorf("setting up mount_fusefs stderr: %v", err) 78 | return 79 | } 80 | stderr, err := cmd.StderrPipe() 81 | if err != nil { 82 | err = fmt.Errorf("setting up mount_fusefs stderr: %v", err) 83 | return 84 | } 85 | 86 | if err = cmd.Start(); err != nil { 87 | err = fmt.Errorf("mount_fusefs: %v", err) 88 | return 89 | } 90 | helperErrCh := make(chan error, 1) 91 | var wg sync.WaitGroup 92 | wg.Add(2) 93 | go lineLogger(&wg, "mount helper output", neverIgnoreLine, stdout) 94 | go lineLogger(&wg, "mount helper error", handleMountFusefsStderr(helperErrCh), stderr) 95 | wg.Wait() 96 | if err = cmd.Wait(); err != nil { 97 | // see if we have a better error to report 98 | select { 99 | case helperErr := <-helperErrCh: 100 | // log the Wait error if it's not what we expected 101 | if !isBoringMountFusefsError(err) { 102 | log.Printf("mount helper failed: %v", err) 103 | } 104 | // and now return what we grabbed from stderr as the real 105 | // error 106 | err = helperErr 107 | return 108 | default: 109 | // nope, fall back to generic message 110 | } 111 | err = fmt.Errorf("mount_fusefs: %v", err) 112 | return 113 | } 114 | 115 | close(ready) 116 | fusefd = f 117 | return 118 | } 119 | -------------------------------------------------------------------------------- /mount_linux.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | "sync" 11 | "syscall" 12 | ) 13 | 14 | func handleFusermountStderr(errCh chan<- error) func(line string) (ignore bool) { 15 | return func(line string) (ignore bool) { 16 | if line == `fusermount: failed to open /etc/fuse.conf: Permission denied` { 17 | // Silence this particular message, it occurs way too 18 | // commonly and isn't very relevant to whether the mount 19 | // succeeds or not. 20 | return true 21 | } 22 | 23 | const ( 24 | noMountpointPrefix = `fusermount: failed to access mountpoint ` 25 | noMountpointSuffix = `: No such file or directory` 26 | ) 27 | if strings.HasPrefix(line, noMountpointPrefix) && strings.HasSuffix(line, noMountpointSuffix) { 28 | // re-extract it from the error message in case some layer 29 | // changed the path 30 | mountpoint := line[len(noMountpointPrefix) : len(line)-len(noMountpointSuffix)] 31 | err := &MountpointDoesNotExistError{ 32 | Path: mountpoint, 33 | } 34 | select { 35 | case errCh <- err: 36 | return true 37 | default: 38 | // not the first error; fall back to logging it 39 | return false 40 | } 41 | } 42 | 43 | return false 44 | } 45 | } 46 | 47 | // isBoringFusermountError returns whether the Wait error is 48 | // uninteresting; exit status 1 is. 49 | func isBoringFusermountError(err error) bool { 50 | if err, ok := err.(*exec.ExitError); ok && err.Exited() { 51 | if status, ok := err.Sys().(syscall.WaitStatus); ok && status.ExitStatus() == 1 { 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | 58 | func mount( 59 | dir string, 60 | conf *mountConfig, 61 | ready chan<- struct{}, 62 | errp *error, 63 | ) (fusefd *os.File, _ Backend, _ backendState, err error) { 64 | // linux mount is never delayed 65 | close(ready) 66 | 67 | fds, err := syscall.Socketpair(syscall.AF_FILE, syscall.SOCK_STREAM, 0) 68 | if err != nil { 69 | err = fmt.Errorf("socketpair error: %v", err) 70 | return 71 | } 72 | 73 | writeFile := os.NewFile(uintptr(fds[0]), "fusermount-child-writes") 74 | defer writeFile.Close() 75 | 76 | readFile := os.NewFile(uintptr(fds[1]), "fusermount-parent-reads") 77 | defer readFile.Close() 78 | 79 | cmd := exec.Command( 80 | "fusermount", 81 | "-o", conf.getOptions(), 82 | "--", 83 | dir, 84 | ) 85 | cmd.Env = append(os.Environ(), "_FUSE_COMMFD=3") 86 | 87 | cmd.ExtraFiles = []*os.File{writeFile} 88 | 89 | var wg sync.WaitGroup 90 | stdout, err := cmd.StdoutPipe() 91 | if err != nil { 92 | err = fmt.Errorf("setting up fusermount stderr: %v", err) 93 | return 94 | } 95 | stderr, err := cmd.StderrPipe() 96 | if err != nil { 97 | err = fmt.Errorf("setting up fusermount stderr: %v", err) 98 | return 99 | } 100 | 101 | if err = cmd.Start(); err != nil { 102 | err = fmt.Errorf("fusermount: %v", err) 103 | return 104 | } 105 | helperErrCh := make(chan error, 1) 106 | wg.Add(2) 107 | go lineLogger(&wg, "mount helper output", neverIgnoreLine, stdout) 108 | go lineLogger(&wg, "mount helper error", handleFusermountStderr(helperErrCh), stderr) 109 | wg.Wait() 110 | if err = cmd.Wait(); err != nil { 111 | // see if we have a better error to report 112 | select { 113 | case helperErr := <-helperErrCh: 114 | // log the Wait error if it's not what we expected 115 | if !isBoringFusermountError(err) { 116 | log.Printf("mount helper failed: %v", err) 117 | } 118 | // and now return what we grabbed from stderr as the real 119 | // error 120 | err = helperErr 121 | return 122 | default: 123 | // nope, fall back to generic message 124 | } 125 | 126 | err = fmt.Errorf("fusermount: %v", err) 127 | return 128 | } 129 | 130 | c, err := net.FileConn(readFile) 131 | if err != nil { 132 | err = fmt.Errorf("FileConn from fusermount socket: %v", err) 133 | return 134 | } 135 | defer c.Close() 136 | 137 | uc, ok := c.(*net.UnixConn) 138 | if !ok { 139 | err = fmt.Errorf("unexpected FileConn type; expected UnixConn, got %T", c) 140 | return 141 | } 142 | 143 | buf := make([]byte, 32) // expect 1 byte 144 | oob := make([]byte, 32) // expect 24 bytes 145 | _, oobn, _, _, err := uc.ReadMsgUnix(buf, oob) 146 | scms, err := syscall.ParseSocketControlMessage(oob[:oobn]) 147 | if err != nil { 148 | err = fmt.Errorf("ParseSocketControlMessage: %v", err) 149 | return 150 | } 151 | if len(scms) != 1 { 152 | err = fmt.Errorf("expected 1 SocketControlMessage; got scms = %#v", scms) 153 | return 154 | } 155 | scm := scms[0] 156 | gotFds, err := syscall.ParseUnixRights(&scm) 157 | if err != nil { 158 | err = fmt.Errorf("syscall.ParseUnixRights: %v", err) 159 | return 160 | } 161 | if len(gotFds) != 1 { 162 | err = fmt.Errorf("wanted 1 fd; got %#v", gotFds) 163 | return 164 | } 165 | fusefd = os.NewFile(uintptr(gotFds[0]), "/dev/fuse") 166 | return 167 | } 168 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | func dummyOption(conf *mountConfig) error { 9 | return nil 10 | } 11 | 12 | // mountConfig holds the configuration for a mount operation. 13 | // Use it by passing MountOption values to Mount. 14 | type mountConfig struct { 15 | options map[string]string 16 | maxReadahead uint32 17 | initFlags InitFlags 18 | maxBackground uint16 19 | congestionThreshold uint16 20 | osxfuseLocations []OSXFUSEPaths 21 | } 22 | 23 | func escapeComma(s string) string { 24 | s = strings.Replace(s, `\`, `\\`, -1) 25 | s = strings.Replace(s, `,`, `\,`, -1) 26 | return s 27 | } 28 | 29 | // getOptions makes a string of options suitable for passing to FUSE 30 | // mount flag `-o`. Returns an empty string if no options were set. 31 | // Any platform specific adjustments should happen before the call. 32 | func (m *mountConfig) getOptions() string { 33 | var opts []string 34 | for k, v := range m.options { 35 | k = escapeComma(k) 36 | if v != "" { 37 | k += "=" + escapeComma(v) 38 | } 39 | opts = append(opts, k) 40 | } 41 | return strings.Join(opts, ",") 42 | } 43 | 44 | func (m *mountConfig) isReadonly() bool { 45 | _, e := m.options["ro"] 46 | return e 47 | } 48 | 49 | func (m *mountConfig) fsname() string { 50 | return m.options["fsname"] 51 | } 52 | 53 | type mountOption func(*mountConfig) error 54 | 55 | // MountOption is passed to Mount to change the behavior of the mount. 56 | type MountOption mountOption 57 | 58 | // FSName sets the file system name (also called source) that is 59 | // visible in the list of mounted file systems. 60 | // 61 | // FreeBSD ignores this option. 62 | func FSName(name string) MountOption { 63 | return func(conf *mountConfig) error { 64 | conf.options["fsname"] = name 65 | return nil 66 | } 67 | } 68 | 69 | // Subtype sets the subtype of the mount. The main type is always 70 | // `fuse`. The type in a list of mounted file systems will look like 71 | // `fuse.foo`. 72 | // 73 | // OS X ignores this option. 74 | // FreeBSD ignores this option. 75 | func Subtype(fstype string) MountOption { 76 | return func(conf *mountConfig) error { 77 | conf.options["subtype"] = fstype 78 | return nil 79 | } 80 | } 81 | 82 | // LocalVolume sets the volume to be local (instead of network), 83 | // changing the behavior of Finder, Spotlight, and such. 84 | // 85 | // OS X only. Others ignore this option. 86 | func LocalVolume() MountOption { 87 | return localVolume 88 | } 89 | 90 | // VolumeName sets the volume name shown in Finder. 91 | // 92 | // OS X only. Others ignore this option. 93 | func VolumeName(name string) MountOption { 94 | return volumeName(name) 95 | } 96 | 97 | // NoAppleDouble makes OSXFUSE disallow files with names used by OS X 98 | // to store extended attributes on file systems that do not support 99 | // them natively. 100 | // 101 | // Such file names are: 102 | // 103 | // ._* 104 | // .DS_Store 105 | // 106 | // OS X only. Others ignore this option. 107 | func NoAppleDouble() MountOption { 108 | return noAppleDouble 109 | } 110 | 111 | // NoAppleXattr makes OSXFUSE disallow extended attributes with the 112 | // prefix "com.apple.". This disables persistent Finder state and 113 | // other such information. 114 | // 115 | // OS X only. Others ignore this option. 116 | func NoAppleXattr() MountOption { 117 | return noAppleXattr 118 | } 119 | 120 | // NoBrowse makes OSXFUSE mark the volume as non-browsable, so that 121 | // Finder won't automatically browse it. 122 | // 123 | // OS X only. Others ignore this option. 124 | func NoBrowse() MountOption { 125 | return noBrowse 126 | } 127 | 128 | // ExclCreate causes O_EXCL flag to be set for only "truly" exclusive creates, 129 | // i.e. create calls for which the initiator explicitly set the O_EXCL flag. 130 | // 131 | // OSXFUSE expects all create calls to return EEXIST in case the file 132 | // already exists, regardless of whether O_EXCL was specified or not. 133 | // To ensure this behavior, it normally sets OpenExclusive for all 134 | // Create calls, regardless of whether the original call had it set. 135 | // For distributed filesystems, that may force every file create to be 136 | // a distributed consensus action, causing undesirable delays. 137 | // 138 | // This option makes the FUSE filesystem see the original flag value, 139 | // and better decide when to ensure global consensus. 140 | // 141 | // Note that returning EEXIST on existing file create is still 142 | // expected with OSXFUSE, regardless of the presence of the 143 | // OpenExclusive flag. 144 | // 145 | // For more information, see 146 | // https://github.com/osxfuse/osxfuse/issues/209 147 | // 148 | // OS X only. Others ignore this options. 149 | // Requires OSXFUSE 3.4.1 or newer. 150 | func ExclCreate() MountOption { 151 | return exclCreate 152 | } 153 | 154 | // DaemonTimeout sets the time in seconds between a request and a reply before 155 | // the FUSE mount is declared dead. 156 | // 157 | // OS X and FreeBSD only. Others ignore this option. 158 | func DaemonTimeout(name string) MountOption { 159 | return daemonTimeout(name) 160 | } 161 | 162 | // AllowOther allows other users to access the file system. 163 | func AllowOther() MountOption { 164 | return func(conf *mountConfig) error { 165 | conf.options["allow_other"] = "" 166 | return nil 167 | } 168 | } 169 | 170 | // AllowDev enables interpreting character or block special devices on the 171 | // filesystem. 172 | func AllowDev() MountOption { 173 | return func(conf *mountConfig) error { 174 | conf.options["dev"] = "" 175 | return nil 176 | } 177 | } 178 | 179 | // AllowSUID allows set-user-identifier or set-group-identifier bits to take 180 | // effect. 181 | func AllowSUID() MountOption { 182 | return func(conf *mountConfig) error { 183 | conf.options["suid"] = "" 184 | return nil 185 | } 186 | } 187 | 188 | // DefaultPermissions makes the kernel enforce access control based on 189 | // the file mode (as in chmod). 190 | // 191 | // Without this option, the Node itself decides what is and is not 192 | // allowed. This is normally ok because FUSE file systems cannot be 193 | // accessed by other users without AllowOther. 194 | // 195 | // FreeBSD ignores this option. 196 | func DefaultPermissions() MountOption { 197 | return func(conf *mountConfig) error { 198 | conf.options["default_permissions"] = "" 199 | return nil 200 | } 201 | } 202 | 203 | // ReadOnly makes the mount read-only. 204 | func ReadOnly() MountOption { 205 | return func(conf *mountConfig) error { 206 | conf.options["ro"] = "" 207 | return nil 208 | } 209 | } 210 | 211 | // MaxReadahead sets the number of bytes that can be prefetched for 212 | // sequential reads. The kernel can enforce a maximum value lower than 213 | // this. 214 | // 215 | // This setting makes the kernel perform speculative reads that do not 216 | // originate from any client process. This usually tremendously 217 | // improves read performance. 218 | func MaxReadahead(n uint32) MountOption { 219 | return func(conf *mountConfig) error { 220 | conf.maxReadahead = n 221 | return nil 222 | } 223 | } 224 | 225 | // AsyncRead enables multiple outstanding read requests for the same 226 | // handle. Without this, there is at most one request in flight at a 227 | // time. 228 | func AsyncRead() MountOption { 229 | return func(conf *mountConfig) error { 230 | conf.initFlags |= InitAsyncRead 231 | return nil 232 | } 233 | } 234 | 235 | // WritebackCache enables the kernel to buffer writes before sending 236 | // them to the FUSE server. Without this, writethrough caching is 237 | // used. 238 | func WritebackCache() MountOption { 239 | return func(conf *mountConfig) error { 240 | conf.initFlags |= InitWritebackCache 241 | return nil 242 | } 243 | } 244 | 245 | // OSXFUSEPaths describes the path used by an installed OSXFUSE 246 | // version. See OSXFUSELocationV4 for typical values. 247 | type OSXFUSEPaths struct { 248 | // Path of the mount helper, used for the mount operation 249 | Mount string 250 | } 251 | 252 | // Default paths for OSXFUSE. See OSXFUSELocations. 253 | var ( 254 | OSXFUSELocationV4 = OSXFUSEPaths{ 255 | Mount: "/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse", 256 | } 257 | OSXFUSELocationV3 = OSXFUSEPaths{ 258 | Mount: "/Library/Filesystems/osxfuse.fs/Contents/Resources/mount_osxfuse", 259 | } 260 | ) 261 | 262 | // OSXFUSELocations sets where to look for OSXFUSE files. The 263 | // arguments are all the possible locations. The previous locations 264 | // are replaced. 265 | // 266 | // Without this option, OSXFUSELocationV3 and OSXFUSELocationV2 are 267 | // used. 268 | // 269 | // OS X only. Others ignore this option. 270 | func OSXFUSELocations(paths ...OSXFUSEPaths) MountOption { 271 | return func(conf *mountConfig) error { 272 | if len(paths) == 0 { 273 | return errors.New("must specify at least one location for OSXFUSELocations") 274 | } 275 | // replace previous values, but make a copy so there's no 276 | // worries about caller mutating their slice 277 | conf.osxfuseLocations = append(conf.osxfuseLocations[:0], paths...) 278 | return nil 279 | } 280 | } 281 | 282 | // AllowNonEmptyMount allows the mounting over a non-empty directory. 283 | // 284 | // The files in it will be shadowed by the freshly created mount. By 285 | // default these mounts are rejected to prevent accidental covering up 286 | // of data, which could for example prevent automatic backup. 287 | func AllowNonEmptyMount() MountOption { 288 | return func(conf *mountConfig) error { 289 | conf.options["nonempty"] = "" 290 | return nil 291 | } 292 | } 293 | 294 | // MaxBackground sets the maximum number of FUSE requests the kernel 295 | // will submit in the background. Background requests are used when an 296 | // immediate answer is not needed. This may help with request latency. 297 | // 298 | // On Linux, this can be adjusted on the fly with 299 | // /sys/fs/fuse/connections/CONN/max_background 300 | func MaxBackground(n uint16) MountOption { 301 | return func(conf *mountConfig) error { 302 | conf.maxBackground = n 303 | return nil 304 | } 305 | } 306 | 307 | // CongestionThreshold sets the number of outstanding background FUSE 308 | // requests beyond which the kernel considers the filesystem 309 | // congested. This may help with request latency. 310 | // 311 | // On Linux, this can be adjusted on the fly with 312 | // /sys/fs/fuse/connections/CONN/congestion_threshold 313 | func CongestionThreshold(n uint16) MountOption { 314 | // TODO to test this, we'd have to figure out our connection id 315 | // and read /sys 316 | return func(conf *mountConfig) error { 317 | conf.congestionThreshold = n 318 | return nil 319 | } 320 | } 321 | 322 | // LockingFlock enables flock-based (BSD) locking. This is mostly 323 | // useful for distributed filesystems with global locking. Without 324 | // this, kernel manages local locking automatically. 325 | func LockingFlock() MountOption { 326 | return func(conf *mountConfig) error { 327 | conf.initFlags |= InitFlockLocks 328 | return nil 329 | } 330 | } 331 | 332 | // LockingPOSIX enables flock-based (BSD) locking. This is mostly 333 | // useful for distributed filesystems with global locking. Without 334 | // this, kernel manages local locking automatically. 335 | // 336 | // Beware POSIX locks are a broken API with unintuitive behavior for 337 | // callers. 338 | func LockingPOSIX() MountOption { 339 | return func(conf *mountConfig) error { 340 | conf.initFlags |= InitPOSIXLocks 341 | return nil 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /options_daemon_timeout_test.go: -------------------------------------------------------------------------------- 1 | // Test for adjustable timeout between a FUSE request and the daemon's response. 2 | // 3 | //go:build darwin 4 | // +build darwin 5 | 6 | package fuse_test 7 | 8 | import ( 9 | "context" 10 | "os" 11 | "runtime" 12 | "syscall" 13 | "testing" 14 | "time" 15 | 16 | "github.com/anacrolix/fuse" 17 | "github.com/anacrolix/fuse/fs" 18 | "github.com/anacrolix/fuse/fs/fstestutil" 19 | ) 20 | 21 | type slowCreaterDir struct { 22 | fstestutil.Dir 23 | } 24 | 25 | var _ fs.NodeCreater = slowCreaterDir{} 26 | 27 | func (c slowCreaterDir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) { 28 | time.Sleep(10 * time.Second) 29 | // pick a really distinct error, to identify it later 30 | return nil, nil, fuse.Errno(syscall.ENAMETOOLONG) 31 | } 32 | 33 | func TestMountOptionDaemonTimeout(t *testing.T) { 34 | if runtime.GOOS != "darwin" { 35 | return 36 | } 37 | if testing.Short() { 38 | t.Skip("skipping time-based test in short mode") 39 | } 40 | maybeParallel(t) 41 | ctx, cancel := context.WithCancel(context.Background()) 42 | defer cancel() 43 | 44 | mnt, err := fstestutil.MountedT(t, 45 | fstestutil.SimpleFS{slowCreaterDir{}}, 46 | nil, 47 | fuse.DaemonTimeout("2"), 48 | ) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | defer mnt.Close() 53 | control := openErrHelper.Spawn(ctx, t) 54 | defer control.Close() 55 | 56 | // This should fail by the kernel timing out the request. 57 | req := openRequest{ 58 | Path: mnt.Dir + "/child", 59 | Flags: os.O_WRONLY | os.O_CREATE, 60 | Perm: 0, 61 | WantErrno: syscall.ENOTCONN, 62 | } 63 | var nothing struct{} 64 | if err := control.JSON("/").Call(ctx, req, ¬hing); err != nil { 65 | t.Fatalf("calling helper: %v", err) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /options_darwin.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | func localVolume(conf *mountConfig) error { 4 | conf.options["local"] = "" 5 | return nil 6 | } 7 | 8 | func volumeName(name string) MountOption { 9 | return func(conf *mountConfig) error { 10 | conf.options["volname"] = name 11 | return nil 12 | } 13 | } 14 | 15 | func daemonTimeout(name string) MountOption { 16 | return func(conf *mountConfig) error { 17 | conf.options["daemon_timeout"] = name 18 | return nil 19 | } 20 | } 21 | 22 | func noAppleXattr(conf *mountConfig) error { 23 | conf.options["noapplexattr"] = "" 24 | return nil 25 | } 26 | 27 | func noAppleDouble(conf *mountConfig) error { 28 | conf.options["noappledouble"] = "" 29 | return nil 30 | } 31 | 32 | func exclCreate(conf *mountConfig) error { 33 | conf.options["excl_create"] = "" 34 | return nil 35 | } 36 | 37 | func noBrowse(conf *mountConfig) error { 38 | conf.options["nobrowse"] = "" 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /options_freebsd.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | func localVolume(conf *mountConfig) error { 4 | return nil 5 | } 6 | 7 | func volumeName(name string) MountOption { 8 | return dummyOption 9 | } 10 | 11 | func daemonTimeout(name string) MountOption { 12 | return func(conf *mountConfig) error { 13 | conf.options["timeout"] = name 14 | return nil 15 | } 16 | } 17 | 18 | func noAppleXattr(conf *mountConfig) error { 19 | return nil 20 | } 21 | 22 | func noAppleDouble(conf *mountConfig) error { 23 | return nil 24 | } 25 | 26 | func exclCreate(conf *mountConfig) error { 27 | return nil 28 | } 29 | 30 | func noBrowse(conf *mountConfig) error { 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /options_helper_test.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | //lint:ignore U1000 Used by TestMountOptionCommaError to bypass safe API 4 | 5 | // for TestMountOptionCommaError 6 | func ForTestSetMountOption(k, v string) MountOption { 7 | fn := func(conf *mountConfig) error { 8 | conf.options[k] = v 9 | return nil 10 | } 11 | return fn 12 | } 13 | -------------------------------------------------------------------------------- /options_linux.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | func localVolume(conf *mountConfig) error { 4 | return nil 5 | } 6 | 7 | func volumeName(name string) MountOption { 8 | return dummyOption 9 | } 10 | 11 | func daemonTimeout(name string) MountOption { 12 | return dummyOption 13 | } 14 | 15 | func noAppleXattr(conf *mountConfig) error { 16 | return nil 17 | } 18 | 19 | func noAppleDouble(conf *mountConfig) error { 20 | return nil 21 | } 22 | 23 | func exclCreate(conf *mountConfig) error { 24 | return nil 25 | } 26 | 27 | func noBrowse(conf *mountConfig) error { 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /options_nocomma_test.go: -------------------------------------------------------------------------------- 1 | // This file contains tests for platforms that have no escape 2 | // mechanism for including commas in mount options. 3 | // 4 | //go:build darwin 5 | // +build darwin 6 | 7 | package fuse_test 8 | 9 | import ( 10 | "runtime" 11 | "testing" 12 | 13 | "github.com/anacrolix/fuse" 14 | "github.com/anacrolix/fuse/fs/fstestutil" 15 | ) 16 | 17 | func TestMountOptionCommaError(t *testing.T) { 18 | maybeParallel(t) 19 | // this test is not tied to any specific option, it just needs 20 | // some string content 21 | var evil = "FuseTest,Marker" 22 | mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, nil, 23 | fuse.ForTestSetMountOption("fusetest", evil), 24 | ) 25 | if err == nil { 26 | mnt.Close() 27 | t.Fatal("expected an error about commas") 28 | } 29 | if g, e := err.Error(), `mount options cannot contain commas on `+runtime.GOOS+`: "fusetest"="FuseTest,Marker"`; g != e { 30 | t.Fatalf("wrong error: %q != %q", g, e) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package fuse_test 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "os" 10 | "runtime" 11 | "syscall" 12 | "testing" 13 | 14 | "github.com/anacrolix/fuse" 15 | "github.com/anacrolix/fuse/fs" 16 | "github.com/anacrolix/fuse/fs/fstestutil" 17 | "github.com/anacrolix/fuse/fs/fstestutil/spawntest" 18 | "github.com/anacrolix/fuse/fs/fstestutil/spawntest/httpjson" 19 | ) 20 | 21 | func init() { 22 | fstestutil.DebugByDefault() 23 | } 24 | 25 | func maybeParallel(t *testing.T) { 26 | // t.Parallel() 27 | } 28 | 29 | var helpers spawntest.Registry 30 | 31 | func TestMain(m *testing.M) { 32 | helpers.AddFlag(flag.CommandLine) 33 | flag.Parse() 34 | helpers.RunIfNeeded() 35 | os.Exit(m.Run()) 36 | } 37 | 38 | func TestMountOptionFSName(t *testing.T) { 39 | if runtime.GOOS == "freebsd" { 40 | t.Skip("FreeBSD does not support FSName") 41 | } 42 | maybeParallel(t) 43 | const name = "FuseTestMarker" 44 | mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, nil, 45 | fuse.FSName(name), 46 | ) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | defer mnt.Close() 51 | 52 | info, err := fstestutil.GetMountInfo(mnt.Dir) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | if g, e := info.FSName, name; g != e { 57 | t.Errorf("wrong FSName: %q != %q", g, e) 58 | } 59 | } 60 | 61 | func testMountOptionFSNameEvil(t *testing.T, evil string) { 62 | if runtime.GOOS == "freebsd" { 63 | t.Skip("FreeBSD does not support FSName") 64 | } 65 | maybeParallel(t) 66 | var name = "FuseTest" + evil + "Marker" 67 | mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, nil, 68 | fuse.FSName(name), 69 | ) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | defer mnt.Close() 74 | 75 | info, err := fstestutil.GetMountInfo(mnt.Dir) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | if g, e := info.FSName, name; g != e { 80 | t.Errorf("wrong FSName: %q != %q", g, e) 81 | } 82 | } 83 | 84 | func TestMountOptionFSNameEvilComma(t *testing.T) { 85 | if runtime.GOOS == "darwin" { 86 | // see TestMountOptionCommaError for a test that enforces we 87 | // at least give a nice error, instead of corrupting the mount 88 | // options 89 | t.Skip("TODO: OS X gets this wrong, commas in mount options cannot be escaped at all") 90 | } 91 | testMountOptionFSNameEvil(t, ",") 92 | } 93 | 94 | func TestMountOptionFSNameEvilSpace(t *testing.T) { 95 | testMountOptionFSNameEvil(t, " ") 96 | } 97 | 98 | func TestMountOptionFSNameEvilTab(t *testing.T) { 99 | testMountOptionFSNameEvil(t, "\t") 100 | } 101 | 102 | func TestMountOptionFSNameEvilNewline(t *testing.T) { 103 | testMountOptionFSNameEvil(t, "\n") 104 | } 105 | 106 | func TestMountOptionFSNameEvilBackslash(t *testing.T) { 107 | testMountOptionFSNameEvil(t, `\`) 108 | } 109 | 110 | func TestMountOptionFSNameEvilBackslashDouble(t *testing.T) { 111 | // catch double-unescaping, if it were to happen 112 | testMountOptionFSNameEvil(t, `\\`) 113 | } 114 | 115 | func TestMountOptionSubtype(t *testing.T) { 116 | if runtime.GOOS == "darwin" { 117 | t.Skip("OS X does not support Subtype") 118 | } 119 | if runtime.GOOS == "freebsd" { 120 | t.Skip("FreeBSD does not support Subtype") 121 | } 122 | maybeParallel(t) 123 | const name = "FuseTestMarker" 124 | mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, nil, 125 | fuse.Subtype(name), 126 | ) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | defer mnt.Close() 131 | 132 | info, err := fstestutil.GetMountInfo(mnt.Dir) 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | if g, e := info.Type, "fuse."+name; g != e { 137 | t.Errorf("wrong Subtype: %q != %q", g, e) 138 | } 139 | } 140 | 141 | func etcFuseHasAllowOther(t testing.TB) bool { 142 | // sucks to go poking around in other programs' config files. 143 | f, err := os.Open("/etc/fuse.conf") 144 | if errors.Is(err, os.ErrNotExist) { 145 | return false 146 | } 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | defer f.Close() 151 | scanner := bufio.NewScanner(f) 152 | for scanner.Scan() { 153 | if scanner.Text() == "user_allow_other" { 154 | return true 155 | } 156 | } 157 | if err := scanner.Err(); err != nil { 158 | t.Fatalf("reading /etc/fuse.conf: %v", err) 159 | } 160 | return false 161 | } 162 | 163 | func TestMountOptionAllowOther(t *testing.T) { 164 | if !etcFuseHasAllowOther(t) { 165 | t.Skip("need user_allow_other in /etc/fuse.conf") 166 | } 167 | maybeParallel(t) 168 | mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, nil, 169 | fuse.AllowOther(), 170 | ) 171 | if err != nil { 172 | t.Fatalf("mount error: %v", err) 173 | } 174 | defer mnt.Close() 175 | // we're not going to bother testing that other users actually can 176 | // access the fs, that's quite fiddly and system specific -- we'd 177 | // need to run as root and switch to some other account for the 178 | // client. 179 | } 180 | 181 | type unwritableFile struct{} 182 | 183 | func (f unwritableFile) Attr(ctx context.Context, a *fuse.Attr) error { 184 | a.Mode = 0o000 185 | return nil 186 | } 187 | 188 | type openRequest struct { 189 | Path string 190 | Flags int 191 | Perm os.FileMode 192 | WantErrno syscall.Errno 193 | } 194 | 195 | func doOpenErr(ctx context.Context, req openRequest) (*struct{}, error) { 196 | f, err := os.OpenFile(req.Path, req.Flags, req.Perm) 197 | if err == nil { 198 | f.Close() 199 | } 200 | if !errors.Is(err, req.WantErrno) { 201 | return nil, fmt.Errorf("wrong error: %v", err) 202 | } 203 | return &struct{}{}, nil 204 | } 205 | 206 | var openErrHelper = helpers.Register("openErr", httpjson.ServePOST(doOpenErr)) 207 | 208 | func TestMountOptionDefaultPermissions(t *testing.T) { 209 | if runtime.GOOS == "freebsd" { 210 | t.Skip("FreeBSD does not support DefaultPermissions") 211 | } 212 | if os.Getuid() == 0 { 213 | t.Skip("root cannot be denied by DefaultPermissions") 214 | } 215 | maybeParallel(t) 216 | ctx, cancel := context.WithCancel(context.Background()) 217 | defer cancel() 218 | 219 | mnt, err := fstestutil.MountedT(t, 220 | fstestutil.SimpleFS{ 221 | &fstestutil.ChildMap{"child": unwritableFile{}}, 222 | }, 223 | nil, 224 | fuse.DefaultPermissions(), 225 | ) 226 | if err != nil { 227 | t.Fatal(err) 228 | } 229 | defer mnt.Close() 230 | control := openErrHelper.Spawn(ctx, t) 231 | defer control.Close() 232 | 233 | // This will be prevented by kernel-level access checking when 234 | // DefaultPermissions is used. 235 | req := openRequest{ 236 | Path: mnt.Dir + "/child", 237 | Flags: os.O_WRONLY, 238 | Perm: 0, 239 | WantErrno: syscall.EACCES, 240 | } 241 | var nothing struct{} 242 | if err := control.JSON("/").Call(ctx, req, ¬hing); err != nil { 243 | t.Fatalf("calling helper: %v", err) 244 | } 245 | } 246 | 247 | type createrDir struct { 248 | fstestutil.Dir 249 | } 250 | 251 | var _ fs.NodeCreater = createrDir{} 252 | 253 | func (createrDir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) { 254 | // pick a really distinct error, to identify it later 255 | return nil, nil, fuse.Errno(syscall.ENAMETOOLONG) 256 | } 257 | 258 | func TestMountOptionReadOnly(t *testing.T) { 259 | maybeParallel(t) 260 | ctx, cancel := context.WithCancel(context.Background()) 261 | defer cancel() 262 | 263 | mnt, err := fstestutil.MountedT(t, 264 | fstestutil.SimpleFS{createrDir{}}, 265 | nil, 266 | fuse.ReadOnly(), 267 | ) 268 | if err != nil { 269 | t.Fatal(err) 270 | } 271 | defer mnt.Close() 272 | control := openErrHelper.Spawn(ctx, t) 273 | defer control.Close() 274 | 275 | // This will be prevented by kernel-level access checking when 276 | // ReadOnly is used. 277 | req := openRequest{ 278 | Path: mnt.Dir + "/child", 279 | Flags: os.O_WRONLY | os.O_CREATE, 280 | Perm: 0, 281 | WantErrno: syscall.EROFS, 282 | } 283 | var nothing struct{} 284 | if err := control.JSON("/").Call(ctx, req, ¬hing); err != nil { 285 | t.Fatalf("calling helper: %v", err) 286 | } 287 | } 288 | 289 | func TestMountOptionMaxBackground(t *testing.T) { 290 | maybeParallel(t) 291 | mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, nil, 292 | fuse.MaxBackground(4), 293 | ) 294 | if err != nil { 295 | t.Fatal(err) 296 | } 297 | defer mnt.Close() 298 | 299 | // TODO figure out our connection id and read 300 | // /sys/fs/fuse/connections/NUM/max_background 301 | } 302 | 303 | func TestMountOptionCongestionThreshold(t *testing.T) { 304 | maybeParallel(t) 305 | mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{fstestutil.Dir{}}, nil, 306 | fuse.CongestionThreshold(3), 307 | ) 308 | if err != nil { 309 | t.Fatal(err) 310 | } 311 | defer mnt.Close() 312 | 313 | // TODO figure out our connection id and read 314 | // /sys/fs/fuse/connections/NUM/congestion_threshold 315 | } 316 | -------------------------------------------------------------------------------- /protocol.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Protocol is a FUSE protocol version number. 8 | type Protocol struct { 9 | Major uint32 10 | Minor uint32 11 | } 12 | 13 | func (p Protocol) String() string { 14 | return fmt.Sprintf("%d.%d", p.Major, p.Minor) 15 | } 16 | 17 | // LT returns whether a is less than b. 18 | func (a Protocol) LT(b Protocol) bool { 19 | return a.Major < b.Major || 20 | (a.Major == b.Major && a.Minor < b.Minor) 21 | } 22 | 23 | // GE returns whether a is greater than or equal to b. 24 | func (a Protocol) GE(b Protocol) bool { 25 | return a.Major > b.Major || 26 | (a.Major == b.Major && a.Minor >= b.Minor) 27 | } 28 | 29 | // HasAttrBlockSize returns whether Attr.BlockSize is respected by the 30 | // kernel. 31 | // 32 | // Deprecated: Guaranteed to be true with our minimum supported 33 | // protocol version. 34 | func (a Protocol) HasAttrBlockSize() bool { 35 | return true 36 | } 37 | 38 | // HasReadWriteFlags returns whether ReadRequest/WriteRequest 39 | // fields Flags and FileFlags are valid. 40 | // 41 | // Deprecated: Guaranteed to be true with our minimum supported 42 | // protocol version. 43 | func (a Protocol) HasReadWriteFlags() bool { 44 | return true 45 | } 46 | 47 | // HasGetattrFlags returns whether GetattrRequest field Flags is 48 | // valid. 49 | // 50 | // Deprecated: Guaranteed to be true with our minimum supported 51 | // protocol version. 52 | func (a Protocol) HasGetattrFlags() bool { 53 | return true 54 | } 55 | 56 | // HasOpenNonSeekable returns whether OpenResponse field Flags flag 57 | // OpenNonSeekable is supported. 58 | // 59 | // Deprecated: Guaranteed to be true with our minimum supported 60 | // protocol version. 61 | func (a Protocol) HasOpenNonSeekable() bool { 62 | return true 63 | } 64 | 65 | // HasUmask returns whether CreateRequest/MkdirRequest/MknodRequest 66 | // field Umask is valid. 67 | // 68 | // Deprecated: Guaranteed to be true with our minimum supported 69 | // protocol version. 70 | func (a Protocol) HasUmask() bool { 71 | return true 72 | } 73 | 74 | // HasInvalidate returns whether InvalidateNode/InvalidateEntry are 75 | // supported. 76 | // 77 | // Deprecated: Guaranteed to be true with our minimum supported 78 | // protocol version. 79 | func (a Protocol) HasInvalidate() bool { 80 | return true 81 | } 82 | -------------------------------------------------------------------------------- /unmount.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | // Unmount tries to unmount the filesystem mounted at dir. 4 | func Unmount(dir string) error { 5 | return unmount(dir) 6 | } 7 | -------------------------------------------------------------------------------- /unmount_linux.go: -------------------------------------------------------------------------------- 1 | package fuse 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "os/exec" 7 | ) 8 | 9 | func unmount(dir string) error { 10 | cmd := exec.Command("fusermount", "-u", dir) 11 | output, err := cmd.CombinedOutput() 12 | if err != nil { 13 | if len(output) > 0 { 14 | output = bytes.TrimRight(output, "\n") 15 | msg := err.Error() + ": " + string(output) 16 | err = errors.New(msg) 17 | } 18 | return err 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /unmount_std.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package fuse 5 | 6 | import ( 7 | "os" 8 | "syscall" 9 | ) 10 | 11 | func unmount(dir string) error { 12 | err := syscall.Unmount(dir, 0) 13 | if err != nil { 14 | err = &os.PathError{Op: "unmount", Path: dir, Err: err} 15 | return err 16 | } 17 | return nil 18 | } 19 | --------------------------------------------------------------------------------