From 52752659c18281a70552c975834e543e51f85b12 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 18 Jun 2017 14:59:44 +0200 Subject: [PATCH] fuse: Rewrite fuse implementation --- src/cmds/restic/cmd_mount.go | 20 ++- src/restic/fuse/blob_size_cache.go | 33 +++++ src/restic/fuse/dir_snapshots.go | 105 ++++++++++++++++ src/restic/fuse/root.go | 118 ++++++++++++++++++ src/restic/fuse/snapshot.go | 189 ----------------------------- 5 files changed, 272 insertions(+), 193 deletions(-) create mode 100644 src/restic/fuse/blob_size_cache.go create mode 100644 src/restic/fuse/dir_snapshots.go create mode 100644 src/restic/fuse/root.go delete mode 100644 src/restic/fuse/snapshot.go diff --git a/src/cmds/restic/cmd_mount.go b/src/cmds/restic/cmd_mount.go index 86e27b351..243fa4e37 100644 --- a/src/cmds/restic/cmd_mount.go +++ b/src/cmds/restic/cmd_mount.go @@ -96,14 +96,26 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error { return err } + systemFuse.Debug = func(msg interface{}) { + debug.Log("fuse: %v", msg) + } + + cfg := fuse.Config{ + OwnerIsRoot: opts.OwnerRoot, + Host: opts.Host, + Tags: opts.Tags, + Paths: opts.Paths, + } + root, err := fuse.NewRoot(context.TODO(), repo, cfg) + if err != nil { + return err + } + Printf("Now serving the repository at %s\n", mountpoint) Printf("Don't forget to umount after quitting!\n") - root := fs.Tree{} - root.Add("snapshots", fuse.NewSnapshotsDir(repo, opts.OwnerRoot, opts.Paths, opts.Tags, opts.Host)) - debug.Log("serving mount at %v", mountpoint) - err = fs.Serve(c, &root) + err = fs.Serve(c, root) if err != nil { return err } diff --git a/src/restic/fuse/blob_size_cache.go b/src/restic/fuse/blob_size_cache.go new file mode 100644 index 000000000..d74bfe8be --- /dev/null +++ b/src/restic/fuse/blob_size_cache.go @@ -0,0 +1,33 @@ +package fuse + +import ( + "restic" + + "golang.org/x/net/context" +) + +// BlobSizeCache caches the size of blobs in the repo. +type BlobSizeCache struct { + m map[restic.ID]uint +} + +// NewBlobSizeCache returns a new blob size cache containing all entries from midx. +func NewBlobSizeCache(ctx context.Context, idx restic.Index) *BlobSizeCache { + m := make(map[restic.ID]uint, 1000) + for pb := range idx.Each(ctx) { + m[pb.ID] = pb.Length + } + return &BlobSizeCache{ + m: m, + } +} + +// Lookup returns the size of the blob id. +func (c *BlobSizeCache) Lookup(id restic.ID) (size uint, found bool) { + if c == nil { + return 0, false + } + + size, found = c.m[id] + return size, found +} diff --git a/src/restic/fuse/dir_snapshots.go b/src/restic/fuse/dir_snapshots.go new file mode 100644 index 000000000..f51cff888 --- /dev/null +++ b/src/restic/fuse/dir_snapshots.go @@ -0,0 +1,105 @@ +package fuse + +import ( + "fmt" + "os" + "restic" + "restic/debug" + "time" + + "golang.org/x/net/context" + + "bazil.org/fuse" + "bazil.org/fuse/fs" +) + +// DirSnapshots is a fuse directory which contains snapshots. +type DirSnapshots struct { + inode uint64 + root *Root + snapshots restic.Snapshots + names map[string]*restic.Snapshot +} + +// ensure that *DirSnapshots implements these interfaces +var _ = fs.HandleReadDirAller(&DirSnapshots{}) +var _ = fs.NodeStringLookuper(&DirSnapshots{}) + +// NewDirSnapshots returns a new directory containing snapshots. +func NewDirSnapshots(root *Root, inode uint64, snapshots restic.Snapshots) *DirSnapshots { + debug.Log("create snapshots dir with %d snapshots, inode %d", len(snapshots), inode) + d := &DirSnapshots{ + root: root, + inode: inode, + snapshots: snapshots, + names: make(map[string]*restic.Snapshot, len(snapshots)), + } + + for _, sn := range snapshots { + name := sn.Time.Format(time.RFC3339) + for i := 1; ; i++ { + if _, ok := d.names[name]; !ok { + break + } + + name = fmt.Sprintf("%s-%d", sn.Time.Format(time.RFC3339), i) + } + + d.names[name] = sn + debug.Log(" add snapshot %v as dir %v", sn.ID().Str(), name) + } + + return d +} + +// Attr returns the attributes for the root node. +func (d *DirSnapshots) Attr(ctx context.Context, attr *fuse.Attr) error { + attr.Inode = d.inode + attr.Mode = os.ModeDir | 0555 + + if !d.root.cfg.OwnerIsRoot { + attr.Uid = uint32(os.Getuid()) + attr.Gid = uint32(os.Getgid()) + } + debug.Log("attr: %v", attr) + return nil +} + +// ReadDirAll returns all entries of the root node. +func (d *DirSnapshots) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { + debug.Log("ReadDirAll()") + items := []fuse.Dirent{ + { + Inode: d.inode, + Name: ".", + Type: fuse.DT_Dir, + }, + { + Inode: d.root.inode, + Name: "..", + Type: fuse.DT_Dir, + }, + } + + for name := range d.names { + items = append(items, fuse.Dirent{ + Inode: fs.GenerateDynamicInode(d.inode, name), + Name: name, + Type: fuse.DT_Dir, + }) + } + + return items, nil +} + +// Lookup returns a specific entry from the root node. +func (d *DirSnapshots) Lookup(ctx context.Context, name string) (fs.Node, error) { + debug.Log("Lookup(%s)", name) + + sn, ok := d.names[name] + if !ok { + return nil, fuse.ENOENT + } + + return newDirFromSnapshot(ctx, d.root.repo, sn, d.root.cfg.OwnerIsRoot, d.root.blobSizeCache) +} diff --git a/src/restic/fuse/root.go b/src/restic/fuse/root.go new file mode 100644 index 000000000..b75ccece3 --- /dev/null +++ b/src/restic/fuse/root.go @@ -0,0 +1,118 @@ +package fuse + +import ( + "os" + "restic" + "restic/debug" + + "golang.org/x/net/context" + + "bazil.org/fuse" + "bazil.org/fuse/fs" +) + +// Config holds settings for the fuse mount. +type Config struct { + OwnerIsRoot bool + Host string + Tags []string + Paths []string +} + +// Root is the root node of the fuse mount of a repository. +type Root struct { + repo restic.Repository + cfg Config + inode uint64 + snapshots restic.Snapshots + dirSnapshots *DirSnapshots + blobSizeCache *BlobSizeCache +} + +// ensure that *Root implements these interfaces +var _ = fs.HandleReadDirAller(&Root{}) +var _ = fs.NodeStringLookuper(&Root{}) + +// NewRoot initializes a new root node from a repository. +func NewRoot(ctx context.Context, repo restic.Repository, cfg Config) (*Root, error) { + debug.Log("NewRoot(), config %v", cfg) + + snapshots := restic.FindFilteredSnapshots(ctx, repo, cfg.Host, cfg.Tags, cfg.Paths) + debug.Log("found %d matching snapshots", len(snapshots)) + + root := &Root{ + repo: repo, + cfg: cfg, + inode: 1, + snapshots: snapshots, + } + + root.dirSnapshots = NewDirSnapshots(root, fs.GenerateDynamicInode(root.inode, "snapshots"), snapshots) + root.blobSizeCache = NewBlobSizeCache(ctx, repo.Index()) + + return root, nil +} + +// Root is just there to satisfy fs.Root, it returns itself. +func (r *Root) Root() (fs.Node, error) { + debug.Log("Root()") + return r, nil +} + +// Attr returns the attributes for the root node. +func (r *Root) Attr(ctx context.Context, attr *fuse.Attr) error { + attr.Inode = r.inode + attr.Mode = os.ModeDir | 0555 + + if !r.cfg.OwnerIsRoot { + attr.Uid = uint32(os.Getuid()) + attr.Gid = uint32(os.Getgid()) + } + debug.Log("attr: %v", attr) + return nil +} + +// ReadDirAll returns all entries of the root node. +func (r *Root) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { + debug.Log("ReadDirAll()") + items := []fuse.Dirent{ + { + Inode: r.inode, + Name: ".", + Type: fuse.DT_Dir, + }, + { + Inode: r.inode, + Name: "..", + Type: fuse.DT_Dir, + }, + { + Inode: fs.GenerateDynamicInode(r.inode, "snapshots"), + Name: "snapshots", + Type: fuse.DT_Dir, + }, + // { + // Inode: fs.GenerateDynamicInode(0, "tags"), + // Name: "tags", + // Type: fuse.DT_Dir, + // }, + // { + // Inode: fs.GenerateDynamicInode(0, "hosts"), + // Name: "hosts", + // Type: fuse.DT_Dir, + // }, + } + + return items, nil +} + +// Lookup returns a specific entry from the root node. +func (r *Root) Lookup(ctx context.Context, name string) (fs.Node, error) { + debug.Log("Lookup(%s)", name) + switch name { + case "snapshots": + return r.dirSnapshots, nil + } + + return nil, fuse.ENOENT +} diff --git a/src/restic/fuse/snapshot.go b/src/restic/fuse/snapshot.go deleted file mode 100644 index af560bb55..000000000 --- a/src/restic/fuse/snapshot.go +++ /dev/null @@ -1,189 +0,0 @@ -// +build !openbsd -// +build !windows - -package fuse - -import ( - "fmt" - "os" - "sync" - "time" - - "bazil.org/fuse" - "bazil.org/fuse/fs" - - "restic" - "restic/debug" - "restic/repository" - - "golang.org/x/net/context" -) - -// BlobSizeCache caches the size of blobs in the repo. -type BlobSizeCache struct { - m map[restic.ID]uint -} - -// NewBlobSizeCache returns a new blob size cache containing all entries from midx. -func NewBlobSizeCache(midx *repository.MasterIndex) *BlobSizeCache { - m := make(map[restic.ID]uint, 1000) - for _, idx := range midx.All() { - for pb := range idx.Each(context.TODO()) { - m[pb.ID] = pb.Length - } - } - return &BlobSizeCache{ - m: m, - } -} - -// Lookup returns the size of the blob id. -func (c *BlobSizeCache) Lookup(id restic.ID) (size uint, found bool) { - if c == nil { - return 0, false - } - - size, found = c.m[id] - return size, found -} - -// These lines statically ensure that a *SnapshotsDir implement the given -// interfaces; a misplaced refactoring of the implementation that breaks -// the interface will be catched by the compiler -var _ = fs.HandleReadDirAller(&SnapshotsDir{}) -var _ = fs.NodeStringLookuper(&SnapshotsDir{}) - -type SnapshotsDir struct { - repo restic.Repository - ownerIsRoot bool - paths []string - tags []string - host string - - blobsize *BlobSizeCache - - // knownSnapshots maps snapshot timestamp to the snapshot - sync.Mutex - knownSnapshots map[string]*restic.Snapshot - processed restic.IDSet -} - -// NewSnapshotsDir returns a new dir object for the snapshots. -func NewSnapshotsDir(repo restic.Repository, ownerIsRoot bool, paths []string, tags []string, host string) *SnapshotsDir { - debug.Log("fuse mount initiated") - return &SnapshotsDir{ - repo: repo, - ownerIsRoot: ownerIsRoot, - paths: paths, - tags: tags, - host: host, - knownSnapshots: make(map[string]*restic.Snapshot), - processed: restic.NewIDSet(), - blobsize: NewBlobSizeCache(repo.Index().(*repository.MasterIndex)), - } -} - -func (sn *SnapshotsDir) Attr(ctx context.Context, attr *fuse.Attr) error { - attr.Inode = 0 - attr.Mode = os.ModeDir | 0555 - - if !sn.ownerIsRoot { - attr.Uid = uint32(os.Getuid()) - attr.Gid = uint32(os.Getgid()) - } - debug.Log("attr is %v", attr) - return nil -} - -func (sn *SnapshotsDir) updateCache(ctx context.Context) error { - debug.Log("called") - sn.Lock() - defer sn.Unlock() - - for id := range sn.repo.List(ctx, restic.SnapshotFile) { - if sn.processed.Has(id) { - debug.Log("skipping snapshot %v, already in list", id.Str()) - continue - } - - debug.Log("found snapshot id %v", id.Str()) - snapshot, err := restic.LoadSnapshot(ctx, sn.repo, id) - if err != nil { - return err - } - - // Filter snapshots we don't care for. - if (sn.host != "" && sn.host != snapshot.Hostname) || - !snapshot.HasTags(sn.tags) || - !snapshot.HasPaths(sn.paths) { - continue - } - - timestamp := snapshot.Time.Format(time.RFC3339) - for i := 1; ; i++ { - if _, ok := sn.knownSnapshots[timestamp]; !ok { - break - } - - timestamp = fmt.Sprintf("%s-%d", snapshot.Time.Format(time.RFC3339), i) - } - - debug.Log(" add %v as dir %v", id.Str(), timestamp) - sn.knownSnapshots[timestamp] = snapshot - sn.processed.Insert(id) - } - return nil -} - -func (sn *SnapshotsDir) get(name string) (snapshot *restic.Snapshot, ok bool) { - sn.Lock() - snapshot, ok = sn.knownSnapshots[name] - sn.Unlock() - debug.Log("get(%s) -> %v %v", name, snapshot, ok) - return snapshot, ok -} - -func (sn *SnapshotsDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { - debug.Log("called") - err := sn.updateCache(ctx) - if err != nil { - return nil, err - } - - sn.Lock() - defer sn.Unlock() - - ret := make([]fuse.Dirent, 0) - for timestamp, snapshot := range sn.knownSnapshots { - ret = append(ret, fuse.Dirent{ - Inode: inodeFromBackendID(*snapshot.ID()), - Type: fuse.DT_Dir, - Name: timestamp, - }) - } - - debug.Log(" -> %d entries", len(ret)) - return ret, nil -} - -func (sn *SnapshotsDir) Lookup(ctx context.Context, name string) (fs.Node, error) { - debug.Log("Lookup(%s)", name) - snapshot, ok := sn.get(name) - - if !ok { - // We don't know about it, update the cache - err := sn.updateCache(ctx) - if err != nil { - debug.Log(" Lookup(%s) -> err %v", name, err) - return nil, err - } - snapshot, ok = sn.get(name) - if !ok { - // We still don't know about it, this time it really doesn't exist - debug.Log(" Lookup(%s) -> not found", name) - return nil, fuse.ENOENT - } - } - - return newDirFromSnapshot(ctx, sn.repo, snapshot, sn.ownerIsRoot, sn.blobsize) -}