mirror of
https://github.com/ipfs/kubo.git
synced 2025-05-17 23:16:11 +08:00
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>
This commit is contained in:
@ -56,6 +56,7 @@ const (
|
||||
initProfileOptionKwd = "init-profile"
|
||||
ipfsMountKwd = "mount-ipfs"
|
||||
ipnsMountKwd = "mount-ipns"
|
||||
mfsMountKwd = "mount-mfs"
|
||||
migrateKwd = "migrate"
|
||||
mountKwd = "mount"
|
||||
offlineKwd = "offline" // global option
|
||||
@ -173,6 +174,7 @@ Headers.
|
||||
cmds.BoolOption(mountKwd, "Mounts IPFS to the filesystem using FUSE (experimental)"),
|
||||
cmds.StringOption(ipfsMountKwd, "Path to the mountpoint for IPFS (if using --mount). Defaults to config setting."),
|
||||
cmds.StringOption(ipnsMountKwd, "Path to the mountpoint for IPNS (if using --mount). Defaults to config setting."),
|
||||
cmds.StringOption(mfsMountKwd, "Path to the mountpoint for MFS (if using --mount). Defaults to config setting."),
|
||||
cmds.BoolOption(unrestrictedAPIAccessKwd, "Allow RPC API access to unlisted hashes"),
|
||||
cmds.BoolOption(unencryptTransportKwd, "Disable transport encryption (for debugging protocols)"),
|
||||
cmds.BoolOption(enableGCKwd, "Enable automatic periodic repo garbage collection"),
|
||||
@ -1062,17 +1064,23 @@ func mountFuse(req *cmds.Request, cctx *oldcmds.Context) error {
|
||||
nsdir = cfg.Mounts.IPNS
|
||||
}
|
||||
|
||||
mfsdir, found := req.Options[mfsMountKwd].(string)
|
||||
if !found {
|
||||
mfsdir = cfg.Mounts.MFS
|
||||
}
|
||||
|
||||
node, err := cctx.ConstructNode()
|
||||
if err != nil {
|
||||
return fmt.Errorf("mountFuse: ConstructNode() failed: %s", err)
|
||||
}
|
||||
|
||||
err = nodeMount.Mount(node, fsdir, nsdir)
|
||||
err = nodeMount.Mount(node, fsdir, nsdir, mfsdir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("IPFS mounted at: %s\n", fsdir)
|
||||
fmt.Printf("IPNS mounted at: %s\n", nsdir)
|
||||
fmt.Printf("MFS mounted at: %s\n", mfsdir)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,7 @@ func InitWithIdentity(identity Identity) (*Config, error) {
|
||||
Mounts: Mounts{
|
||||
IPFS: "/ipfs",
|
||||
IPNS: "/ipns",
|
||||
MFS: "/mfs",
|
||||
},
|
||||
|
||||
Ipns: Ipns{
|
||||
|
@ -4,5 +4,6 @@ package config
|
||||
type Mounts struct {
|
||||
IPFS string
|
||||
IPNS string
|
||||
MFS string
|
||||
FuseAllowOther bool
|
||||
}
|
||||
|
@ -14,10 +14,11 @@ var MountCmd = &cmds.Command{
|
||||
ShortDescription: `
|
||||
This version of ipfs is compiled without fuse support, which is required
|
||||
for mounting. If you'd like to be able to mount, please use a version of
|
||||
ipfs compiled with fuse.
|
||||
Kubo compiled with fuse.
|
||||
|
||||
For the latest instructions, please check the project's repository:
|
||||
http://github.com/ipfs/go-ipfs
|
||||
http://github.com/ipfs/kubo
|
||||
https://github.com/ipfs/kubo/blob/master/docs/fuse.md
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
const (
|
||||
mountIPFSPathOptionName = "ipfs-path"
|
||||
mountIPNSPathOptionName = "ipns-path"
|
||||
mountMFSPathOptionName = "mfs-path"
|
||||
)
|
||||
|
||||
var MountCmd = &cmds.Command{
|
||||
@ -25,14 +26,14 @@ var MountCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Mounts IPFS to the filesystem (read-only).",
|
||||
ShortDescription: `
|
||||
Mount IPFS at a read-only mountpoint on the OS (default: /ipfs and /ipns).
|
||||
Mount IPFS at a read-only mountpoint on the OS (default: /ipfs, /ipns, /mfs).
|
||||
All IPFS objects will be accessible under that directory. Note that the
|
||||
root will not be listable, as it is virtual. Access known paths directly.
|
||||
|
||||
You may have to create /ipfs and /ipns before using 'ipfs mount':
|
||||
|
||||
> sudo mkdir /ipfs /ipns
|
||||
> sudo chown $(whoami) /ipfs /ipns
|
||||
> sudo mkdir /ipfs /ipns /mfs
|
||||
> sudo chown $(whoami) /ipfs /ipns /mfs
|
||||
> ipfs daemon &
|
||||
> ipfs mount
|
||||
`,
|
||||
@ -44,8 +45,8 @@ root will not be listable, as it is virtual. Access known paths directly.
|
||||
|
||||
You may have to create /ipfs and /ipns before using 'ipfs mount':
|
||||
|
||||
> sudo mkdir /ipfs /ipns
|
||||
> sudo chown $(whoami) /ipfs /ipns
|
||||
> sudo mkdir /ipfs /ipns /mfs
|
||||
> sudo chown $(whoami) /ipfs /ipns /mfs
|
||||
> ipfs daemon &
|
||||
> ipfs mount
|
||||
|
||||
@ -67,6 +68,7 @@ baz
|
||||
> ipfs mount
|
||||
IPFS mounted at: /ipfs
|
||||
IPNS mounted at: /ipns
|
||||
MFS mounted at: /mfs
|
||||
> cd /ipfs/QmSh5e7S6fdcu75LAbXNZAFY2nGyZUJXyLCJDvn2zRkWyC
|
||||
> ls
|
||||
bar
|
||||
@ -81,6 +83,7 @@ baz
|
||||
Options: []cmds.Option{
|
||||
cmds.StringOption(mountIPFSPathOptionName, "f", "The path where IPFS should be mounted."),
|
||||
cmds.StringOption(mountIPNSPathOptionName, "n", "The path where IPNS should be mounted."),
|
||||
cmds.StringOption(mountMFSPathOptionName, "m", "The path where MFS should be mounted."),
|
||||
},
|
||||
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
||||
cfg, err := env.(*oldcmds.Context).GetConfig()
|
||||
@ -109,7 +112,12 @@ baz
|
||||
nsdir = cfg.Mounts.IPNS // NB: be sure to not redeclare!
|
||||
}
|
||||
|
||||
err = nodeMount.Mount(nd, fsdir, nsdir)
|
||||
mfsdir, found := req.Options[mountMFSPathOptionName].(string)
|
||||
if !found {
|
||||
mfsdir = cfg.Mounts.MFS
|
||||
}
|
||||
|
||||
err = nodeMount.Mount(nd, fsdir, nsdir, mfsdir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -117,6 +125,7 @@ baz
|
||||
var output config.Mounts
|
||||
output.IPFS = fsdir
|
||||
output.IPNS = nsdir
|
||||
output.MFS = mfsdir
|
||||
return cmds.EmitOnce(res, &output)
|
||||
},
|
||||
Type: config.Mounts{},
|
||||
@ -124,6 +133,7 @@ baz
|
||||
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, mounts *config.Mounts) error {
|
||||
fmt.Fprintf(w, "IPFS mounted at: %s\n", cmdenv.EscNonPrint(mounts.IPFS))
|
||||
fmt.Fprintf(w, "IPNS mounted at: %s\n", cmdenv.EscNonPrint(mounts.IPNS))
|
||||
fmt.Fprintf(w, "MFS mounted at: %s\n", cmdenv.EscNonPrint(mounts.MFS))
|
||||
|
||||
return nil
|
||||
}),
|
||||
|
@ -134,6 +134,7 @@ type IpfsNode struct {
|
||||
type Mounts struct {
|
||||
Ipfs mount.Mount
|
||||
Ipns mount.Mount
|
||||
Mfs mount.Mount
|
||||
}
|
||||
|
||||
// Close calls Close() on the App object
|
||||
|
@ -12,6 +12,7 @@ This release was brought to you by the [Shipyard](http://ipshipyard.com/) team.
|
||||
- [🔦 Highlights](#-highlights)
|
||||
- [Opt-in HTTP Retrieval client](#opt-in-http-retrieval-client)
|
||||
- [Dedicated `Reprovider.Strategy` for MFS](#dedicated-reproviderstrategy-for-mfs)
|
||||
- [Experimental support for MFS as a FUSE mount point](#experimental-support-for-mfs-as-a-fuse-mount-point)
|
||||
- [Grid view in WebUI](#grid-view-in-webui)
|
||||
- [Enhanced DAG-Shaping Controls](#enhanced-dag-shaping-controls)
|
||||
- [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
|
||||
|
||||
See [`Reprovider.Strategy`](https://github.com/ipfs/kubo/blob/master/docs/config.md#reproviderstrategy) for more details.
|
||||
|
||||
#### Experimental support for MFS as a FUSE mount point
|
||||
|
||||
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`.
|
||||
|
||||
Note that the operations supported by the MFS FUSE mountpoint are limited, since MFS doesn't store file attributes.
|
||||
|
||||
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.
|
||||
|
||||
#### Grid view in WebUI
|
||||
|
||||
The WebUI, accessible at http://127.0.0.1:5001/webui/, now includes support for the grid view on the _Files_ screen:
|
||||
|
@ -97,6 +97,7 @@ config file at runtime.
|
||||
- [`Mounts`](#mounts)
|
||||
- [`Mounts.IPFS`](#mountsipfs)
|
||||
- [`Mounts.IPNS`](#mountsipns)
|
||||
- [`Mounts.MFS`](#mountsmfs)
|
||||
- [`Mounts.FuseAllowOther`](#mountsfuseallowother)
|
||||
- [`Pinning`](#pinning)
|
||||
- [`Pinning.RemoteServices`](#pinningremoteservices)
|
||||
@ -1368,7 +1369,8 @@ Default: `cache`
|
||||
|
||||
## `Mounts`
|
||||
|
||||
**EXPERIMENTAL:** read about current limitations at [fuse.md](./fuse.md).
|
||||
> [!CAUTION]
|
||||
> **EXPERIMENTAL:** read about current limitations at [fuse.md](./fuse.md).
|
||||
|
||||
FUSE mount point configuration options.
|
||||
|
||||
@ -1388,6 +1390,18 @@ Default: `/ipns`
|
||||
|
||||
Type: `string` (filesystem path)
|
||||
|
||||
### `Mounts.MFS`
|
||||
|
||||
Mountpoint for Mutable File System (MFS) behind the `ipfs files` API.
|
||||
|
||||
> [!CAUTION]
|
||||
> - Write support is highly experimental and not recommended for mission-critical deployments.
|
||||
> - 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.
|
||||
|
||||
Default: `/mfs`
|
||||
|
||||
Type: `string` (filesystem path)
|
||||
|
||||
### `Mounts.FuseAllowOther`
|
||||
|
||||
Sets the 'FUSE allow other'-option on the mount point.
|
||||
|
@ -404,7 +404,7 @@ We also support the use of protocol names of the form /x/$NAME/http where $NAME
|
||||
|
||||
## FUSE
|
||||
|
||||
FUSE makes it possible to mount `/ipfs` and `/ipns` namespaces in your OS,
|
||||
FUSE makes it possible to mount `/ipfs`, `/ipns` and `/mfs` namespaces in your OS,
|
||||
allowing arbitrary apps access to IPFS using a subset of filesystem abstractions.
|
||||
|
||||
It is considered EXPERIMENTAL due to limited (and buggy) support on some platforms.
|
||||
|
28
docs/fuse.md
28
docs/fuse.md
@ -2,7 +2,7 @@
|
||||
|
||||
**EXPERIMENTAL:** FUSE support is limited, YMMV.
|
||||
|
||||
Kubo makes it possible to mount `/ipfs` and `/ipns` namespaces in your OS,
|
||||
Kubo makes it possible to mount `/ipfs`, `/ipns` and `/mfs` namespaces in your OS,
|
||||
allowing arbitrary apps access to IPFS.
|
||||
|
||||
## Install FUSE
|
||||
@ -50,18 +50,20 @@ speak with us, or if you figure something new out, please add to this document!
|
||||
|
||||
## Prepare mountpoints
|
||||
|
||||
By default ipfs uses `/ipfs` and `/ipns` directories for mounting, this can be
|
||||
changed in config. You will have to create the `/ipfs` and `/ipns` directories
|
||||
By default ipfs uses `/ipfs`, `/ipns` and `/mfs` directories for mounting, this can be
|
||||
changed in config. You will have to create the `/ipfs`, `/ipns` and `/mfs` directories
|
||||
explicitly. Note that modifying root requires sudo permissions.
|
||||
|
||||
```sh
|
||||
# make the directories
|
||||
sudo mkdir /ipfs
|
||||
sudo mkdir /ipns
|
||||
sudo mkdir /mfs
|
||||
|
||||
# chown them so ipfs can use them without root permissions
|
||||
sudo chown <username> /ipfs
|
||||
sudo chown <username> /ipns
|
||||
sudo chown <username> /mfs
|
||||
```
|
||||
|
||||
Depending on whether you are using OSX or Linux, follow the proceeding instructions.
|
||||
@ -105,6 +107,25 @@ ipfs config --json Mounts.FuseAllowOther true
|
||||
ipfs daemon --mount
|
||||
```
|
||||
|
||||
## MFS mountpoint
|
||||
|
||||
Kubo v0.35.0 and later supports mounting the MFS (Mutable File System) root as
|
||||
a FUSE filesystem, enabling manipulation of content-addressed data like regular
|
||||
files. The CID for any file or directory is retrievable via the `ipfs_cid`
|
||||
extended attribute.
|
||||
|
||||
```sh
|
||||
getfattr -n ipfs_cid /mfs/welcome-to-IPFS.jpg
|
||||
getfattr: Removing leading '/' from absolute path names
|
||||
# file: mfs/welcome-to-IPFS.jpg
|
||||
ipfs_cid="QmaeXDdwpUeKQcMy7d5SFBfVB4y7LtREbhm5KizawPsBSH"
|
||||
```
|
||||
|
||||
Please note that the operations supported by the MFS FUSE mountpoint are
|
||||
limited. Since the MFS wasn't designed to store file attributes like ownership
|
||||
information, permissions and creation date, some applications like `vim` and
|
||||
`sed` may misbehave due to missing functionality.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
#### `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.
|
||||
```
|
||||
sudo umount /ipfs
|
||||
sudo umount /ipns
|
||||
sudo umount /mfs
|
||||
```
|
||||
|
||||
#### Mounting fails with "error mounting: could not resolve name"
|
||||
|
342
fuse/mfs/mfs_test.go
Normal file
342
fuse/mfs/mfs_test.go
Normal file
@ -0,0 +1,342 @@
|
||||
//go:build !nofuse && !openbsd && !netbsd && !plan9
|
||||
// +build !nofuse,!openbsd,!netbsd,!plan9
|
||||
|
||||
package mfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
iofs "io/fs"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"bazil.org/fuse"
|
||||
"bazil.org/fuse/fs"
|
||||
"bazil.org/fuse/fs/fstestutil"
|
||||
"github.com/ipfs/kubo/core"
|
||||
"github.com/ipfs/kubo/core/node"
|
||||
"github.com/libp2p/go-libp2p-testing/ci"
|
||||
)
|
||||
|
||||
// Create an Ipfs.Node, a filesystem and a mount point.
|
||||
func setUp(t *testing.T, ipfs *core.IpfsNode) (fs.FS, *fstestutil.Mount) {
|
||||
if ci.NoFuse() {
|
||||
t.Skip("Skipping FUSE tests")
|
||||
}
|
||||
|
||||
if ipfs == nil {
|
||||
var err error
|
||||
ipfs, err = core.NewNode(context.Background(), &node.BuildCfg{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
fs := NewFileSystem(ipfs)
|
||||
mnt, err := fstestutil.MountedT(t, fs, nil)
|
||||
if err == fuse.ErrOSXFUSENotFound {
|
||||
t.Skip(err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return fs, mnt
|
||||
}
|
||||
|
||||
// Test reading and writing a file.
|
||||
func TestReadWrite(t *testing.T) {
|
||||
_, mnt := setUp(t, nil)
|
||||
defer mnt.Close()
|
||||
|
||||
path := mnt.Dir + "/testrw"
|
||||
content := make([]byte, 8196)
|
||||
_, err := rand.Read(content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("write", func(t *testing.T) {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Write(content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
t.Run("read", func(t *testing.T) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
buf := make([]byte, 8196)
|
||||
l, err := f.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(content, buf[:l]) != true {
|
||||
t.Fatal("read and write not equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test creating a directory.
|
||||
func TestMkdir(t *testing.T) {
|
||||
_, mnt := setUp(t, nil)
|
||||
defer mnt.Close()
|
||||
|
||||
path := mnt.Dir + "/foo/bar/baz/qux/quux"
|
||||
|
||||
t.Run("write", func(t *testing.T) {
|
||||
err := os.MkdirAll(path, iofs.ModeDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
t.Run("read", func(t *testing.T) {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !stat.IsDir() {
|
||||
t.Fatal("not dir")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test file persistence across mounts.
|
||||
func TestPersistence(t *testing.T) {
|
||||
ipfs, err := core.NewNode(context.Background(), &node.BuildCfg{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
content := make([]byte, 8196)
|
||||
_, err = rand.Read(content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("write", func(t *testing.T) {
|
||||
_, mnt := setUp(t, ipfs)
|
||||
defer mnt.Close()
|
||||
path := mnt.Dir + "/testpersistence"
|
||||
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Write(content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
t.Run("read", func(t *testing.T) {
|
||||
_, mnt := setUp(t, ipfs)
|
||||
defer mnt.Close()
|
||||
path := mnt.Dir + "/testpersistence"
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
buf := make([]byte, 8196)
|
||||
l, err := f.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(content, buf[:l]) != true {
|
||||
t.Fatal("read and write not equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test getting the file attributes.
|
||||
func TestAttr(t *testing.T) {
|
||||
_, mnt := setUp(t, nil)
|
||||
defer mnt.Close()
|
||||
|
||||
path := mnt.Dir + "/testattr"
|
||||
content := make([]byte, 8196)
|
||||
_, err := rand.Read(content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("write", func(t *testing.T) {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Write(content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
t.Run("read", func(t *testing.T) {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
t.Fatal("file is a directory")
|
||||
}
|
||||
|
||||
if fi.ModTime().After(time.Now()) {
|
||||
t.Fatal("future modtime")
|
||||
}
|
||||
if time.Since(fi.ModTime()) > time.Second {
|
||||
t.Fatal("past modtime")
|
||||
}
|
||||
|
||||
if fi.Name() != "testattr" {
|
||||
t.Fatal("invalid filename")
|
||||
}
|
||||
|
||||
if fi.Size() != 8196 {
|
||||
t.Fatal("invalid size")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test concurrent access to the filesystem.
|
||||
func TestConcurrentRW(t *testing.T) {
|
||||
_, mnt := setUp(t, nil)
|
||||
defer mnt.Close()
|
||||
|
||||
files := 5
|
||||
fileWorkers := 5
|
||||
|
||||
path := mnt.Dir + "/testconcurrent"
|
||||
content := make([][]byte, files)
|
||||
|
||||
for i := range content {
|
||||
content[i] = make([]byte, 8196)
|
||||
_, err := rand.Read(content[i])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("write", func(t *testing.T) {
|
||||
errs := make(chan (error), 1)
|
||||
for i := 0; i < files; i++ {
|
||||
go func() {
|
||||
var err error
|
||||
defer func() { errs <- err }()
|
||||
|
||||
f, err := os.Create(path + strconv.Itoa(i))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Write(content[i])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
for i := 0; i < files; i++ {
|
||||
err := <-errs
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("read", func(t *testing.T) {
|
||||
errs := make(chan (error), 1)
|
||||
for i := 0; i < files*fileWorkers; i++ {
|
||||
go func() {
|
||||
var err error
|
||||
defer func() { errs <- err }()
|
||||
|
||||
f, err := os.Open(path + strconv.Itoa(i/fileWorkers))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
buf := make([]byte, 8196)
|
||||
l, err := f.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if bytes.Equal(content[i/fileWorkers], buf[:l]) != true {
|
||||
err = errors.New("read and write not equal")
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
for i := 0; i < files; i++ {
|
||||
err := <-errs
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test ipfs_cid extended attribute
|
||||
func TestMFSRootXattr(t *testing.T) {
|
||||
ipfs, err := core.NewNode(context.Background(), &node.BuildCfg{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fs, mnt := setUp(t, ipfs)
|
||||
defer mnt.Close()
|
||||
|
||||
node, err := fs.Root()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
root := node.(*Dir)
|
||||
|
||||
listReq := fuse.ListxattrRequest{}
|
||||
listRes := fuse.ListxattrResponse{}
|
||||
err = root.Listxattr(context.Background(), &listReq, &listRes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if slices.Compare(listRes.Xattr, []byte("ipfs_cid\x00")) != 0 {
|
||||
t.Fatal("list xattr returns invalid value")
|
||||
}
|
||||
|
||||
getReq := fuse.GetxattrRequest{
|
||||
Name: "ipfs_cid",
|
||||
}
|
||||
getRes := fuse.GetxattrResponse{}
|
||||
err = root.Getxattr(context.Background(), &getReq, &getRes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ipldNode, err := ipfs.FilesRoot.GetDirectory().GetNode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if slices.Compare(getRes.Xattr, []byte(ipldNode.Cid().String())) != 0 {
|
||||
t.Fatal("xattr cid not equal to mfs root cid")
|
||||
}
|
||||
}
|
414
fuse/mfs/mfs_unix.go
Normal file
414
fuse/mfs/mfs_unix.go
Normal file
@ -0,0 +1,414 @@
|
||||
//go:build (linux || darwin || freebsd || netbsd || openbsd) && !nofuse
|
||||
// +build linux darwin freebsd netbsd openbsd
|
||||
// +build !nofuse
|
||||
|
||||
package mfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"bazil.org/fuse"
|
||||
"bazil.org/fuse/fs"
|
||||
|
||||
dag "github.com/ipfs/boxo/ipld/merkledag"
|
||||
ft "github.com/ipfs/boxo/ipld/unixfs"
|
||||
"github.com/ipfs/boxo/mfs"
|
||||
"github.com/ipfs/kubo/core"
|
||||
)
|
||||
|
||||
const (
|
||||
ipfsCIDXattr = "ipfs_cid"
|
||||
mfsDirMode = os.ModeDir | 0755
|
||||
mfsFileMode = 0644
|
||||
blockSize = 512
|
||||
dirSize = 8
|
||||
)
|
||||
|
||||
// FUSE filesystem mounted at /mfs.
|
||||
type FileSystem struct {
|
||||
root Dir
|
||||
}
|
||||
|
||||
// Get filesystem root.
|
||||
func (fs *FileSystem) Root() (fs.Node, error) {
|
||||
return &fs.root, nil
|
||||
}
|
||||
|
||||
// FUSE Adapter for MFS directories.
|
||||
type Dir struct {
|
||||
mfsDir *mfs.Directory
|
||||
}
|
||||
|
||||
// Directory attributes (stat).
|
||||
func (dir *Dir) Attr(ctx context.Context, attr *fuse.Attr) error {
|
||||
attr.Mode = mfsDirMode
|
||||
attr.Size = dirSize * blockSize
|
||||
attr.Blocks = dirSize
|
||||
return nil
|
||||
}
|
||||
|
||||
// Access files in a directory.
|
||||
func (dir *Dir) Lookup(ctx context.Context, req *fuse.LookupRequest, resp *fuse.LookupResponse) (fs.Node, error) {
|
||||
mfsNode, err := dir.mfsDir.Child(req.Name)
|
||||
switch err {
|
||||
case os.ErrNotExist:
|
||||
return nil, syscall.Errno(syscall.ENOENT)
|
||||
case nil:
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch mfsNode.Type() {
|
||||
case mfs.TDir:
|
||||
result := Dir{
|
||||
mfsDir: mfsNode.(*mfs.Directory),
|
||||
}
|
||||
return &result, nil
|
||||
case mfs.TFile:
|
||||
result := File{
|
||||
mfsFile: mfsNode.(*mfs.File),
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
return nil, syscall.Errno(syscall.ENOENT)
|
||||
}
|
||||
|
||||
// List (ls) MFS directory.
|
||||
func (dir *Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
|
||||
var res []fuse.Dirent
|
||||
nodes, err := dir.mfsDir.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
nodeType := fuse.DT_File
|
||||
if node.Type == 1 {
|
||||
nodeType = fuse.DT_Dir
|
||||
}
|
||||
res = append(res, fuse.Dirent{
|
||||
Type: nodeType,
|
||||
Name: node.Name,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Mkdir (mkdir) in MFS.
|
||||
func (dir *Dir) Mkdir(ctx context.Context, req *fuse.MkdirRequest) (fs.Node, error) {
|
||||
mfsDir, err := dir.mfsDir.Mkdir(req.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Dir{
|
||||
mfsDir: mfsDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Remove (rm/rmdir) an MFS file.
|
||||
func (dir *Dir) Remove(ctx context.Context, req *fuse.RemoveRequest) error {
|
||||
// Check for empty directory.
|
||||
if req.Dir {
|
||||
targetNode, err := dir.mfsDir.Child(req.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target := targetNode.(*mfs.Directory)
|
||||
|
||||
children, err := target.ListNames(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(children) > 0 {
|
||||
return os.ErrExist
|
||||
}
|
||||
}
|
||||
err := dir.mfsDir.Unlink(req.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dir.mfsDir.Flush()
|
||||
}
|
||||
|
||||
// Move (mv) an MFS file.
|
||||
func (dir *Dir) Rename(ctx context.Context, req *fuse.RenameRequest, newDir fs.Node) error {
|
||||
file, err := dir.mfsDir.Child(req.OldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node, err := file.GetNode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetDir := newDir.(*Dir)
|
||||
|
||||
// Remove file if exists
|
||||
err = targetDir.mfsDir.Unlink(req.NewName)
|
||||
if err != nil && err != os.ErrNotExist {
|
||||
return err
|
||||
}
|
||||
|
||||
err = targetDir.mfsDir.AddChild(req.NewName, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dir.mfsDir.Unlink(req.OldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dir.mfsDir.Flush()
|
||||
}
|
||||
|
||||
// Create (touch) an MFS file.
|
||||
func (dir *Dir) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse.CreateResponse) (fs.Node, fs.Handle, error) {
|
||||
node := dag.NodeWithData(ft.FilePBData(nil, 0))
|
||||
if err := node.SetCidBuilder(dir.mfsDir.GetCidBuilder()); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := dir.mfsDir.AddChild(req.Name, node); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := dir.mfsDir.Flush(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
mfsNode, err := dir.mfsDir.Child(req.Name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := mfsNode.SetModTime(time.Now()); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
mfsFile := mfsNode.(*mfs.File)
|
||||
|
||||
file := File{
|
||||
mfsFile: mfsFile,
|
||||
}
|
||||
|
||||
// Read access flags and create a handler.
|
||||
accessMode := req.Flags & fuse.OpenAccessModeMask
|
||||
flags := mfs.Flags{
|
||||
Read: accessMode == fuse.OpenReadOnly || accessMode == fuse.OpenReadWrite,
|
||||
Write: accessMode == fuse.OpenWriteOnly || accessMode == fuse.OpenReadWrite,
|
||||
Sync: req.Flags|fuse.OpenSync > 0,
|
||||
}
|
||||
|
||||
fd, err := mfsFile.Open(flags)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
handler := FileHandler{
|
||||
mfsFD: fd,
|
||||
}
|
||||
|
||||
return &file, &handler, nil
|
||||
}
|
||||
|
||||
// List dir xattr.
|
||||
func (dir *Dir) Listxattr(ctx context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error {
|
||||
resp.Append(ipfsCIDXattr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get dir xattr.
|
||||
func (dir *Dir) Getxattr(ctx context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
|
||||
switch req.Name {
|
||||
case ipfsCIDXattr:
|
||||
node, err := dir.mfsDir.GetNode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Xattr = []byte(node.Cid().String())
|
||||
return nil
|
||||
default:
|
||||
return fuse.ErrNoXattr
|
||||
}
|
||||
}
|
||||
|
||||
// FUSE adapter for MFS files.
|
||||
type File struct {
|
||||
mfsFile *mfs.File
|
||||
}
|
||||
|
||||
// File attributes.
|
||||
func (file *File) Attr(ctx context.Context, attr *fuse.Attr) error {
|
||||
size, _ := file.mfsFile.Size()
|
||||
|
||||
attr.Size = uint64(size)
|
||||
if size%blockSize == 0 {
|
||||
attr.Blocks = uint64(size / blockSize)
|
||||
} else {
|
||||
attr.Blocks = uint64(size/blockSize + 1)
|
||||
}
|
||||
|
||||
mtime, _ := file.mfsFile.ModTime()
|
||||
attr.Mtime = mtime
|
||||
|
||||
attr.Mode = mfsFileMode
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open an MFS file.
|
||||
func (file *File) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fs.Handle, error) {
|
||||
accessMode := req.Flags & fuse.OpenAccessModeMask
|
||||
flags := mfs.Flags{
|
||||
Read: accessMode == fuse.OpenReadOnly || accessMode == fuse.OpenReadWrite,
|
||||
Write: accessMode == fuse.OpenWriteOnly || accessMode == fuse.OpenReadWrite,
|
||||
Sync: req.Flags|fuse.OpenSync > 0,
|
||||
}
|
||||
fd, err := file.mfsFile.Open(flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if flags.Write {
|
||||
if err := file.mfsFile.SetModTime(time.Now()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &FileHandler{
|
||||
mfsFD: fd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Sync the file's contents to MFS.
|
||||
func (file *File) Fsync(ctx context.Context, req *fuse.FsyncRequest) error {
|
||||
return file.mfsFile.Sync()
|
||||
}
|
||||
|
||||
// List file xattr.
|
||||
func (file *File) Listxattr(ctx context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error {
|
||||
resp.Append(ipfsCIDXattr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get file xattr.
|
||||
func (file *File) Getxattr(ctx context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
|
||||
switch req.Name {
|
||||
case ipfsCIDXattr:
|
||||
node, err := file.mfsFile.GetNode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Xattr = []byte(node.Cid().String())
|
||||
return nil
|
||||
default:
|
||||
return fuse.ErrNoXattr
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for MFS's file descriptor that conforms to the FUSE fs.Handler
|
||||
// interface.
|
||||
type FileHandler struct {
|
||||
mfsFD mfs.FileDescriptor
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Read a opened MFS file.
|
||||
func (fh *FileHandler) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
|
||||
fh.mu.Lock()
|
||||
defer fh.mu.Unlock()
|
||||
|
||||
_, err := fh.mfsFD.Seek(req.Offset, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := make([]byte, req.Size)
|
||||
l, err := fh.mfsFD.Read(buf)
|
||||
|
||||
resp.Data = buf[:l]
|
||||
|
||||
switch err {
|
||||
case nil, io.EOF, io.ErrUnexpectedEOF:
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes to an opened MFS file.
|
||||
func (fh *FileHandler) Write(ctx context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) error {
|
||||
fh.mu.Lock()
|
||||
defer fh.mu.Unlock()
|
||||
|
||||
l, err := fh.mfsFD.WriteAt(req.Data, req.Offset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Size = l
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flushes the file's buffer.
|
||||
func (fh *FileHandler) Flush(ctx context.Context, req *fuse.FlushRequest) error {
|
||||
fh.mu.Lock()
|
||||
defer fh.mu.Unlock()
|
||||
|
||||
return fh.mfsFD.Flush()
|
||||
}
|
||||
|
||||
// Closes the file.
|
||||
func (fh *FileHandler) Release(ctx context.Context, req *fuse.ReleaseRequest) error {
|
||||
fh.mu.Lock()
|
||||
defer fh.mu.Unlock()
|
||||
|
||||
return fh.mfsFD.Close()
|
||||
}
|
||||
|
||||
// Create new filesystem.
|
||||
func NewFileSystem(ipfs *core.IpfsNode) fs.FS {
|
||||
return &FileSystem{
|
||||
root: Dir{
|
||||
mfsDir: ipfs.FilesRoot.GetDirectory(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Check that our structs implement all the interfaces we want.
|
||||
type mfsDir interface {
|
||||
fs.Node
|
||||
fs.NodeGetxattrer
|
||||
fs.NodeListxattrer
|
||||
fs.HandleReadDirAller
|
||||
fs.NodeRequestLookuper
|
||||
fs.NodeMkdirer
|
||||
fs.NodeRenamer
|
||||
fs.NodeRemover
|
||||
fs.NodeCreater
|
||||
}
|
||||
|
||||
var _ mfsDir = (*Dir)(nil)
|
||||
|
||||
type mfsFile interface {
|
||||
fs.Node
|
||||
fs.NodeGetxattrer
|
||||
fs.NodeListxattrer
|
||||
fs.NodeOpener
|
||||
fs.NodeFsyncer
|
||||
}
|
||||
|
||||
var _ mfsFile = (*File)(nil)
|
||||
|
||||
type mfsHandler interface {
|
||||
fs.Handle
|
||||
fs.HandleReader
|
||||
fs.HandleWriter
|
||||
fs.HandleFlusher
|
||||
fs.HandleReleaser
|
||||
}
|
||||
|
||||
var _ mfsHandler = (*FileHandler)(nil)
|
21
fuse/mfs/mount_unix.go
Normal file
21
fuse/mfs/mount_unix.go
Normal file
@ -0,0 +1,21 @@
|
||||
//go:build (linux || darwin || freebsd || netbsd || openbsd) && !nofuse
|
||||
// +build linux darwin freebsd netbsd openbsd
|
||||
// +build !nofuse
|
||||
|
||||
package mfs
|
||||
|
||||
import (
|
||||
core "github.com/ipfs/kubo/core"
|
||||
mount "github.com/ipfs/kubo/fuse/mount"
|
||||
)
|
||||
|
||||
// Mount mounts MFS at a given location, and returns a mount.Mount instance.
|
||||
func Mount(ipfs *core.IpfsNode, mountpoint string) (mount.Mount, error) {
|
||||
cfg, err := ipfs.Repo.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowOther := cfg.Mounts.FuseAllowOther
|
||||
fsys := NewFileSystem(ipfs)
|
||||
return mount.NewMount(ipfs.Process, fsys, mountpoint, allowOther)
|
||||
}
|
@ -9,6 +9,6 @@ import (
|
||||
core "github.com/ipfs/kubo/core"
|
||||
)
|
||||
|
||||
func Mount(node *core.IpfsNode, fsdir, nsdir string) error {
|
||||
func Mount(node *core.IpfsNode, fsdir, nsdir, mfsdir string) error {
|
||||
return errors.New("not compiled in")
|
||||
}
|
||||
|
@ -9,6 +9,6 @@ import (
|
||||
core "github.com/ipfs/kubo/core"
|
||||
)
|
||||
|
||||
func Mount(node *core.IpfsNode, fsdir, nsdir string) error {
|
||||
func Mount(node *core.IpfsNode, fsdir, nsdir, mfsdir string) error {
|
||||
return errors.New("FUSE not supported on OpenBSD or NetBSD. See #5334 (https://github.com/ipfs/kubo/issues/5334).")
|
||||
}
|
||||
|
@ -56,10 +56,12 @@ func TestExternalUnmount(t *testing.T) {
|
||||
|
||||
ipfsDir := dir + "/ipfs"
|
||||
ipnsDir := dir + "/ipns"
|
||||
mfsDir := dir + "/mfs"
|
||||
mkdir(t, ipfsDir)
|
||||
mkdir(t, ipnsDir)
|
||||
mkdir(t, mfsDir)
|
||||
|
||||
err = Mount(node, ipfsDir, ipnsDir)
|
||||
err = Mount(node, ipfsDir, ipnsDir, mfsDir)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "unable to check fuse version") || err == fuse.ErrOSXFUSENotFound {
|
||||
t.Skip(err)
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
core "github.com/ipfs/kubo/core"
|
||||
ipns "github.com/ipfs/kubo/fuse/ipns"
|
||||
mfs "github.com/ipfs/kubo/fuse/mfs"
|
||||
mount "github.com/ipfs/kubo/fuse/mount"
|
||||
rofs "github.com/ipfs/kubo/fuse/readonly"
|
||||
|
||||
@ -31,7 +32,7 @@ var platformFuseChecks = func(*core.IpfsNode) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Mount(node *core.IpfsNode, fsdir, nsdir string) error {
|
||||
func Mount(node *core.IpfsNode, fsdir, nsdir, mfsdir string) error {
|
||||
// check if we already have live mounts.
|
||||
// if the user said "Mount", then there must be something wrong.
|
||||
// so, close them and try again.
|
||||
@ -43,15 +44,19 @@ func Mount(node *core.IpfsNode, fsdir, nsdir string) error {
|
||||
// best effort
|
||||
_ = node.Mounts.Ipns.Unmount()
|
||||
}
|
||||
if node.Mounts.Mfs != nil && node.Mounts.Mfs.IsActive() {
|
||||
// best effort
|
||||
_ = node.Mounts.Mfs.Unmount()
|
||||
}
|
||||
|
||||
if err := platformFuseChecks(node); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return doMount(node, fsdir, nsdir)
|
||||
return doMount(node, fsdir, nsdir, mfsdir)
|
||||
}
|
||||
|
||||
func doMount(node *core.IpfsNode, fsdir, nsdir string) error {
|
||||
func doMount(node *core.IpfsNode, fsdir, nsdir, mfsdir string) error {
|
||||
fmtFuseErr := func(err error, mountpoint string) error {
|
||||
s := err.Error()
|
||||
if strings.Contains(s, fuseNoDirectory) {
|
||||
@ -67,8 +72,8 @@ func doMount(node *core.IpfsNode, fsdir, nsdir string) error {
|
||||
}
|
||||
|
||||
// this sync stuff is so that both can be mounted simultaneously.
|
||||
var fsmount, nsmount mount.Mount
|
||||
var err1, err2 error
|
||||
var fsmount, nsmount, mfmount mount.Mount
|
||||
var err1, err2, err3 error
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
@ -86,6 +91,12 @@ func doMount(node *core.IpfsNode, fsdir, nsdir string) error {
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
mfmount, err3 = mfs.Mount(node, mfsdir)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if err1 != nil {
|
||||
@ -96,22 +107,33 @@ func doMount(node *core.IpfsNode, fsdir, nsdir string) error {
|
||||
log.Errorf("error mounting IPNS %s for IPFS %s: %s", nsdir, fsdir, err2)
|
||||
}
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
if err3 != nil {
|
||||
log.Errorf("error mounting MFS %s: %s", mfsdir, err3)
|
||||
}
|
||||
|
||||
if err1 != nil || err2 != nil || err3 != nil {
|
||||
if fsmount != nil {
|
||||
_ = fsmount.Unmount()
|
||||
}
|
||||
if nsmount != nil {
|
||||
_ = nsmount.Unmount()
|
||||
}
|
||||
if mfmount != nil {
|
||||
_ = mfmount.Unmount()
|
||||
}
|
||||
|
||||
if err1 != nil {
|
||||
return fmtFuseErr(err1, fsdir)
|
||||
}
|
||||
if err2 != nil {
|
||||
return fmtFuseErr(err2, nsdir)
|
||||
}
|
||||
return fmtFuseErr(err3, mfsdir)
|
||||
}
|
||||
|
||||
// setup node state, so that it can be canceled
|
||||
node.Mounts.Ipfs = fsmount
|
||||
node.Mounts.Ipns = nsmount
|
||||
node.Mounts.Mfs = mfmount
|
||||
return nil
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"github.com/ipfs/kubo/core"
|
||||
)
|
||||
|
||||
func Mount(node *core.IpfsNode, fsdir, nsdir string) error {
|
||||
func Mount(node *core.IpfsNode, fsdir, nsdir, mfsdir string) error {
|
||||
// TODO
|
||||
// currently a no-op, but we don't want to return an error
|
||||
return nil
|
||||
|
@ -15,7 +15,8 @@
|
||||
},
|
||||
"Mounts": {
|
||||
"IPFS": "/ipfs",
|
||||
"IPNS": "/ipns"
|
||||
"IPNS": "/ipns",
|
||||
"MFS": "/mfs"
|
||||
},
|
||||
"Version": {
|
||||
"Current": "0.1.7",
|
||||
|
@ -17,7 +17,8 @@
|
||||
},
|
||||
"Mounts": {
|
||||
"IPFS": "/ipfs",
|
||||
"IPNS": "/ipns"
|
||||
"IPNS": "/ipns",
|
||||
"MFS": "/mfs"
|
||||
},
|
||||
"Version": {
|
||||
"AutoUpdate": "minor",
|
||||
|
@ -17,7 +17,8 @@
|
||||
},
|
||||
"Mounts": {
|
||||
"IPFS": "/ipfs",
|
||||
"IPNS": "/ipns"
|
||||
"IPNS": "/ipns",
|
||||
"MFS": "/mfs"
|
||||
},
|
||||
"Version": {
|
||||
"AutoUpdate": "minor",
|
||||
|
@ -167,7 +167,7 @@ func TestBitswapConfig(t *testing.T) {
|
||||
node.UpdateConfig(func(cfg *config.Config) {
|
||||
cfg.HTTPRetrieval.Enabled = config.False
|
||||
cfg.Bitswap.Libp2pEnabled = config.False
|
||||
cfg.Bitswap.ServerEnabled = config.True // bad user config: cant enable server when libp2p is down
|
||||
cfg.Bitswap.ServerEnabled = config.True // bad user config: can't enable server when libp2p is down
|
||||
})
|
||||
res := node.RunIPFS("daemon")
|
||||
assert.Contains(t, res.Stderr.Trimmed(), "invalid configuration: Bitswap.Libp2pEnabled and HTTPRetrieval.Enabled are both disabled, unable to initialize Bitswap")
|
||||
|
@ -206,9 +206,10 @@ test_init_ipfs() {
|
||||
'
|
||||
|
||||
test_expect_success "prepare config -- mounting" '
|
||||
mkdir mountdir ipfs ipns &&
|
||||
mkdir mountdir ipfs ipns mfs &&
|
||||
test_config_set Mounts.IPFS "$(pwd)/ipfs" &&
|
||||
test_config_set Mounts.IPNS "$(pwd)/ipns" ||
|
||||
test_config_set Mounts.IPNS "$(pwd)/ipns" &&
|
||||
test_config_set Mounts.MFS "$(pwd)/mfs" ||
|
||||
test_fsh cat "\"$IPFS_PATH/config\""
|
||||
'
|
||||
|
||||
@ -321,12 +322,14 @@ test_mount_ipfs() {
|
||||
test_expect_success FUSE "'ipfs mount' succeeds" '
|
||||
do_umount "$(pwd)/ipfs" || true &&
|
||||
do_umount "$(pwd)/ipns" || true &&
|
||||
do_umount "$(pwd)/mfs" || true &&
|
||||
ipfs mount >actual
|
||||
'
|
||||
|
||||
test_expect_success FUSE "'ipfs mount' output looks good" '
|
||||
echo "IPFS mounted at: $(pwd)/ipfs" >expected &&
|
||||
echo "IPNS mounted at: $(pwd)/ipns" >>expected &&
|
||||
echo "MFS mounted at: $(pwd)/mfs" >>expected &&
|
||||
test_cmp expected actual
|
||||
'
|
||||
|
||||
|
@ -16,7 +16,8 @@ if ! test_have_prereq FUSE; then
|
||||
fi
|
||||
|
||||
|
||||
export IPFS_NS_MAP="welcome.example.com:/ipfs/$HASH_WELCOME_DOCS"
|
||||
# echo -n "ipfs" > expected && ipfs add --cid-version 1 -Q -w expected
|
||||
export IPFS_NS_MAP="welcome.example.com:/ipfs/bafybeicq7bvn5lz42qlmghaoiwrve74pzi53auqetbantp5kajucsabike"
|
||||
|
||||
# start iptb + wait for peering
|
||||
NUM_NODES=5
|
||||
@ -27,17 +28,17 @@ startup_cluster $NUM_NODES
|
||||
|
||||
# test mount failure before mounting properly.
|
||||
test_expect_success "'ipfs mount' fails when there is no mount dir" '
|
||||
tmp_ipfs_mount() { ipfsi 0 mount -f=not_ipfs -n=not_ipns >output 2>output.err; } &&
|
||||
tmp_ipfs_mount() { ipfsi 0 mount -f=not_ipfs -n=not_ipns -m=not_mfs >output 2>output.err; } &&
|
||||
test_must_fail tmp_ipfs_mount
|
||||
'
|
||||
|
||||
test_expect_success "'ipfs mount' output looks good" '
|
||||
test_must_be_empty output &&
|
||||
test_should_contain "not_ipns\|not_ipfs" output.err
|
||||
test_should_contain "not_ipns\|not_ipfs\|not_mfs" output.err
|
||||
'
|
||||
|
||||
test_expect_success "setup and publish default IPNS value" '
|
||||
mkdir "$(pwd)/ipfs" "$(pwd)/ipns" &&
|
||||
mkdir "$(pwd)/ipfs" "$(pwd)/ipns" "$(pwd)/mfs" &&
|
||||
ipfsi 0 name publish QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn
|
||||
'
|
||||
|
||||
@ -46,12 +47,14 @@ test_expect_success "setup and publish default IPNS value" '
|
||||
test_expect_success FUSE "'ipfs mount' succeeds" '
|
||||
do_umount "$(pwd)/ipfs" || true &&
|
||||
do_umount "$(pwd)/ipns" || true &&
|
||||
ipfsi 0 mount -f "$(pwd)/ipfs" -n "$(pwd)/ipns" >actual
|
||||
do_umount "$(pwd)/mfs" || true &&
|
||||
ipfsi 0 mount -f "$(pwd)/ipfs" -n "$(pwd)/ipns" -m "$(pwd)/mfs" >actual
|
||||
'
|
||||
|
||||
test_expect_success FUSE "'ipfs mount' output looks good" '
|
||||
echo "IPFS mounted at: $(pwd)/ipfs" >expected &&
|
||||
echo "IPNS mounted at: $(pwd)/ipns" >>expected &&
|
||||
echo "MFS mounted at: $(pwd)/mfs" >>expected &&
|
||||
test_cmp expected actual
|
||||
'
|
||||
|
||||
@ -63,21 +66,64 @@ test_expect_success FUSE "local symlink works" '
|
||||
|
||||
test_expect_success FUSE "can resolve ipns names" '
|
||||
echo -n "ipfs" > expected &&
|
||||
cat ipns/welcome.example.com/ping > actual &&
|
||||
ipfsi 0 add --cid-version 1 -Q -w expected &&
|
||||
cat ipns/welcome.example.com/expected > actual &&
|
||||
test_cmp expected actual
|
||||
'
|
||||
|
||||
test_expect_success FUSE "create mfs file via fuse" '
|
||||
touch mfs/testfile &&
|
||||
ipfsi 0 files ls | grep testfile
|
||||
'
|
||||
|
||||
test_expect_success FUSE "create mfs dir via fuse" '
|
||||
mkdir mfs/testdir &&
|
||||
ipfsi 0 files ls | grep testdir
|
||||
'
|
||||
|
||||
test_expect_success FUSE "read mfs file from fuse" '
|
||||
echo content > mfs/testfile &&
|
||||
getfattr -n ipfs_cid mfs/testfile
|
||||
'
|
||||
test_expect_success FUSE "ipfs add file and read it back via fuse" '
|
||||
echo content3 | ipfsi 0 files write -e /testfile3 &&
|
||||
grep content3 mfs/testfile3
|
||||
'
|
||||
|
||||
test_expect_success FUSE "ipfs add file and read it back via fuse" '
|
||||
echo content > testfile2 &&
|
||||
ipfsi 0 add --to-files /testfile2 testfile2 &&
|
||||
grep content mfs/testfile2
|
||||
'
|
||||
|
||||
test_expect_success FUSE "test file xattr" '
|
||||
echo content > mfs/testfile &&
|
||||
getfattr -n ipfs_cid mfs/testfile
|
||||
'
|
||||
|
||||
test_expect_success FUSE "test file removal" '
|
||||
touch mfs/testfile &&
|
||||
rm mfs/testfile
|
||||
'
|
||||
|
||||
test_expect_success FUSE "test nested dirs" '
|
||||
mkdir -p mfs/foo/bar/baz/qux &&
|
||||
echo content > mfs/foo/bar/baz/qux/quux &&
|
||||
ipfsi 0 files stat /foo/bar/baz/qux/quux
|
||||
'
|
||||
|
||||
test_expect_success "mount directories cannot be removed while active" '
|
||||
test_must_fail rmdir ipfs ipns 2>/dev/null
|
||||
test_must_fail rmdir ipfs ipns mfs 2>/dev/null
|
||||
'
|
||||
|
||||
test_expect_success "unmount directories" '
|
||||
do_umount "$(pwd)/ipfs" &&
|
||||
do_umount "$(pwd)/ipns"
|
||||
do_umount "$(pwd)/ipns" &&
|
||||
do_umount "$(pwd)/mfs"
|
||||
'
|
||||
|
||||
test_expect_success "mount directories can be removed after shutdown" '
|
||||
rmdir ipfs ipns
|
||||
rmdir ipfs ipns mfs
|
||||
'
|
||||
|
||||
test_expect_success 'stop iptb' '
|
||||
|
@ -63,7 +63,7 @@ test_filestore_adds() {
|
||||
|
||||
init_ipfs_filestore() {
|
||||
test_expect_success "clean up old node" '
|
||||
rm -rf "$IPFS_PATH" mountdir ipfs ipns
|
||||
rm -rf "$IPFS_PATH" mountdir ipfs ipns mfs
|
||||
'
|
||||
|
||||
test_init_ipfs
|
||||
|
@ -10,7 +10,7 @@ test_description="Test out the filestore nocopy functionality"
|
||||
|
||||
test_init_filestore() {
|
||||
test_expect_success "clean up old node" '
|
||||
rm -rf "$IPFS_PATH" mountdir ipfs ipns
|
||||
rm -rf "$IPFS_PATH" mountdir ipfs ipns mfs
|
||||
'
|
||||
|
||||
test_init_ipfs
|
||||
|
Reference in New Issue
Block a user