mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-25 20:37:53 +08:00
Let's save log.Error for things the user can take action on. Moved all our diagnostics to log.Debug. We can ideally reduce them even further.
641 lines
14 KiB
Go
641 lines
14 KiB
Go
// package fuse/ipns implements a fuse filesystem that interfaces
|
|
// with ipns, the naming system for ipfs.
|
|
package ipns
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
fuse "github.com/jbenet/go-ipfs/Godeps/_workspace/src/bazil.org/fuse"
|
|
fs "github.com/jbenet/go-ipfs/Godeps/_workspace/src/bazil.org/fuse/fs"
|
|
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context"
|
|
proto "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/goprotobuf/proto"
|
|
eventlog "github.com/jbenet/go-ipfs/thirdparty/eventlog"
|
|
|
|
core "github.com/jbenet/go-ipfs/core"
|
|
chunk "github.com/jbenet/go-ipfs/importer/chunk"
|
|
mdag "github.com/jbenet/go-ipfs/merkledag"
|
|
nsys "github.com/jbenet/go-ipfs/namesys"
|
|
ci "github.com/jbenet/go-ipfs/p2p/crypto"
|
|
path "github.com/jbenet/go-ipfs/path"
|
|
ft "github.com/jbenet/go-ipfs/unixfs"
|
|
uio "github.com/jbenet/go-ipfs/unixfs/io"
|
|
ftpb "github.com/jbenet/go-ipfs/unixfs/pb"
|
|
u "github.com/jbenet/go-ipfs/util"
|
|
lgbl "github.com/jbenet/go-ipfs/util/eventlog/loggables"
|
|
)
|
|
|
|
const IpnsReadonly = true
|
|
|
|
var log = eventlog.Logger("fuse/ipns")
|
|
|
|
var (
|
|
shortRepublishTimeout = time.Millisecond * 5
|
|
longRepublishTimeout = time.Millisecond * 500
|
|
)
|
|
|
|
// InitializeKeyspace sets the ipns record for the given key to
|
|
// point to an empty directory.
|
|
func InitializeKeyspace(n *core.IpfsNode, key ci.PrivKey) error {
|
|
emptyDir := &mdag.Node{Data: ft.FolderPBData()}
|
|
nodek, err := n.DAG.Add(emptyDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = n.Pinning.Pin(emptyDir, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = n.Pinning.Flush()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pub := nsys.NewRoutingPublisher(n.Routing)
|
|
err = pub.Publish(n.Context(), key, nodek)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FileSystem is the readwrite IPNS Fuse Filesystem.
|
|
type FileSystem struct {
|
|
Ipfs *core.IpfsNode
|
|
RootNode *Root
|
|
}
|
|
|
|
// NewFileSystem constructs new fs using given core.IpfsNode instance.
|
|
func NewFileSystem(ipfs *core.IpfsNode, sk ci.PrivKey, ipfspath string) (*FileSystem, error) {
|
|
root, err := CreateRoot(ipfs, []ci.PrivKey{sk}, ipfspath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &FileSystem{Ipfs: ipfs, RootNode: root}, nil
|
|
}
|
|
|
|
func CreateRoot(n *core.IpfsNode, keys []ci.PrivKey, ipfsroot string) (*Root, error) {
|
|
root := new(Root)
|
|
root.LocalDirs = make(map[string]*Node)
|
|
root.Ipfs = n
|
|
abspath, err := filepath.Abs(ipfsroot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root.IpfsRoot = abspath
|
|
|
|
root.Keys = keys
|
|
|
|
if len(keys) == 0 {
|
|
log.Warning("No keys given for ipns root creation")
|
|
} else {
|
|
k := keys[0]
|
|
pub := k.GetPublic()
|
|
hash, err := pub.Hash()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root.LocalLink = &Link{u.Key(hash).Pretty()}
|
|
}
|
|
|
|
for _, k := range keys {
|
|
hash, err := k.GetPublic().Hash()
|
|
if err != nil {
|
|
log.Debug("failed to hash public key.")
|
|
continue
|
|
}
|
|
name := u.Key(hash).Pretty()
|
|
nd := new(Node)
|
|
nd.Ipfs = n
|
|
nd.key = k
|
|
nd.repub = NewRepublisher(nd, shortRepublishTimeout, longRepublishTimeout)
|
|
|
|
go nd.repub.Run()
|
|
|
|
pointsTo, err := n.Namesys.Resolve(n.Context(), name)
|
|
if err != nil {
|
|
log.Warning("Could not resolve value for local ipns entry, providing empty dir")
|
|
nd.Nd = &mdag.Node{Data: ft.FolderPBData()}
|
|
root.LocalDirs[name] = nd
|
|
continue
|
|
}
|
|
|
|
if !u.IsValidHash(pointsTo.B58String()) {
|
|
log.Criticalf("Got back bad data from namesys resolve! [%s]", pointsTo)
|
|
return nil, nil
|
|
}
|
|
|
|
node, err := n.Resolver.ResolvePath(path.Path(pointsTo.B58String()))
|
|
if err != nil {
|
|
log.Warning("Failed to resolve value from ipns entry in ipfs")
|
|
continue
|
|
}
|
|
|
|
nd.Nd = node
|
|
root.LocalDirs[name] = nd
|
|
}
|
|
|
|
return root, nil
|
|
}
|
|
|
|
// Root constructs the Root of the filesystem, a Root object.
|
|
func (f FileSystem) Root() (fs.Node, fuse.Error) {
|
|
return f.RootNode, nil
|
|
}
|
|
|
|
// Root is the root object of the filesystem tree.
|
|
type Root struct {
|
|
Ipfs *core.IpfsNode
|
|
Keys []ci.PrivKey
|
|
|
|
// Used for symlinking into ipfs
|
|
IpfsRoot string
|
|
LocalDirs map[string]*Node
|
|
|
|
LocalLink *Link
|
|
}
|
|
|
|
// Attr returns file attributes.
|
|
func (*Root) Attr() fuse.Attr {
|
|
return fuse.Attr{Mode: os.ModeDir | 0111} // -rw+x
|
|
}
|
|
|
|
// Lookup performs a lookup under this node.
|
|
func (s *Root) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) {
|
|
switch name {
|
|
case "mach_kernel", ".hidden", "._.":
|
|
// Just quiet some log noise on OS X.
|
|
return nil, fuse.ENOENT
|
|
}
|
|
|
|
if name == "local" {
|
|
if s.LocalLink == nil {
|
|
return nil, fuse.ENOENT
|
|
}
|
|
return s.LocalLink, nil
|
|
}
|
|
|
|
nd, ok := s.LocalDirs[name]
|
|
if ok {
|
|
return nd, nil
|
|
}
|
|
|
|
resolved, err := s.Ipfs.Namesys.Resolve(s.Ipfs.Context(), name)
|
|
if err != nil {
|
|
log.Warningf("ipns: namesys resolve error: %s", err)
|
|
return nil, fuse.ENOENT
|
|
}
|
|
|
|
return &Link{s.IpfsRoot + "/" + resolved.B58String()}, nil
|
|
}
|
|
|
|
// ReadDir reads a particular directory. Disallowed for root.
|
|
func (r *Root) ReadDir(intr fs.Intr) ([]fuse.Dirent, fuse.Error) {
|
|
listing := []fuse.Dirent{
|
|
fuse.Dirent{
|
|
Name: "local",
|
|
Type: fuse.DT_Link,
|
|
},
|
|
}
|
|
for _, k := range r.Keys {
|
|
pub := k.GetPublic()
|
|
hash, err := pub.Hash()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
ent := fuse.Dirent{
|
|
Name: u.Key(hash).Pretty(),
|
|
Type: fuse.DT_Dir,
|
|
}
|
|
listing = append(listing, ent)
|
|
}
|
|
return listing, nil
|
|
}
|
|
|
|
// Node is the core object representing a filesystem tree node.
|
|
type Node struct {
|
|
root *Root
|
|
nsRoot *Node
|
|
parent *Node
|
|
|
|
repub *Republisher
|
|
|
|
// This nodes name in its parent dir.
|
|
// NOTE: this strategy wont work well if we allow hard links
|
|
// (im all for murdering the thought of hard links)
|
|
name string
|
|
|
|
// Private keys held by nodes at the root of a keyspace
|
|
// WARNING(security): the PrivKey interface is currently insecure
|
|
// (holds the raw key). It will be secured later.
|
|
key ci.PrivKey
|
|
|
|
Ipfs *core.IpfsNode
|
|
Nd *mdag.Node
|
|
dagMod *uio.DagModifier
|
|
cached *ftpb.Data
|
|
}
|
|
|
|
func (s *Node) loadData() error {
|
|
s.cached = new(ftpb.Data)
|
|
return proto.Unmarshal(s.Nd.Data, s.cached)
|
|
}
|
|
|
|
// Attr returns the attributes of a given node.
|
|
func (s *Node) Attr() fuse.Attr {
|
|
if s.cached == nil {
|
|
err := s.loadData()
|
|
if err != nil {
|
|
log.Debugf("Error loading PBData for file: '%s'", s.name)
|
|
}
|
|
}
|
|
switch s.cached.GetType() {
|
|
case ftpb.Data_Directory:
|
|
return fuse.Attr{Mode: os.ModeDir | 0555}
|
|
case ftpb.Data_File, ftpb.Data_Raw:
|
|
size, err := ft.DataSize(s.Nd.Data)
|
|
if err != nil {
|
|
log.Debugf("Error getting size of file: %s", err)
|
|
size = 0
|
|
}
|
|
if size == 0 {
|
|
size = s.dagMod.Size()
|
|
}
|
|
|
|
mode := os.FileMode(0666)
|
|
if IpnsReadonly {
|
|
mode = 0444
|
|
}
|
|
|
|
return fuse.Attr{
|
|
Mode: mode,
|
|
Size: size,
|
|
Blocks: uint64(len(s.Nd.Links)),
|
|
}
|
|
default:
|
|
log.Debug("Invalid data type.")
|
|
return fuse.Attr{}
|
|
}
|
|
}
|
|
|
|
// Lookup performs a lookup under this node.
|
|
func (s *Node) Lookup(name string, intr fs.Intr) (fs.Node, fuse.Error) {
|
|
nodes, err := s.Ipfs.Resolver.ResolveLinks(s.Nd, []string{name})
|
|
if err != nil {
|
|
// todo: make this error more versatile.
|
|
return nil, fuse.ENOENT
|
|
}
|
|
|
|
return s.makeChild(name, nodes[len(nodes)-1]), nil
|
|
}
|
|
|
|
func (n *Node) makeChild(name string, node *mdag.Node) *Node {
|
|
child := &Node{
|
|
Ipfs: n.Ipfs,
|
|
Nd: node,
|
|
name: name,
|
|
nsRoot: n.nsRoot,
|
|
parent: n,
|
|
}
|
|
|
|
// Always ensure that each child knows where the root is
|
|
if n.nsRoot == nil {
|
|
child.nsRoot = n
|
|
} else {
|
|
child.nsRoot = n.nsRoot
|
|
}
|
|
|
|
return child
|
|
}
|
|
|
|
// ReadDir reads the link structure as directory entries
|
|
func (s *Node) ReadDir(intr fs.Intr) ([]fuse.Dirent, fuse.Error) {
|
|
entries := make([]fuse.Dirent, len(s.Nd.Links))
|
|
for i, link := range s.Nd.Links {
|
|
n := link.Name
|
|
if len(n) == 0 {
|
|
n = link.Hash.B58String()
|
|
}
|
|
entries[i] = fuse.Dirent{Name: n, Type: fuse.DT_File}
|
|
}
|
|
|
|
if len(entries) > 0 {
|
|
return entries, nil
|
|
}
|
|
return nil, fuse.ENOENT
|
|
}
|
|
|
|
func (s *Node) Read(req *fuse.ReadRequest, resp *fuse.ReadResponse, intr fs.Intr) fuse.Error {
|
|
// intr will be closed by fuse if the request is cancelled. turn this into a context.
|
|
ctx, cancel := context.WithCancel(context.TODO())
|
|
defer cancel() // make sure all operations we started close.
|
|
|
|
// we wait on intr and cancel our context if it closes.
|
|
go func() {
|
|
select {
|
|
case <-intr: // closed by fuse
|
|
cancel() // cancel our context
|
|
case <-ctx.Done():
|
|
}
|
|
}()
|
|
|
|
k, err := s.Nd.Key()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// setup our logging event
|
|
lm := make(lgbl.DeferredMap)
|
|
lm["fs"] = "ipns"
|
|
lm["key"] = func() interface{} { return k.Pretty() }
|
|
lm["req_offset"] = req.Offset
|
|
lm["req_size"] = req.Size
|
|
defer log.EventBegin(ctx, "fuseRead", lm).Done()
|
|
|
|
r, err := uio.NewDagReader(ctx, s.Nd, s.Ipfs.DAG)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o, err := r.Seek(req.Offset, os.SEEK_SET)
|
|
lm["res_offset"] = o
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, err := io.ReadFull(r, resp.Data[:req.Size])
|
|
resp.Data = resp.Data[:n]
|
|
lm["res_size"] = n
|
|
return err // may be non-nil / not succeeded
|
|
}
|
|
|
|
func (n *Node) Write(req *fuse.WriteRequest, resp *fuse.WriteResponse, intr fs.Intr) fuse.Error {
|
|
// log.Debugf("ipns: Node Write [%s]: flags = %s, offset = %d, size = %d", n.name, req.Flags.String(), req.Offset, len(req.Data))
|
|
if IpnsReadonly {
|
|
log.Debug("Attempted to write on readonly ipns filesystem.")
|
|
return fuse.EPERM
|
|
}
|
|
|
|
if n.dagMod == nil {
|
|
// Create a DagModifier to allow us to change the existing dag node
|
|
dmod, err := uio.NewDagModifier(n.Nd, n.Ipfs.DAG, chunk.DefaultSplitter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.dagMod = dmod
|
|
}
|
|
wrote, err := n.dagMod.WriteAt(req.Data, uint64(req.Offset))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Size = wrote
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) Flush(req *fuse.FlushRequest, intr fs.Intr) fuse.Error {
|
|
if IpnsReadonly {
|
|
return nil
|
|
}
|
|
|
|
// If a write has happened
|
|
if n.dagMod != nil {
|
|
newNode, err := n.dagMod.GetNode()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if n.parent != nil {
|
|
log.Error("updating self in parent!")
|
|
err := n.parent.update(n.name, newNode)
|
|
if err != nil {
|
|
log.Criticalf("error in updating ipns dag tree: %s", err)
|
|
// return fuse.ETHISISPRETTYBAD
|
|
return err
|
|
}
|
|
}
|
|
n.Nd = newNode
|
|
|
|
/*/TEMP
|
|
dr, err := mdag.NewDagReader(n.Nd, n.Ipfs.DAG)
|
|
if err != nil {
|
|
log.Critical("Verification read failed.")
|
|
}
|
|
b, err := ioutil.ReadAll(dr)
|
|
if err != nil {
|
|
log.Critical("Verification read failed.")
|
|
}
|
|
fmt.Println("VERIFICATION READ")
|
|
fmt.Printf("READ %d BYTES\n", len(b))
|
|
fmt.Println(string(b))
|
|
fmt.Println(b)
|
|
//*/
|
|
|
|
n.dagMod = nil
|
|
|
|
n.wasChanged()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Signal that a node in this tree was changed so the root can republish
|
|
func (n *Node) wasChanged() {
|
|
if IpnsReadonly {
|
|
return
|
|
}
|
|
root := n.nsRoot
|
|
if root == nil {
|
|
root = n
|
|
}
|
|
|
|
root.repub.Publish <- struct{}{}
|
|
}
|
|
|
|
func (n *Node) republishRoot() error {
|
|
|
|
// We should already be the root, this is just a sanity check
|
|
var root *Node
|
|
if n.nsRoot != nil {
|
|
root = n.nsRoot
|
|
} else {
|
|
root = n
|
|
}
|
|
|
|
// Add any nodes that may be new to the DAG service
|
|
err := n.Ipfs.DAG.AddRecursive(root.Nd)
|
|
if err != nil {
|
|
log.Criticalf("ipns: Dag Add Error: %s", err)
|
|
return err
|
|
}
|
|
|
|
ndkey, err := root.Nd.Key()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = n.Ipfs.Namesys.Publish(n.Ipfs.Context(), root.key, ndkey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) Fsync(req *fuse.FsyncRequest, intr fs.Intr) fuse.Error {
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) Mkdir(req *fuse.MkdirRequest, intr fs.Intr) (fs.Node, fuse.Error) {
|
|
if IpnsReadonly {
|
|
return nil, fuse.EPERM
|
|
}
|
|
dagnd := &mdag.Node{Data: ft.FolderPBData()}
|
|
nnode := n.Nd.Copy()
|
|
nnode.AddNodeLink(req.Name, dagnd)
|
|
|
|
child := &Node{
|
|
Ipfs: n.Ipfs,
|
|
Nd: dagnd,
|
|
name: req.Name,
|
|
}
|
|
|
|
if n.nsRoot == nil {
|
|
child.nsRoot = n
|
|
} else {
|
|
child.nsRoot = n.nsRoot
|
|
}
|
|
|
|
if n.parent != nil {
|
|
err := n.parent.update(n.name, nnode)
|
|
if err != nil {
|
|
log.Criticalf("Error updating node: %s", err)
|
|
return nil, err
|
|
}
|
|
}
|
|
n.Nd = nnode
|
|
|
|
n.wasChanged()
|
|
|
|
return child, nil
|
|
}
|
|
|
|
func (n *Node) Open(req *fuse.OpenRequest, resp *fuse.OpenResponse, intr fs.Intr) (fs.Handle, fuse.Error) {
|
|
//log.Debug("[%s] Received open request! flags = %s", n.name, req.Flags.String())
|
|
//TODO: check open flags and truncate if necessary
|
|
if req.Flags&fuse.OpenTruncate != 0 {
|
|
log.Warning("Need to truncate file!")
|
|
n.cached = nil
|
|
n.Nd = &mdag.Node{Data: ft.FilePBData(nil, 0)}
|
|
} else if req.Flags&fuse.OpenAppend != 0 {
|
|
log.Warning("Need to append to file!")
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func (n *Node) Mknod(req *fuse.MknodRequest, intr fs.Intr) (fs.Node, fuse.Error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *Node) Create(req *fuse.CreateRequest, resp *fuse.CreateResponse, intr fs.Intr) (fs.Node, fs.Handle, fuse.Error) {
|
|
if IpnsReadonly {
|
|
log.Debug("Attempted to call Create on a readonly filesystem.")
|
|
return nil, nil, fuse.EPERM
|
|
}
|
|
|
|
// New 'empty' file
|
|
nd := &mdag.Node{Data: ft.FilePBData(nil, 0)}
|
|
child := n.makeChild(req.Name, nd)
|
|
|
|
nnode := n.Nd.Copy()
|
|
|
|
err := nnode.AddNodeLink(req.Name, nd)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if n.parent != nil {
|
|
err := n.parent.update(n.name, nnode)
|
|
if err != nil {
|
|
log.Criticalf("Error updating node: %s", err)
|
|
// Can we panic, please?
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
n.Nd = nnode
|
|
n.wasChanged()
|
|
|
|
return child, child, nil
|
|
}
|
|
|
|
func (n *Node) Remove(req *fuse.RemoveRequest, intr fs.Intr) fuse.Error {
|
|
if IpnsReadonly {
|
|
return fuse.EPERM
|
|
}
|
|
|
|
nnode := n.Nd.Copy()
|
|
err := nnode.RemoveNodeLink(req.Name)
|
|
if err != nil {
|
|
return fuse.ENOENT
|
|
}
|
|
|
|
if n.parent != nil {
|
|
err := n.parent.update(n.name, nnode)
|
|
if err != nil {
|
|
log.Criticalf("Error updating node: %s", err)
|
|
return err
|
|
}
|
|
}
|
|
n.Nd = nnode
|
|
n.wasChanged()
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) Rename(req *fuse.RenameRequest, newDir fs.Node, intr fs.Intr) fuse.Error {
|
|
if IpnsReadonly {
|
|
log.Debug("Attempted to call Rename on a readonly filesystem.")
|
|
return fuse.EPERM
|
|
}
|
|
|
|
var mdn *mdag.Node
|
|
for _, l := range n.Nd.Links {
|
|
if l.Name == req.OldName {
|
|
mdn = l.Node
|
|
}
|
|
}
|
|
if mdn == nil {
|
|
log.Critical("nil Link found on rename!")
|
|
return fuse.ENOENT
|
|
}
|
|
n.Nd.RemoveNodeLink(req.OldName)
|
|
|
|
switch newDir := newDir.(type) {
|
|
case *Node:
|
|
err := newDir.Nd.AddNodeLink(req.NewName, mdn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
log.Critical("Unknown node type for rename target dir!")
|
|
return errors.New("Unknown fs node type!")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Updates the child of this node, specified by name to the given newnode
|
|
func (n *Node) update(name string, newnode *mdag.Node) error {
|
|
nnode, err := n.Nd.UpdateNodeLink(name, newnode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if n.parent != nil {
|
|
err := n.parent.update(n.name, nnode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
n.Nd = nnode
|
|
return nil
|
|
}
|