forked from TrueCloudLab/restic
fuse: Rewrite fuse implementation
This commit is contained in:
parent
f676c0c41b
commit
52752659c1
5 changed files with 272 additions and 193 deletions
|
@ -96,14 +96,26 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
|
||||||
return err
|
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("Now serving the repository at %s\n", mountpoint)
|
||||||
Printf("Don't forget to umount after quitting!\n")
|
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)
|
debug.Log("serving mount at %v", mountpoint)
|
||||||
err = fs.Serve(c, &root)
|
err = fs.Serve(c, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
33
src/restic/fuse/blob_size_cache.go
Normal file
33
src/restic/fuse/blob_size_cache.go
Normal file
|
@ -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
|
||||||
|
}
|
105
src/restic/fuse/dir_snapshots.go
Normal file
105
src/restic/fuse/dir_snapshots.go
Normal file
|
@ -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)
|
||||||
|
}
|
118
src/restic/fuse/root.go
Normal file
118
src/restic/fuse/root.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
Loading…
Reference in a new issue