@cryptotaxi247 / kubo / commits / 7c844bace

feat(fuse): Expose MFS as FUSE mount point (#10781)

* Add MFS command line options, extend existing mount functions for MFS, set defaults. * Directory listing and file stat. * Add a read-only MFS view. * Add mkdir and interface checks. * Add remove and rename functionality. * Implement all required write interfaces. * Adjust mount functions for other architechtures. * Merge branch 'master' into feat/10710-mfs-fuse-mount * Write a basic read/write test. * Write more basic tests, add a mutex to the file object, fix modtime. * Add a concurrency test, remove mutexes from file and directory structures. * Refactor naming(mfdir -> mfsdir) and add documentation. * Add CID retrieval through ipfs_cid xattr. * Add docs, add xattr listing, fix bugs for mv and stat, refactor. * Add MFS command line options, extend existing mount functions for MFS, set defaults. * docs phrasing * docs: Mounts.MFS * docs: warn about lazy-loaded DAGs * test: TEST_FUSE=1 ./t0030-mount.sh -v --------- Co-authored-by: Guillaume Michel <guillaumemichel@users.noreply.github.com> Co-authored-by: guillaumemichel <guillaume@michel.id> Co-authored-by: Marcin Rataj <lidel@lidel.org>

Sergey Gorbunov committed May 6, 2025 at 22:55 UTC 7c844baceadf991ca49f427f4ac17b0eab4b314f
26 files changed +962 -42
cmd/ipfs/kubo/daemon.go
+9 -1
@@ -56,6 +56,7 @@ const (
56 initProfileOptionKwd = "init-profile"
57 ipfsMountKwd = "mount-ipfs"
58 ipnsMountKwd = "mount-ipns"
59 + mfsMountKwd = "mount-mfs"
60 migrateKwd = "migrate"
61 mountKwd = "mount"
62 offlineKwd = "offline" // global option
@@ -173,6 +174,7 @@ Headers.
174 cmds.BoolOption(mountKwd, "Mounts IPFS to the filesystem using FUSE (experimental)"),
175 cmds.StringOption(ipfsMountKwd, "Path to the mountpoint for IPFS (if using --mount). Defaults to config setting."),
176 cmds.StringOption(ipnsMountKwd, "Path to the mountpoint for IPNS (if using --mount). Defaults to config setting."),
177 + cmds.StringOption(mfsMountKwd, "Path to the mountpoint for MFS (if using --mount). Defaults to config setting."),
178 cmds.BoolOption(unrestrictedAPIAccessKwd, "Allow RPC API access to unlisted hashes"),
179 cmds.BoolOption(unencryptTransportKwd, "Disable transport encryption (for debugging protocols)"),
180 cmds.BoolOption(enableGCKwd, "Enable automatic periodic repo garbage collection"),
@@ -1062,17 +1064,23 @@ func mountFuse(req *cmds.Request, cctx *oldcmds.Context) error {
1064 nsdir = cfg.Mounts.IPNS
1065 }
1066
1067 + mfsdir, found := req.Options[mfsMountKwd].(string)
1068 + if !found {
1069 + mfsdir = cfg.Mounts.MFS
1070 + }
1071 +
1072 node, err := cctx.ConstructNode()
1073 if err != nil {
1074 return fmt.Errorf("mountFuse: ConstructNode() failed: %s", err)
1075 }
1076
1070 - err = nodeMount.Mount(node, fsdir, nsdir)
1077 + err = nodeMount.Mount(node, fsdir, nsdir, mfsdir)
1078 if err != nil {
1079 return err
1080 }
1081 fmt.Printf("IPFS mounted at: %s\n", fsdir)
1082 fmt.Printf("IPNS mounted at: %s\n", nsdir)
1083 + fmt.Printf("MFS mounted at: %s\n", mfsdir)
1084 return nil
1085 }
1086
config/init.go
+1
@@ -52,6 +52,7 @@ func InitWithIdentity(identity Identity) (*Config, error) {
52 Mounts: Mounts{
53 IPFS: "/ipfs",
54 IPNS: "/ipns",
55 + MFS: "/mfs",
56 },
57
58 Ipns: Ipns{
config/mounts.go
+1
@@ -4,5 +4,6 @@ package config
4 type Mounts struct {
5 IPFS string
6 IPNS string
7 + MFS string
8 FuseAllowOther bool
9 }
core/commands/mount_nofuse.go
+3 -2
@@ -14,10 +14,11 @@ var MountCmd = &cmds.Command{
14 ShortDescription: `
15 This version of ipfs is compiled without fuse support, which is required
16 for mounting. If you'd like to be able to mount, please use a version of
17 -ipfs compiled with fuse.
17 +Kubo compiled with fuse.
18
19 For the latest instructions, please check the project's repository:
20 - http://github.com/ipfs/go-ipfs
20 + http://github.com/ipfs/kubo
21 + https://github.com/ipfs/kubo/blob/master/docs/fuse.md
22 `,
23 },
24 }
core/commands/mount_unix.go
+16 -6
@@ -18,6 +18,7 @@ import (
18 const (
19 mountIPFSPathOptionName = "ipfs-path"
20 mountIPNSPathOptionName = "ipns-path"
21 + mountMFSPathOptionName = "mfs-path"
22 )
23
24 var MountCmd = &cmds.Command{
@@ -25,14 +26,14 @@ var MountCmd = &cmds.Command{
26 Helptext: cmds.HelpText{
27 Tagline: "Mounts IPFS to the filesystem (read-only).",
28 ShortDescription: `
28 -Mount IPFS at a read-only mountpoint on the OS (default: /ipfs and /ipns).
29 +Mount IPFS at a read-only mountpoint on the OS (default: /ipfs, /ipns, /mfs).
30 All IPFS objects will be accessible under that directory. Note that the
31 root will not be listable, as it is virtual. Access known paths directly.
32
33 You may have to create /ipfs and /ipns before using 'ipfs mount':
34
34 -> sudo mkdir /ipfs /ipns
35 -> sudo chown $(whoami) /ipfs /ipns
35 +> sudo mkdir /ipfs /ipns /mfs
36 +> sudo chown $(whoami) /ipfs /ipns /mfs
37 > ipfs daemon &
38 > ipfs mount
39 `,
@@ -44,8 +45,8 @@ root will not be listable, as it is virtual. Access known paths directly.
45
46 You may have to create /ipfs and /ipns before using 'ipfs mount':
47
47 -> sudo mkdir /ipfs /ipns
48 -> sudo chown $(whoami) /ipfs /ipns
48 +> sudo mkdir /ipfs /ipns /mfs
49 +> sudo chown $(whoami) /ipfs /ipns /mfs
50 > ipfs daemon &
51 > ipfs mount
52
@@ -67,6 +68,7 @@ baz
68 > ipfs mount
69 IPFS mounted at: /ipfs
70 IPNS mounted at: /ipns
71 +MFS mounted at: /mfs
72 > cd /ipfs/QmSh5e7S6fdcu75LAbXNZAFY2nGyZUJXyLCJDvn2zRkWyC
73 > ls
74 bar
@@ -81,6 +83,7 @@ baz
83 Options: []cmds.Option{
84 cmds.StringOption(mountIPFSPathOptionName, "f", "The path where IPFS should be mounted."),
85 cmds.StringOption(mountIPNSPathOptionName, "n", "The path where IPNS should be mounted."),
86 + cmds.StringOption(mountMFSPathOptionName, "m", "The path where MFS should be mounted."),
87 },
88 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
89 cfg, err := env.(*oldcmds.Context).GetConfig()
@@ -109,7 +112,12 @@ baz
112 nsdir = cfg.Mounts.IPNS // NB: be sure to not redeclare!
113 }
114
112 - err = nodeMount.Mount(nd, fsdir, nsdir)
115 + mfsdir, found := req.Options[mountMFSPathOptionName].(string)
116 + if !found {
117 + mfsdir = cfg.Mounts.MFS
118 + }
119 +
120 + err = nodeMount.Mount(nd, fsdir, nsdir, mfsdir)
121 if err != nil {
122 return err
123 }
@@ -117,6 +125,7 @@ baz
125 var output config.Mounts
126 output.IPFS = fsdir
127 output.IPNS = nsdir
128 + output.MFS = mfsdir
129 return cmds.EmitOnce(res, &output)
130 },
131 Type: config.Mounts{},
@@ -124,6 +133,7 @@ baz
133 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, mounts *config.Mounts) error {
134 fmt.Fprintf(w, "IPFS mounted at: %s\n", cmdenv.EscNonPrint(mounts.IPFS))
135 fmt.Fprintf(w, "IPNS mounted at: %s\n", cmdenv.EscNonPrint(mounts.IPNS))
136 + fmt.Fprintf(w, "MFS mounted at: %s\n", cmdenv.EscNonPrint(mounts.MFS))
137
138 return nil
139 }),
core/core.go
+1
@@ -134,6 +134,7 @@ type IpfsNode struct {
134 type Mounts struct {
135 Ipfs mount.Mount
136 Ipns mount.Mount
137 + Mfs mount.Mount
138 }
139
140 // Close calls Close() on the App object
docs/changelogs/v0.35.md
+9
@@ -12,6 +12,7 @@ This release was brought to you by the [Shipyard](http://ipshipyard.com/) team.
12 - [🔦 Highlights](#-highlights)
13 - [Opt-in HTTP Retrieval client](#opt-in-http-retrieval-client)
14 - [Dedicated `Reprovider.Strategy` for MFS](#dedicated-reproviderstrategy-for-mfs)
15 + - [Experimental support for MFS as a FUSE mount point](#experimental-support-for-mfs-as-a-fuse-mount-point)
16 - [Grid view in WebUI](#grid-view-in-webui)
17 - [Enhanced DAG-Shaping Controls](#enhanced-dag-shaping-controls)
18 - [New DAG-Shaping `ipfs add` Options](#new-dag-shaping-ipfs-add-options)
@@ -64,6 +65,14 @@ Users relying on the `pinned` strategy can switch to `pinned+mfs` and use MFS al
65
66 See [`Reprovider.Strategy`](https://github.com/ipfs/kubo/blob/master/docs/config.md#reproviderstrategy) for more details.
67
68 +#### Experimental support for MFS as a FUSE mount point
69 +
70 +The MFS root (filesystem behind the `ipfs files` API) is now available as a read/write FUSE mount point at `Mounts.MFS`. This filesystem is mounted in the same way as `Mounts.IPFS` and `Mounts.IPNS` when running `ipfs mount` or `ipfs daemon --mount`.
71 +
72 +Note that the operations supported by the MFS FUSE mountpoint are limited, since MFS doesn't store file attributes.
73 +
74 +See [`Mounts`](https://github.com/ipfs/kubo/blob/master/docs/config.md#mounts) and [`docs/fuse.md`](https://github.com/ipfs/kubo/blob/master/docs/fuse.md) for more details.
75 +
76 #### Grid view in WebUI
77
78 The WebUI, accessible at http://127.0.0.1:5001/webui/, now includes support for the grid view on the _Files_ screen:
docs/config.md
+15 -1
@@ -97,6 +97,7 @@ config file at runtime.
97 - [`Mounts`](#mounts)
98 - [`Mounts.IPFS`](#mountsipfs)
99 - [`Mounts.IPNS`](#mountsipns)
100 + - [`Mounts.MFS`](#mountsmfs)
101 - [`Mounts.FuseAllowOther`](#mountsfuseallowother)
102 - [`Pinning`](#pinning)
103 - [`Pinning.RemoteServices`](#pinningremoteservices)
@@ -1368,7 +1369,8 @@ Default: `cache`
1369
1370 ## `Mounts`
1371
1371 -**EXPERIMENTAL:** read about current limitations at [fuse.md](./fuse.md).
1372 +> [!CAUTION]
1373 +> **EXPERIMENTAL:** read about current limitations at [fuse.md](./fuse.md).
1374
1375 FUSE mount point configuration options.
1376
@@ -1388,6 +1390,18 @@ Default: `/ipns`
1390
1391 Type: `string` (filesystem path)
1392
1393 +### `Mounts.MFS`
1394 +
1395 +Mountpoint for Mutable File System (MFS) behind the `ipfs files` API.
1396 +
1397 +> [!CAUTION]
1398 +> - Write support is highly experimental and not recommended for mission-critical deployments.
1399 +> - Avoid storing lazy-loaded datasets in MFS. Exposing a partially local, lazy-loaded DAG risks operating system search indexers crawling it, which may trigger unintended network prefetching of non-local DAG components.
1400 +
1401 +Default: `/mfs`
1402 +
1403 +Type: `string` (filesystem path)
1404 +
1405 ### `Mounts.FuseAllowOther`
1406
1407 Sets the 'FUSE allow other'-option on the mount point.
docs/experimental-features.md
+1 -1
@@ -404,7 +404,7 @@ We also support the use of protocol names of the form /x/$NAME/http where $NAME
404
405 ## FUSE
406
407 -FUSE makes it possible to mount `/ipfs` and `/ipns` namespaces in your OS,
407 +FUSE makes it possible to mount `/ipfs`, `/ipns` and `/mfs` namespaces in your OS,
408 allowing arbitrary apps access to IPFS using a subset of filesystem abstractions.
409
410 It is considered EXPERIMENTAL due to limited (and buggy) support on some platforms.
docs/fuse.md
+25 -3
@@ -2,7 +2,7 @@
2
3 **EXPERIMENTAL:** FUSE support is limited, YMMV.
4
5 -Kubo makes it possible to mount `/ipfs` and `/ipns` namespaces in your OS,
5 +Kubo makes it possible to mount `/ipfs`, `/ipns` and `/mfs` namespaces in your OS,
6 allowing arbitrary apps access to IPFS.
7
8 ## Install FUSE
@@ -50,18 +50,20 @@ speak with us, or if you figure something new out, please add to this document!
50
51 ## Prepare mountpoints
52
53 -By default ipfs uses `/ipfs` and `/ipns` directories for mounting, this can be
54 -changed in config. You will have to create the `/ipfs` and `/ipns` directories
53 +By default ipfs uses `/ipfs`, `/ipns` and `/mfs` directories for mounting, this can be
54 +changed in config. You will have to create the `/ipfs`, `/ipns` and `/mfs` directories
55 explicitly. Note that modifying root requires sudo permissions.
56
57 ```sh
58 # make the directories
59 sudo mkdir /ipfs
60 sudo mkdir /ipns
61 +sudo mkdir /mfs
62
63 # chown them so ipfs can use them without root permissions
64 sudo chown <username> /ipfs
65 sudo chown <username> /ipns
66 +sudo chown <username> /mfs
67 ```
68
69 Depending on whether you are using OSX or Linux, follow the proceeding instructions.
@@ -105,6 +107,25 @@ ipfs config --json Mounts.FuseAllowOther true
107 ipfs daemon --mount
108 ```
109
110 +## MFS mountpoint
111 +
112 +Kubo v0.35.0 and later supports mounting the MFS (Mutable File System) root as
113 +a FUSE filesystem, enabling manipulation of content-addressed data like regular
114 +files. The CID for any file or directory is retrievable via the `ipfs_cid`
115 +extended attribute.
116 +
117 +```sh
118 +getfattr -n ipfs_cid /mfs/welcome-to-IPFS.jpg
119 +getfattr: Removing leading '/' from absolute path names
120 +# file: mfs/welcome-to-IPFS.jpg
121 +ipfs_cid="QmaeXDdwpUeKQcMy7d5SFBfVB4y7LtREbhm5KizawPsBSH"
122 +```
123 +
124 +Please note that the operations supported by the MFS FUSE mountpoint are
125 +limited. Since the MFS wasn't designed to store file attributes like ownership
126 +information, permissions and creation date, some applications like `vim` and
127 +`sed` may misbehave due to missing functionality.
128 +
129 ## Troubleshooting
130
131 #### `Permission denied` or `fusermount: user has no write access to mountpoint` error in Linux
@@ -145,6 +166,7 @@ set for user running `ipfs mount` command.
166 ```
167 sudo umount /ipfs
168 sudo umount /ipns
169 +sudo umount /mfs
170 ```
171
172 #### Mounting fails with "error mounting: could not resolve name"
fuse/mfs/mfs_test.go new
+342
@@ -0,0 +1,342 @@
1 +//go:build !nofuse && !openbsd && !netbsd && !plan9
2 +// +build !nofuse,!openbsd,!netbsd,!plan9
3 +
4 +package mfs
5 +
6 +import (
7 + "bytes"
8 + "context"
9 + "crypto/rand"
10 + "errors"
11 + iofs "io/fs"
12 + "os"
13 + "slices"
14 + "strconv"
15 + "testing"
16 + "time"
17 +
18 + "bazil.org/fuse"
19 + "bazil.org/fuse/fs"
20 + "bazil.org/fuse/fs/fstestutil"
21 + "github.com/ipfs/kubo/core"
22 + "github.com/ipfs/kubo/core/node"
23 + "github.com/libp2p/go-libp2p-testing/ci"
24 +)
25 +
26 +// Create an Ipfs.Node, a filesystem and a mount point.
27 +func setUp(t *testing.T, ipfs *core.IpfsNode) (fs.FS, *fstestutil.Mount) {
28 + if ci.NoFuse() {
29 + t.Skip("Skipping FUSE tests")
30 + }
31 +
32 + if ipfs == nil {
33 + var err error
34 + ipfs, err = core.NewNode(context.Background(), &node.BuildCfg{})
35 + if err != nil {
36 + t.Fatal(err)
37 + }
38 + }
39 +
40 + fs := NewFileSystem(ipfs)
41 + mnt, err := fstestutil.MountedT(t, fs, nil)
42 + if err == fuse.ErrOSXFUSENotFound {
43 + t.Skip(err)
44 + }
45 + if err != nil {
46 + t.Fatal(err)
47 + }
48 +
49 + return fs, mnt
50 +}
51 +
52 +// Test reading and writing a file.
53 +func TestReadWrite(t *testing.T) {
54 + _, mnt := setUp(t, nil)
55 + defer mnt.Close()
56 +
57 + path := mnt.Dir + "/testrw"
58 + content := make([]byte, 8196)
59 + _, err := rand.Read(content)
60 + if err != nil {
61 + t.Fatal(err)
62 + }
63 +
64 + t.Run("write", func(t *testing.T) {
65 + f, err := os.Create(path)
66 + if err != nil {
67 + t.Fatal(err)
68 + }
69 + defer f.Close()
70 +
71 + _, err = f.Write(content)
72 + if err != nil {
73 + t.Fatal(err)
74 + }
75 + })
76 + t.Run("read", func(t *testing.T) {
77 + f, err := os.Open(path)
78 + if err != nil {
79 + t.Fatal(err)
80 + }
81 + defer f.Close()
82 +
83 + buf := make([]byte, 8196)
84 + l, err := f.Read(buf)
85 + if err != nil {
86 + t.Fatal(err)
87 + }
88 + if bytes.Equal(content, buf[:l]) != true {
89 + t.Fatal("read and write not equal")
90 + }
91 + })
92 +}
93 +
94 +// Test creating a directory.
95 +func TestMkdir(t *testing.T) {
96 + _, mnt := setUp(t, nil)
97 + defer mnt.Close()
98 +
99 + path := mnt.Dir + "/foo/bar/baz/qux/quux"
100 +
101 + t.Run("write", func(t *testing.T) {
102 + err := os.MkdirAll(path, iofs.ModeDir)
103 + if err != nil {
104 + t.Fatal(err)
105 + }
106 + })
107 + t.Run("read", func(t *testing.T) {
108 + stat, err := os.Stat(path)
109 + if err != nil {
110 + t.Fatal(err)
111 + }
112 + if !stat.IsDir() {
113 + t.Fatal("not dir")
114 + }
115 + })
116 +}
117 +
118 +// Test file persistence across mounts.
119 +func TestPersistence(t *testing.T) {
120 + ipfs, err := core.NewNode(context.Background(), &node.BuildCfg{})
121 + if err != nil {
122 + t.Fatal(err)
123 + }
124 +
125 + content := make([]byte, 8196)
126 + _, err = rand.Read(content)
127 + if err != nil {
128 + t.Fatal(err)
129 + }
130 +
131 + t.Run("write", func(t *testing.T) {
132 + _, mnt := setUp(t, ipfs)
133 + defer mnt.Close()
134 + path := mnt.Dir + "/testpersistence"
135 +
136 + f, err := os.Create(path)
137 + if err != nil {
138 + t.Fatal(err)
139 + }
140 + defer f.Close()
141 +
142 + _, err = f.Write(content)
143 + if err != nil {
144 + t.Fatal(err)
145 + }
146 + })
147 + t.Run("read", func(t *testing.T) {
148 + _, mnt := setUp(t, ipfs)
149 + defer mnt.Close()
150 + path := mnt.Dir + "/testpersistence"
151 +
152 + f, err := os.Open(path)
153 + if err != nil {
154 + t.Fatal(err)
155 + }
156 + defer f.Close()
157 +
158 + buf := make([]byte, 8196)
159 + l, err := f.Read(buf)
160 + if err != nil {
161 + t.Fatal(err)
162 + }
163 + if bytes.Equal(content, buf[:l]) != true {
164 + t.Fatal("read and write not equal")
165 + }
166 + })
167 +}
168 +
169 +// Test getting the file attributes.
170 +func TestAttr(t *testing.T) {
171 + _, mnt := setUp(t, nil)
172 + defer mnt.Close()
173 +
174 + path := mnt.Dir + "/testattr"
175 + content := make([]byte, 8196)
176 + _, err := rand.Read(content)
177 + if err != nil {
178 + t.Fatal(err)
179 + }
180 +
181 + t.Run("write", func(t *testing.T) {
182 + f, err := os.Create(path)
183 + if err != nil {
184 + t.Fatal(err)
185 + }
186 + defer f.Close()
187 +
188 + _, err = f.Write(content)
189 + if err != nil {
190 + t.Fatal(err)
191 + }
192 + })
193 + t.Run("read", func(t *testing.T) {
194 + fi, err := os.Stat(path)
195 + if err != nil {
196 + t.Fatal(err)
197 + }
198 +
199 + if fi.IsDir() {
200 + t.Fatal("file is a directory")
201 + }
202 +
203 + if fi.ModTime().After(time.Now()) {
204 + t.Fatal("future modtime")
205 + }
206 + if time.Since(fi.ModTime()) > time.Second {
207 + t.Fatal("past modtime")
208 + }
209 +
210 + if fi.Name() != "testattr" {
211 + t.Fatal("invalid filename")
212 + }
213 +
214 + if fi.Size() != 8196 {
215 + t.Fatal("invalid size")
216 + }
217 + })
218 +}
219 +
220 +// Test concurrent access to the filesystem.
221 +func TestConcurrentRW(t *testing.T) {
222 + _, mnt := setUp(t, nil)
223 + defer mnt.Close()
224 +
225 + files := 5
226 + fileWorkers := 5
227 +
228 + path := mnt.Dir + "/testconcurrent"
229 + content := make([][]byte, files)
230 +
231 + for i := range content {
232 + content[i] = make([]byte, 8196)
233 + _, err := rand.Read(content[i])
234 + if err != nil {
235 + t.Fatal(err)
236 + }
237 + }
238 +
239 + t.Run("write", func(t *testing.T) {
240 + errs := make(chan (error), 1)
241 + for i := 0; i < files; i++ {
242 + go func() {
243 + var err error
244 + defer func() { errs <- err }()
245 +
246 + f, err := os.Create(path + strconv.Itoa(i))
247 + if err != nil {
248 + return
249 + }
250 + defer f.Close()
251 +
252 + _, err = f.Write(content[i])
253 + if err != nil {
254 + return
255 + }
256 + }()
257 + }
258 + for i := 0; i < files; i++ {
259 + err := <-errs
260 + if err != nil {
261 + t.Fatal(err)
262 + }
263 + }
264 + })
265 + t.Run("read", func(t *testing.T) {
266 + errs := make(chan (error), 1)
267 + for i := 0; i < files*fileWorkers; i++ {
268 + go func() {
269 + var err error
270 + defer func() { errs <- err }()
271 +
272 + f, err := os.Open(path + strconv.Itoa(i/fileWorkers))
273 + if err != nil {
274 + return
275 + }
276 + defer f.Close()
277 +
278 + buf := make([]byte, 8196)
279 + l, err := f.Read(buf)
280 + if err != nil {
281 + return
282 + }
283 + if bytes.Equal(content[i/fileWorkers], buf[:l]) != true {
284 + err = errors.New("read and write not equal")
285 + return
286 + }
287 + }()
288 + }
289 + for i := 0; i < files; i++ {
290 + err := <-errs
291 + if err != nil {
292 + t.Fatal(err)
293 + }
294 + }
295 + })
296 +}
297 +
298 +// Test ipfs_cid extended attribute
299 +func TestMFSRootXattr(t *testing.T) {
300 + ipfs, err := core.NewNode(context.Background(), &node.BuildCfg{})
301 + if err != nil {
302 + t.Fatal(err)
303 + }
304 +
305 + fs, mnt := setUp(t, ipfs)
306 + defer mnt.Close()
307 +
308 + node, err := fs.Root()
309 + if err != nil {
310 + t.Fatal(err)
311 + }
312 +
313 + root := node.(*Dir)
314 +
315 + listReq := fuse.ListxattrRequest{}
316 + listRes := fuse.ListxattrResponse{}
317 + err = root.Listxattr(context.Background(), &listReq, &listRes)
318 + if err != nil {
319 + t.Fatal(err)
320 + }
321 + if slices.Compare(listRes.Xattr, []byte("ipfs_cid\x00")) != 0 {
322 + t.Fatal("list xattr returns invalid value")
323 + }
324 +
325 + getReq := fuse.GetxattrRequest{
326 + Name: "ipfs_cid",
327 + }
328 + getRes := fuse.GetxattrResponse{}
329 + err = root.Getxattr(context.Background(), &getReq, &getRes)
330 + if err != nil {
331 + t.Fatal(err)
332 + }
333 +
334 + ipldNode, err := ipfs.FilesRoot.GetDirectory().GetNode()
335 + if err != nil {
336 + t.Fatal(err)
337 + }
338 +
339 + if slices.Compare(getRes.Xattr, []byte(ipldNode.Cid().String())) != 0 {
340 + t.Fatal("xattr cid not equal to mfs root cid")
341 + }
342 +}
fuse/mfs/mfs_unix.go new
+414
@@ -0,0 +1,414 @@
1 +//go:build (linux || darwin || freebsd || netbsd || openbsd) && !nofuse
2 +// +build linux darwin freebsd netbsd openbsd
3 +// +build !nofuse
4 +
5 +package mfs
6 +
7 +import (
8 + "context"
9 + "io"
10 + "os"
11 + "sync"
12 + "syscall"
13 + "time"
14 +
15 + "bazil.org/fuse"
16 + "bazil.org/fuse/fs"
17 +
18 + dag "github.com/ipfs/boxo/ipld/merkledag"
19 + ft "github.com/ipfs/boxo/ipld/unixfs"
20 + "github.com/ipfs/boxo/mfs"
21 + "github.com/ipfs/kubo/core"
22 +)
23 +
24 +const (
25 + ipfsCIDXattr = "ipfs_cid"
26 + mfsDirMode = os.ModeDir | 0755
27 + mfsFileMode = 0644
28 + blockSize = 512
29 + dirSize = 8
30 +)
31 +
32 +// FUSE filesystem mounted at /mfs.
33 +type FileSystem struct {
34 + root Dir
35 +}
36 +
37 +// Get filesystem root.
38 +func (fs *FileSystem) Root() (fs.Node, error) {
39 + return &fs.root, nil
40 +}
41 +
42 +// FUSE Adapter for MFS directories.
43 +type Dir struct {
44 + mfsDir *mfs.Directory
45 +}
46 +
47 +// Directory attributes (stat).
48 +func (dir *Dir) Attr(ctx context.Context, attr *fuse.Attr) error {
49 + attr.Mode = mfsDirMode
50 + attr.Size = dirSize * blockSize
51 + attr.Blocks = dirSize
52 + return nil
53 +}
54 +
55 +// Access files in a directory.
56 +func (dir *Dir) Lookup(ctx context.Context, req *fuse.LookupRequest, resp *fuse.LookupResponse) (fs.Node, error) {
57 + mfsNode, err := dir.mfsDir.Child(req.Name)
58 + switch err {
59 + case os.ErrNotExist:
60 + return nil, syscall.Errno(syscall.ENOENT)
61 + case nil:
62 + default:
63 + return nil, err
64 + }
65 +
66 + switch mfsNode.Type() {
67 + case mfs.TDir:
68 + result := Dir{
69 + mfsDir: mfsNode.(*mfs.Directory),
70 + }
71 + return &result, nil
72 + case mfs.TFile:
73 + result := File{
74 + mfsFile: mfsNode.(*mfs.File),
75 + }
76 + return &result, nil
77 + }
78 +
79 + return nil, syscall.Errno(syscall.ENOENT)
80 +}
81 +
82 +// List (ls) MFS directory.
83 +func (dir *Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
84 + var res []fuse.Dirent
85 + nodes, err := dir.mfsDir.List(ctx)
86 + if err != nil {
87 + return nil, err
88 + }
89 +
90 + for _, node := range nodes {
91 + nodeType := fuse.DT_File
92 + if node.Type == 1 {
93 + nodeType = fuse.DT_Dir
94 + }
95 + res = append(res, fuse.Dirent{
96 + Type: nodeType,
97 + Name: node.Name,
98 + })
99 + }
100 + return res, nil
101 +}
102 +
103 +// Mkdir (mkdir) in MFS.
104 +func (dir *Dir) Mkdir(ctx context.Context, req *fuse.MkdirRequest) (fs.Node, error) {
105 + mfsDir, err := dir.mfsDir.Mkdir(req.Name)
106 + if err != nil {
107 + return nil, err
108 + }
109 + return &Dir{
110 + mfsDir: mfsDir,
111 + }, nil
112 +}
113 +
114 +// Remove (rm/rmdir) an MFS file.
115 +func (dir *Dir) Remove(ctx context.Context, req *fuse.RemoveRequest) error {
116 + // Check for empty directory.
117 + if req.Dir {
118 + targetNode, err := dir.mfsDir.Child(req.Name)
119 + if err != nil {
120 + return err
121 + }
122 + target := targetNode.(*mfs.Directory)
123 +
124 + children, err := target.ListNames(ctx)
125 + if err != nil {
126 + return err
127 + }
128 + if len(children) > 0 {
129 + return os.ErrExist
130 + }
131 + }
132 + err := dir.mfsDir.Unlink(req.Name)
133 + if err != nil {
134 + return err
135 + }
136 + return dir.mfsDir.Flush()
137 +}
138 +
139 +// Move (mv) an MFS file.
140 +func (dir *Dir) Rename(ctx context.Context, req *fuse.RenameRequest, newDir fs.Node) error {
141 + file, err := dir.mfsDir.Child(req.OldName)
142 + if err != nil {
143 + return err
144 + }
145 + node, err := file.GetNode()
146 + if err != nil {
147 + return err
148 + }
149 + targetDir := newDir.(*Dir)
150 +
151 + // Remove file if exists
152 + err = targetDir.mfsDir.Unlink(req.NewName)
153 + if err != nil && err != os.ErrNotExist {
154 + return err
155 + }
156 +
157 + err = targetDir.mfsDir.AddChild(req.NewName, node)
158 + if err != nil {
159 + return err
160 + }
161 +
162 + err = dir.mfsDir.Unlink(req.OldName)
163 + if err != nil {
164 + return err
165 + }
166 +
167 + return dir.mfsDir.Flush()
168 +}
169 +
170 +// Create (touch) an MFS file.
171 +func (dir *Dir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) {
172 + node := dag.NodeWithData(ft.FilePBData(nil, 0))
173 + if err := node.SetCidBuilder(dir.mfsDir.GetCidBuilder()); err != nil {
174 + return nil, nil, err
175 + }
176 +
177 + if err := dir.mfsDir.AddChild(req.Name, node); err != nil {
178 + return nil, nil, err
179 + }
180 +
181 + if err := dir.mfsDir.Flush(); err != nil {
182 + return nil, nil, err
183 + }
184 +
185 + mfsNode, err := dir.mfsDir.Child(req.Name)
186 + if err != nil {
187 + return nil, nil, err
188 + }
189 + if err := mfsNode.SetModTime(time.Now()); err != nil {
190 + return nil, nil, err
191 + }
192 +
193 + mfsFile := mfsNode.(*mfs.File)
194 +
195 + file := File{
196 + mfsFile: mfsFile,
197 + }
198 +
199 + // Read access flags and create a handler.
200 + accessMode := req.Flags & fuse.OpenAccessModeMask
201 + flags := mfs.Flags{
202 + Read: accessMode == fuse.OpenReadOnly || accessMode == fuse.OpenReadWrite,
203 + Write: accessMode == fuse.OpenWriteOnly || accessMode == fuse.OpenReadWrite,
204 + Sync: req.Flags|fuse.OpenSync > 0,
205 + }
206 +
207 + fd, err := mfsFile.Open(flags)
208 + if err != nil {
209 + return nil, nil, err
210 + }
211 + handler := FileHandler{
212 + mfsFD: fd,
213 + }
214 +
215 + return &file, &handler, nil
216 +}
217 +
218 +// List dir xattr.
219 +func (dir *Dir) Listxattr(ctx context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error {
220 + resp.Append(ipfsCIDXattr)
221 + return nil
222 +}
223 +
224 +// Get dir xattr.
225 +func (dir *Dir) Getxattr(ctx context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
226 + switch req.Name {
227 + case ipfsCIDXattr:
228 + node, err := dir.mfsDir.GetNode()
229 + if err != nil {
230 + return err
231 + }
232 + resp.Xattr = []byte(node.Cid().String())
233 + return nil
234 + default:
235 + return fuse.ErrNoXattr
236 + }
237 +}
238 +
239 +// FUSE adapter for MFS files.
240 +type File struct {
241 + mfsFile *mfs.File
242 +}
243 +
244 +// File attributes.
245 +func (file *File) Attr(ctx context.Context, attr *fuse.Attr) error {
246 + size, _ := file.mfsFile.Size()
247 +
248 + attr.Size = uint64(size)
249 + if size%blockSize == 0 {
250 + attr.Blocks = uint64(size / blockSize)
251 + } else {
252 + attr.Blocks = uint64(size/blockSize + 1)
253 + }
254 +
255 + mtime, _ := file.mfsFile.ModTime()
256 + attr.Mtime = mtime
257 +
258 + attr.Mode = mfsFileMode
259 + return nil
260 +}
261 +
262 +// Open an MFS file.
263 +func (file *File) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) {
264 + accessMode := req.Flags & fuse.OpenAccessModeMask
265 + flags := mfs.Flags{
266 + Read: accessMode == fuse.OpenReadOnly || accessMode == fuse.OpenReadWrite,
267 + Write: accessMode == fuse.OpenWriteOnly || accessMode == fuse.OpenReadWrite,
268 + Sync: req.Flags|fuse.OpenSync > 0,
269 + }
270 + fd, err := file.mfsFile.Open(flags)
271 + if err != nil {
272 + return nil, err
273 + }
274 +
275 + if flags.Write {
276 + if err := file.mfsFile.SetModTime(time.Now()); err != nil {
277 + return nil, err
278 + }
279 + }
280 +
281 + return &FileHandler{
282 + mfsFD: fd,
283 + }, nil
284 +}
285 +
286 +// Sync the file's contents to MFS.
287 +func (file *File) Fsync(ctx context.Context, req *fuse.FsyncRequest) error {
288 + return file.mfsFile.Sync()
289 +}
290 +
291 +// List file xattr.
292 +func (file *File) Listxattr(ctx context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error {
293 + resp.Append(ipfsCIDXattr)
294 + return nil
295 +}
296 +
297 +// Get file xattr.
298 +func (file *File) Getxattr(ctx context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
299 + switch req.Name {
300 + case ipfsCIDXattr:
301 + node, err := file.mfsFile.GetNode()
302 + if err != nil {
303 + return err
304 + }
305 + resp.Xattr = []byte(node.Cid().String())
306 + return nil
307 + default:
308 + return fuse.ErrNoXattr
309 + }
310 +}
311 +
312 +// Wrapper for MFS's file descriptor that conforms to the FUSE fs.Handler
313 +// interface.
314 +type FileHandler struct {
315 + mfsFD mfs.FileDescriptor
316 + mu sync.Mutex
317 +}
318 +
319 +// Read a opened MFS file.
320 +func (fh *FileHandler) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
321 + fh.mu.Lock()
322 + defer fh.mu.Unlock()
323 +
324 + _, err := fh.mfsFD.Seek(req.Offset, io.SeekStart)
325 + if err != nil {
326 + return err
327 + }
328 +
329 + buf := make([]byte, req.Size)
330 + l, err := fh.mfsFD.Read(buf)
331 +
332 + resp.Data = buf[:l]
333 +
334 + switch err {
335 + case nil, io.EOF, io.ErrUnexpectedEOF:
336 + return nil
337 + default:
338 + return err
339 + }
340 +}
341 +
342 +// Write writes to an opened MFS file.
343 +func (fh *FileHandler) Write(ctx context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) error {
344 + fh.mu.Lock()
345 + defer fh.mu.Unlock()
346 +
347 + l, err := fh.mfsFD.WriteAt(req.Data, req.Offset)
348 + if err != nil {
349 + return err
350 + }
351 + resp.Size = l
352 +
353 + return nil
354 +}
355 +
356 +// Flushes the file's buffer.
357 +func (fh *FileHandler) Flush(ctx context.Context, req *fuse.FlushRequest) error {
358 + fh.mu.Lock()
359 + defer fh.mu.Unlock()
360 +
361 + return fh.mfsFD.Flush()
362 +}
363 +
364 +// Closes the file.
365 +func (fh *FileHandler) Release(ctx context.Context, req *fuse.ReleaseRequest) error {
366 + fh.mu.Lock()
367 + defer fh.mu.Unlock()
368 +
369 + return fh.mfsFD.Close()
370 +}
371 +
372 +// Create new filesystem.
373 +func NewFileSystem(ipfs *core.IpfsNode) fs.FS {
374 + return &FileSystem{
375 + root: Dir{
376 + mfsDir: ipfs.FilesRoot.GetDirectory(),
377 + },
378 + }
379 +}
380 +
381 +// Check that our structs implement all the interfaces we want.
382 +type mfsDir interface {
383 + fs.Node
384 + fs.NodeGetxattrer
385 + fs.NodeListxattrer
386 + fs.HandleReadDirAller
387 + fs.NodeRequestLookuper
388 + fs.NodeMkdirer
389 + fs.NodeRenamer
390 + fs.NodeRemover
391 + fs.NodeCreater
392 +}
393 +
394 +var _ mfsDir = (*Dir)(nil)
395 +
396 +type mfsFile interface {
397 + fs.Node
398 + fs.NodeGetxattrer
399 + fs.NodeListxattrer
400 + fs.NodeOpener
401 + fs.NodeFsyncer
402 +}
403 +
404 +var _ mfsFile = (*File)(nil)
405 +
406 +type mfsHandler interface {
407 + fs.Handle
408 + fs.HandleReader
409 + fs.HandleWriter
410 + fs.HandleFlusher
411 + fs.HandleReleaser
412 +}
413 +
414 +var _ mfsHandler = (*FileHandler)(nil)
fuse/mfs/mount_unix.go new
+21
@@ -0,0 +1,21 @@
1 +//go:build (linux || darwin || freebsd || netbsd || openbsd) && !nofuse
2 +// +build linux darwin freebsd netbsd openbsd
3 +// +build !nofuse
4 +
5 +package mfs
6 +
7 +import (
8 + core "github.com/ipfs/kubo/core"
9 + mount "github.com/ipfs/kubo/fuse/mount"
10 +)
11 +
12 +// Mount mounts MFS at a given location, and returns a mount.Mount instance.
13 +func Mount(ipfs *core.IpfsNode, mountpoint string) (mount.Mount, error) {
14 + cfg, err := ipfs.Repo.Config()
15 + if err != nil {
16 + return nil, err
17 + }
18 + allowOther := cfg.Mounts.FuseAllowOther
19 + fsys := NewFileSystem(ipfs)
20 + return mount.NewMount(ipfs.Process, fsys, mountpoint, allowOther)
21 +}
fuse/node/mount_nofuse.go
+1 -1
@@ -9,6 +9,6 @@ import (
9 core "github.com/ipfs/kubo/core"
10 )
11
12 -func Mount(node *core.IpfsNode, fsdir, nsdir string) error {
12 +func Mount(node *core.IpfsNode, fsdir, nsdir, mfsdir string) error {
13 return errors.New("not compiled in")
14 }
fuse/node/mount_notsupp.go
+1 -1
@@ -9,6 +9,6 @@ import (
9 core "github.com/ipfs/kubo/core"
10 )
11
12 -func Mount(node *core.IpfsNode, fsdir, nsdir string) error {
12 +func Mount(node *core.IpfsNode, fsdir, nsdir, mfsdir string) error {
13 return errors.New("FUSE not supported on OpenBSD or NetBSD. See #5334 (https://github.com/ipfs/kubo/issues/5334).")
14 }
fuse/node/mount_test.go
+3 -1
@@ -56,10 +56,12 @@ func TestExternalUnmount(t *testing.T) {
56
57 ipfsDir := dir + "/ipfs"
58 ipnsDir := dir + "/ipns"
59 + mfsDir := dir + "/mfs"
60 mkdir(t, ipfsDir)
61 mkdir(t, ipnsDir)
62 + mkdir(t, mfsDir)
63
62 - err = Mount(node, ipfsDir, ipnsDir)
64 + err = Mount(node, ipfsDir, ipnsDir, mfsDir)
65 if err != nil {
66 if strings.Contains(err.Error(), "unable to check fuse version") || err == fuse.ErrOSXFUSENotFound {
67 t.Skip(err)
fuse/node/mount_unix.go
+29 -7
@@ -11,6 +11,7 @@ import (
11
12 core "github.com/ipfs/kubo/core"
13 ipns "github.com/ipfs/kubo/fuse/ipns"
14 + mfs "github.com/ipfs/kubo/fuse/mfs"
15 mount "github.com/ipfs/kubo/fuse/mount"
16 rofs "github.com/ipfs/kubo/fuse/readonly"
17
@@ -31,7 +32,7 @@ var platformFuseChecks = func(*core.IpfsNode) error {
32 return nil
33 }
34
34 -func Mount(node *core.IpfsNode, fsdir, nsdir string) error {
35 +func Mount(node *core.IpfsNode, fsdir, nsdir, mfsdir string) error {
36 // check if we already have live mounts.
37 // if the user said "Mount", then there must be something wrong.
38 // so, close them and try again.
@@ -43,15 +44,19 @@ func Mount(node *core.IpfsNode, fsdir, nsdir string) error {
44 // best effort
45 _ = node.Mounts.Ipns.Unmount()
46 }
47 + if node.Mounts.Mfs != nil && node.Mounts.Mfs.IsActive() {
48 + // best effort
49 + _ = node.Mounts.Mfs.Unmount()
50 + }
51
52 if err := platformFuseChecks(node); err != nil {
53 return err
54 }
55
51 - return doMount(node, fsdir, nsdir)
56 + return doMount(node, fsdir, nsdir, mfsdir)
57 }
58
54 -func doMount(node *core.IpfsNode, fsdir, nsdir string) error {
59 +func doMount(node *core.IpfsNode, fsdir, nsdir, mfsdir string) error {
60 fmtFuseErr := func(err error, mountpoint string) error {
61 s := err.Error()
62 if strings.Contains(s, fuseNoDirectory) {
@@ -67,8 +72,8 @@ func doMount(node *core.IpfsNode, fsdir, nsdir string) error {
72 }
73
74 // this sync stuff is so that both can be mounted simultaneously.
70 - var fsmount, nsmount mount.Mount
71 - var err1, err2 error
75 + var fsmount, nsmount, mfmount mount.Mount
76 + var err1, err2, err3 error
77
78 var wg sync.WaitGroup
79
@@ -86,6 +91,12 @@ func doMount(node *core.IpfsNode, fsdir, nsdir string) error {
91 }()
92 }
93
94 + wg.Add(1)
95 + go func() {
96 + defer wg.Done()
97 + mfmount, err3 = mfs.Mount(node, mfsdir)
98 + }()
99 +
100 wg.Wait()
101
102 if err1 != nil {
@@ -96,22 +107,33 @@ func doMount(node *core.IpfsNode, fsdir, nsdir string) error {
107 log.Errorf("error mounting IPNS %s for IPFS %s: %s", nsdir, fsdir, err2)
108 }
109
99 - if err1 != nil || err2 != nil {
110 + if err3 != nil {
111 + log.Errorf("error mounting MFS %s: %s", mfsdir, err3)
112 + }
113 +
114 + if err1 != nil || err2 != nil || err3 != nil {
115 if fsmount != nil {
116 _ = fsmount.Unmount()
117 }
118 if nsmount != nil {
119 _ = nsmount.Unmount()
120 }
121 + if mfmount != nil {
122 + _ = mfmount.Unmount()
123 + }
124
125 if err1 != nil {
126 return fmtFuseErr(err1, fsdir)
127 }
110 - return fmtFuseErr(err2, nsdir)
128 + if err2 != nil {
129 + return fmtFuseErr(err2, nsdir)
130 + }
131 + return fmtFuseErr(err3, mfsdir)
132 }
133
134 // setup node state, so that it can be canceled
135 node.Mounts.Ipfs = fsmount
136 node.Mounts.Ipns = nsmount
137 + node.Mounts.Mfs = mfmount
138 return nil
139 }
fuse/node/mount_windows.go
+1 -1
@@ -4,7 +4,7 @@ import (
4 "github.com/ipfs/kubo/core"
5 )
6
7 -func Mount(node *core.IpfsNode, fsdir, nsdir string) error {
7 +func Mount(node *core.IpfsNode, fsdir, nsdir, mfsdir string) error {
8 // TODO
9 // currently a no-op, but we don't want to return an error
10 return nil
test/3nodetest/bootstrap/config
+2 -1
@@ -15,7 +15,8 @@
15 },
16 "Mounts": {
17 "IPFS": "/ipfs",
18 - "IPNS": "/ipns"
18 + "IPNS": "/ipns",
19 + "MFS": "/mfs"
20 },
21 "Version": {
22 "Current": "0.1.7",
test/3nodetest/client/config
+2 -1
@@ -17,7 +17,8 @@
17 },
18 "Mounts": {
19 "IPFS": "/ipfs",
20 - "IPNS": "/ipns"
20 + "IPNS": "/ipns",
21 + "MFS": "/mfs"
22 },
23 "Version": {
24 "AutoUpdate": "minor",
test/3nodetest/server/config
+2 -1
@@ -17,7 +17,8 @@
17 },
18 "Mounts": {
19 "IPFS": "/ipfs",
20 - "IPNS": "/ipns"
20 + "IPNS": "/ipns",
21 + "MFS": "/mfs"
22 },
23 "Version": {
24 "AutoUpdate": "minor",
test/cli/bitswap_config_test.go
+1 -1
@@ -167,7 +167,7 @@ func TestBitswapConfig(t *testing.T) {
167 node.UpdateConfig(func(cfg *config.Config) {
168 cfg.HTTPRetrieval.Enabled = config.False
169 cfg.Bitswap.Libp2pEnabled = config.False
170 - cfg.Bitswap.ServerEnabled = config.True // bad user config: cant enable server when libp2p is down
170 + cfg.Bitswap.ServerEnabled = config.True // bad user config: can't enable server when libp2p is down
171 })
172 res := node.RunIPFS("daemon")
173 assert.Contains(t, res.Stderr.Trimmed(), "invalid configuration: Bitswap.Libp2pEnabled and HTTPRetrieval.Enabled are both disabled, unable to initialize Bitswap")
test/sharness/lib/test-lib.sh
+5 -2
@@ -206,9 +206,10 @@ test_init_ipfs() {
206 '
207
208 test_expect_success "prepare config -- mounting" '
209 - mkdir mountdir ipfs ipns &&
209 + mkdir mountdir ipfs ipns mfs &&
210 test_config_set Mounts.IPFS "$(pwd)/ipfs" &&
211 - test_config_set Mounts.IPNS "$(pwd)/ipns" ||
211 + test_config_set Mounts.IPNS "$(pwd)/ipns" &&
212 + test_config_set Mounts.MFS "$(pwd)/mfs" ||
213 test_fsh cat "\"$IPFS_PATH/config\""
214 '
215
@@ -321,12 +322,14 @@ test_mount_ipfs() {
322 test_expect_success FUSE "'ipfs mount' succeeds" '
323 do_umount "$(pwd)/ipfs" || true &&
324 do_umount "$(pwd)/ipns" || true &&
325 + do_umount "$(pwd)/mfs" || true &&
326 ipfs mount >actual
327 '
328
329 test_expect_success FUSE "'ipfs mount' output looks good" '
330 echo "IPFS mounted at: $(pwd)/ipfs" >expected &&
331 echo "IPNS mounted at: $(pwd)/ipns" >>expected &&
332 + echo "MFS mounted at: $(pwd)/mfs" >>expected &&
333 test_cmp expected actual
334 '
335
test/sharness/t0030-mount.sh
+55 -9
@@ -16,7 +16,8 @@ if ! test_have_prereq FUSE; then
16 fi
17
18
19 -export IPFS_NS_MAP="welcome.example.com:/ipfs/$HASH_WELCOME_DOCS"
19 +# echo -n "ipfs" > expected && ipfs add --cid-version 1 -Q -w expected
20 +export IPFS_NS_MAP="welcome.example.com:/ipfs/bafybeicq7bvn5lz42qlmghaoiwrve74pzi53auqetbantp5kajucsabike"
21
22 # start iptb + wait for peering
23 NUM_NODES=5
@@ -27,17 +28,17 @@ startup_cluster $NUM_NODES
28
29 # test mount failure before mounting properly.
30 test_expect_success "'ipfs mount' fails when there is no mount dir" '
30 - tmp_ipfs_mount() { ipfsi 0 mount -f=not_ipfs -n=not_ipns >output 2>output.err; } &&
31 + tmp_ipfs_mount() { ipfsi 0 mount -f=not_ipfs -n=not_ipns -m=not_mfs >output 2>output.err; } &&
32 test_must_fail tmp_ipfs_mount
33 '
34
35 test_expect_success "'ipfs mount' output looks good" '
36 test_must_be_empty output &&
36 - test_should_contain "not_ipns\|not_ipfs" output.err
37 + test_should_contain "not_ipns\|not_ipfs\|not_mfs" output.err
38 '
39
40 test_expect_success "setup and publish default IPNS value" '
40 - mkdir "$(pwd)/ipfs" "$(pwd)/ipns" &&
41 + mkdir "$(pwd)/ipfs" "$(pwd)/ipns" "$(pwd)/mfs" &&
42 ipfsi 0 name publish QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn
43 '
44
@@ -46,12 +47,14 @@ test_expect_success "setup and publish default IPNS value" '
47 test_expect_success FUSE "'ipfs mount' succeeds" '
48 do_umount "$(pwd)/ipfs" || true &&
49 do_umount "$(pwd)/ipns" || true &&
49 - ipfsi 0 mount -f "$(pwd)/ipfs" -n "$(pwd)/ipns" >actual
50 + do_umount "$(pwd)/mfs" || true &&
51 + ipfsi 0 mount -f "$(pwd)/ipfs" -n "$(pwd)/ipns" -m "$(pwd)/mfs" >actual
52 '
53
54 test_expect_success FUSE "'ipfs mount' output looks good" '
55 echo "IPFS mounted at: $(pwd)/ipfs" >expected &&
56 echo "IPNS mounted at: $(pwd)/ipns" >>expected &&
57 + echo "MFS mounted at: $(pwd)/mfs" >>expected &&
58 test_cmp expected actual
59 '
60
@@ -63,21 +66,64 @@ test_expect_success FUSE "local symlink works" '
66
67 test_expect_success FUSE "can resolve ipns names" '
68 echo -n "ipfs" > expected &&
66 - cat ipns/welcome.example.com/ping > actual &&
69 + ipfsi 0 add --cid-version 1 -Q -w expected &&
70 + cat ipns/welcome.example.com/expected > actual &&
71 test_cmp expected actual
72 '
73
74 +test_expect_success FUSE "create mfs file via fuse" '
75 + touch mfs/testfile &&
76 + ipfsi 0 files ls | grep testfile
77 +'
78 +
79 +test_expect_success FUSE "create mfs dir via fuse" '
80 + mkdir mfs/testdir &&
81 + ipfsi 0 files ls | grep testdir
82 +'
83 +
84 +test_expect_success FUSE "read mfs file from fuse" '
85 + echo content > mfs/testfile &&
86 + getfattr -n ipfs_cid mfs/testfile
87 +'
88 +test_expect_success FUSE "ipfs add file and read it back via fuse" '
89 + echo content3 | ipfsi 0 files write -e /testfile3 &&
90 + grep content3 mfs/testfile3
91 +'
92 +
93 +test_expect_success FUSE "ipfs add file and read it back via fuse" '
94 + echo content > testfile2 &&
95 + ipfsi 0 add --to-files /testfile2 testfile2 &&
96 + grep content mfs/testfile2
97 +'
98 +
99 +test_expect_success FUSE "test file xattr" '
100 + echo content > mfs/testfile &&
101 + getfattr -n ipfs_cid mfs/testfile
102 +'
103 +
104 +test_expect_success FUSE "test file removal" '
105 + touch mfs/testfile &&
106 + rm mfs/testfile
107 +'
108 +
109 +test_expect_success FUSE "test nested dirs" '
110 + mkdir -p mfs/foo/bar/baz/qux &&
111 + echo content > mfs/foo/bar/baz/qux/quux &&
112 + ipfsi 0 files stat /foo/bar/baz/qux/quux
113 +'
114 +
115 test_expect_success "mount directories cannot be removed while active" '
71 - test_must_fail rmdir ipfs ipns 2>/dev/null
116 + test_must_fail rmdir ipfs ipns mfs 2>/dev/null
117 '
118
119 test_expect_success "unmount directories" '
120 do_umount "$(pwd)/ipfs" &&
76 - do_umount "$(pwd)/ipns"
121 + do_umount "$(pwd)/ipns" &&
122 + do_umount "$(pwd)/mfs"
123 '
124
125 test_expect_success "mount directories can be removed after shutdown" '
80 - rmdir ipfs ipns
126 + rmdir ipfs ipns mfs
127 '
128
129 test_expect_success 'stop iptb' '
test/sharness/t0270-filestore.sh
+1 -1
@@ -63,7 +63,7 @@ test_filestore_adds() {
63
64 init_ipfs_filestore() {
65 test_expect_success "clean up old node" '
66 - rm -rf "$IPFS_PATH" mountdir ipfs ipns
66 + rm -rf "$IPFS_PATH" mountdir ipfs ipns mfs
67 '
68
69 test_init_ipfs
test/sharness/t0271-filestore-utils.sh
+1 -1
@@ -10,7 +10,7 @@ test_description="Test out the filestore nocopy functionality"
10
11 test_init_filestore() {
12 test_expect_success "clean up old node" '
13 - rm -rf "$IPFS_PATH" mountdir ipfs ipns
13 + rm -rf "$IPFS_PATH" mountdir ipfs ipns mfs
14 '
15
16 test_init_ipfs