| 1 | //go:build (linux || darwin || freebsd) && !nofuse |
| 2 | |
| 3 | // Package ipns implements a FUSE filesystem that interfaces with IPNS, |
| 4 | // the naming system for IPFS. Only names for which the node holds |
| 5 | // private keys are writable; all other names resolve to read-only |
| 6 | // symlinks pointing at the /ipfs mount. |
| 7 | package ipns |
| 8 | |
| 9 | import ( |
| 10 | "context" |
| 11 | "strings" |
| 12 | "syscall" |
| 13 | |
| 14 | "github.com/hanwen/go-fuse/v2/fs" |
| 15 | "github.com/hanwen/go-fuse/v2/fuse" |
| 16 | dag "github.com/ipfs/boxo/ipld/merkledag" |
| 17 | ft "github.com/ipfs/boxo/ipld/unixfs" |
| 18 | mfs "github.com/ipfs/boxo/mfs" |
| 19 | "github.com/ipfs/boxo/namesys" |
| 20 | "github.com/ipfs/boxo/path" |
| 21 | cid "github.com/ipfs/go-cid" |
| 22 | logging "github.com/ipfs/go-log/v2" |
| 23 | "github.com/ipfs/kubo/config" |
| 24 | iface "github.com/ipfs/kubo/core/coreiface" |
| 25 | options "github.com/ipfs/kubo/core/coreiface/options" |
| 26 | fusemnt "github.com/ipfs/kubo/fuse/mount" |
| 27 | "github.com/ipfs/kubo/fuse/writable" |
| 28 | "github.com/ipfs/kubo/internal/fusemount" |
| 29 | ) |
| 30 | |
| 31 | var log = logging.Logger("fuse/ipns") |
| 32 | |
| 33 | // Root is the root object of the /ipns filesystem tree. |
| 34 | type Root struct { |
| 35 | fs.Inode |
| 36 | Ipfs iface.CoreAPI |
| 37 | Keys map[string]iface.Key |
| 38 | |
| 39 | // Used for symlinking into ipfs |
| 40 | IpfsRoot string |
| 41 | IpnsRoot string |
| 42 | LocalDirs map[string]*writable.Dir |
| 43 | Roots map[string]*mfs.Root |
| 44 | |
| 45 | LocalLinks map[string]*Link |
| 46 | RepoPath string |
| 47 | } |
| 48 | |
| 49 | func ipnsPubFunc(ipfs iface.CoreAPI, key iface.Key) mfs.PubFunc { |
| 50 | return func(ctx context.Context, c cid.Cid) error { |
| 51 | // Bypass the "cannot publish while IPNS is mounted" guard. |
| 52 | // Without this the mount's own publishes are blocked, |
| 53 | // causing silent data loss on daemon restart (issue #2168). |
| 54 | ctx = fusemount.ContextWithPublish(ctx) |
| 55 | _, err := ipfs.Name().Publish(ctx, path.FromCid(c), options.Name.Key(key.Name()), options.Name.AllowOffline(true)) |
| 56 | return err |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | func loadRoot(ctx context.Context, ipfs iface.CoreAPI, key iface.Key, cfg *writable.Config, mfsOpts ...mfs.Option) (*mfs.Root, *writable.Dir, error) { |
| 61 | node, err := ipfs.ResolveNode(ctx, key.Path()) |
| 62 | switch err { |
| 63 | case nil: |
| 64 | case namesys.ErrResolveFailed: |
| 65 | node = ft.EmptyDirNode() |
| 66 | default: |
| 67 | log.Errorf("looking up %s: %s", key.Path(), err) |
| 68 | return nil, nil, err |
| 69 | } |
| 70 | |
| 71 | pbnode, ok := node.(*dag.ProtoNode) |
| 72 | if !ok { |
| 73 | return nil, nil, dag.ErrNotProtobuf |
| 74 | } |
| 75 | |
| 76 | root, err := mfs.NewRoot(ctx, ipfs.Dag(), pbnode, ipnsPubFunc(ipfs, key), nil, mfsOpts...) |
| 77 | if err != nil { |
| 78 | return nil, nil, err |
| 79 | } |
| 80 | |
| 81 | return root, writable.NewDir(root.GetDirectory(), cfg), nil |
| 82 | } |
| 83 | |
| 84 | // CreateRoot creates the IPNS FUSE root with one writable directory per key. |
| 85 | func CreateRoot(ctx context.Context, ipfs iface.CoreAPI, keys map[string]iface.Key, ipfspath, ipnspath, repoPath string, mountsCfg config.Mounts, imp config.Import, mfsOpts ...mfs.Option) (*Root, error) { |
| 86 | cfg := &writable.Config{ |
| 87 | StoreMtime: mountsCfg.StoreMtime.WithDefault(config.DefaultStoreMtime), |
| 88 | StoreMode: mountsCfg.StoreMode.WithDefault(config.DefaultStoreMode), |
| 89 | DAG: ipfs.Dag(), |
| 90 | RepoPath: repoPath, |
| 91 | Blksize: fusemnt.BlksizeFromChunker(imp.UnixFSChunker.WithDefault(config.DefaultUnixFSChunker)), |
| 92 | } |
| 93 | |
| 94 | ldirs := make(map[string]*writable.Dir) |
| 95 | roots := make(map[string]*mfs.Root) |
| 96 | links := make(map[string]*Link) |
| 97 | for alias, k := range keys { |
| 98 | root, dir, err := loadRoot(ctx, ipfs, k, cfg, mfsOpts...) |
| 99 | if err != nil { |
| 100 | return nil, err |
| 101 | } |
| 102 | |
| 103 | name := k.ID().String() |
| 104 | roots[name] = root |
| 105 | ldirs[name] = dir |
| 106 | links[alias] = &Link{Target: name} |
| 107 | } |
| 108 | |
| 109 | return &Root{ |
| 110 | Ipfs: ipfs, |
| 111 | IpfsRoot: ipfspath, |
| 112 | IpnsRoot: ipnspath, |
| 113 | Keys: keys, |
| 114 | LocalDirs: ldirs, |
| 115 | LocalLinks: links, |
| 116 | Roots: roots, |
| 117 | RepoPath: repoPath, |
| 118 | }, nil |
| 119 | } |
| 120 | |
| 121 | // Getattr returns the root directory attributes. |
| 122 | func (r *Root) Getattr(_ context.Context, _ fs.FileHandle, out *fuse.AttrOut) syscall.Errno { |
| 123 | out.Attr.Mode = uint32(fusemnt.NamespaceRootMode.Perm()) |
| 124 | return 0 |
| 125 | } |
| 126 | |
| 127 | // Statfs reports disk-space statistics for the underlying filesystem. |
| 128 | // macOS Finder checks free space before copying; without this it |
| 129 | // reports "not enough free space" because go-fuse returns zeroed stats. |
| 130 | func (r *Root) Statfs(_ context.Context, out *fuse.StatfsOut) syscall.Errno { |
| 131 | if r.RepoPath == "" { |
| 132 | return 0 |
| 133 | } |
| 134 | var s syscall.Statfs_t |
| 135 | if err := syscall.Statfs(r.RepoPath, &s); err != nil { |
| 136 | return fs.ToErrno(err) |
| 137 | } |
| 138 | out.FromStatfsT(&s) |
| 139 | return 0 |
| 140 | } |
| 141 | |
| 142 | func (r *Root) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { |
| 143 | switch name { |
| 144 | case "mach_kernel", ".hidden", "._.": |
| 145 | return nil, syscall.ENOENT |
| 146 | } |
| 147 | |
| 148 | if lnk, ok := r.LocalLinks[name]; ok { |
| 149 | return r.NewInode(ctx, lnk, fs.StableAttr{Mode: syscall.S_IFLNK}), 0 |
| 150 | } |
| 151 | |
| 152 | if dir, ok := r.LocalDirs[name]; ok { |
| 153 | return r.NewInode(ctx, dir, fs.StableAttr{Mode: syscall.S_IFDIR}), 0 |
| 154 | } |
| 155 | |
| 156 | // Other links go through IPNS resolution and are symlinked into the /ipfs mount. |
| 157 | resolved, err := r.Ipfs.Name().Resolve(ctx, "/ipns/"+name) |
| 158 | if err != nil { |
| 159 | log.Warnf("ipns: namesys resolve error: %s", err) |
| 160 | return nil, syscall.ENOENT |
| 161 | } |
| 162 | |
| 163 | if resolved.Namespace() != path.IPFSNamespace { |
| 164 | return nil, syscall.ENOENT |
| 165 | } |
| 166 | |
| 167 | lnk := &Link{Target: r.IpfsRoot + "/" + strings.TrimPrefix(resolved.String(), "/ipfs/")} |
| 168 | return r.NewInode(ctx, lnk, fs.StableAttr{Mode: syscall.S_IFLNK}), 0 |
| 169 | } |
| 170 | |
| 171 | func (r *Root) Readdir(_ context.Context) (fs.DirStream, syscall.Errno) { |
| 172 | entries := make([]fuse.DirEntry, 0, len(r.Keys)*2) |
| 173 | for alias, k := range r.Keys { |
| 174 | entries = append(entries, |
| 175 | fuse.DirEntry{Name: k.ID().String(), Mode: syscall.S_IFDIR}, |
| 176 | fuse.DirEntry{Name: alias, Mode: syscall.S_IFLNK}, |
| 177 | ) |
| 178 | } |
| 179 | return fs.NewListDirStream(entries), 0 |
| 180 | } |
| 181 | |
| 182 | func (r *Root) Close() error { |
| 183 | for _, mr := range r.Roots { |
| 184 | if err := mr.Close(); err != nil { |
| 185 | return err |
| 186 | } |
| 187 | } |
| 188 | return nil |
| 189 | } |
| 190 | |
| 191 | // Interface compliance checks for Root. |
| 192 | var ( |
| 193 | _ fs.NodeGetattrer = (*Root)(nil) |
| 194 | _ fs.NodeLookuper = (*Root)(nil) |
| 195 | _ fs.NodeReaddirer = (*Root)(nil) |
| 196 | _ fs.NodeStatfser = (*Root)(nil) |
| 197 | ) |