diff --git a/cmd/restic/cmd_mount.go b/cmd/restic/cmd_mount.go index 033c98850..d06274d00 100644 --- a/cmd/restic/cmd_mount.go +++ b/cmd/restic/cmd_mount.go @@ -3,21 +3,12 @@ package main import ( - "encoding/binary" "fmt" "os" - "sync" - "time" - "golang.org/x/net/context" + "github.com/restic/restic/cmd/restic/fuse" - "github.com/restic/restic" - "github.com/restic/restic/backend" - "github.com/restic/restic/crypto" - "github.com/restic/restic/pack" - "github.com/restic/restic/repository" - - "bazil.org/fuse" + systemFuse "bazil.org/fuse" "bazil.org/fuse/fs" ) @@ -66,20 +57,17 @@ func (cmd CmdMount) Execute(args []string) error { return err } } - c, err := fuse.Mount( + c, err := systemFuse.Mount( mountpoint, - fuse.ReadOnly(), - fuse.FSName("restic"), + systemFuse.ReadOnly(), + systemFuse.FSName("restic"), ) if err != nil { return err } root := fs.Tree{} - root.Add("snapshots", &snapshots{ - repo: repo, - knownSnapshots: make(map[string]snapshotWithId), - }) + root.Add("snapshots", fuse.NewSnapshotsDir(repo)) cmd.global.Printf("Now serving %s at %s\n", repo.Backend().Location(), mountpoint) cmd.global.Printf("Don't forget to umount after quitting!\n") @@ -94,261 +82,3 @@ func (cmd CmdMount) Execute(args []string) error { <-c.Ready return c.MountError } - -type snapshotWithId struct { - *restic.Snapshot - backend.ID -} - -// These lines statically ensure that a *snapshots implement the given -// interfaces; a misplaced refactoring of the implementation that breaks -// the interface will be catched by the compiler -var _ = fs.HandleReadDirAller(&snapshots{}) -var _ = fs.NodeStringLookuper(&snapshots{}) - -type snapshots struct { - repo *repository.Repository - - // knownSnapshots maps snapshot timestamp to the snapshot - sync.RWMutex - knownSnapshots map[string]snapshotWithId -} - -func (sn *snapshots) Attr(ctx context.Context, attr *fuse.Attr) error { - attr.Inode = 0 - attr.Mode = os.ModeDir | 0555 - return nil -} - -func (sn *snapshots) updateCache(ctx context.Context) error { - sn.Lock() - defer sn.Unlock() - - for id := range sn.repo.List(backend.Snapshot, ctx.Done()) { - snapshot, err := restic.LoadSnapshot(sn.repo, id) - if err != nil { - return err - } - sn.knownSnapshots[snapshot.Time.Format(time.RFC3339)] = snapshotWithId{snapshot, id} - } - return nil -} -func (sn *snapshots) get(name string) (snapshot snapshotWithId, ok bool) { - sn.Lock() - snapshot, ok = sn.knownSnapshots[name] - sn.Unlock() - return snapshot, ok -} - -func (sn *snapshots) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { - err := sn.updateCache(ctx) - if err != nil { - return nil, err - } - - sn.RLock() - defer sn.RUnlock() - - ret := make([]fuse.Dirent, 0) - for _, snapshot := range sn.knownSnapshots { - ret = append(ret, fuse.Dirent{ - Inode: binary.BigEndian.Uint64(snapshot.ID[:8]), - Type: fuse.DT_Dir, - Name: snapshot.Time.Format(time.RFC3339), - }) - } - - return ret, nil -} - -func (sn *snapshots) Lookup(ctx context.Context, name string) (fs.Node, error) { - snapshot, ok := sn.get(name) - - if !ok { - // We don't know about it, update the cache - err := sn.updateCache(ctx) - if err != nil { - return nil, err - } - snapshot, ok = sn.get(name) - if !ok { - // We still don't know about it, this time it really doesn't exist - return nil, fuse.ENOENT - } - } - - return newDirFromSnapshot(sn.repo, snapshot) -} - -// Statically ensure that *dir implement those interface -var _ = fs.HandleReadDirAller(&dir{}) -var _ = fs.NodeStringLookuper(&dir{}) - -type dir struct { - repo *repository.Repository - children map[string]*restic.Node - inode uint64 -} - -func newDir(repo *repository.Repository, node *restic.Node) (*dir, error) { - tree, err := restic.LoadTree(repo, node.Subtree) - if err != nil { - return nil, err - } - children := make(map[string]*restic.Node) - for _, child := range tree.Nodes { - children[child.Name] = child - } - - return &dir{ - repo: repo, - children: children, - inode: node.Inode, - }, nil -} - -func newDirFromSnapshot(repo *repository.Repository, snapshot snapshotWithId) (*dir, error) { - tree, err := restic.LoadTree(repo, snapshot.Tree) - if err != nil { - return nil, err - } - children := make(map[string]*restic.Node) - for _, node := range tree.Nodes { - children[node.Name] = node - } - - return &dir{ - repo: repo, - children: children, - inode: binary.BigEndian.Uint64(snapshot.ID), - }, nil -} - -func (d *dir) Attr(ctx context.Context, a *fuse.Attr) error { - a.Inode = d.inode - a.Mode = os.ModeDir | 0555 - return nil -} - -func (d *dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { - ret := make([]fuse.Dirent, 0, len(d.children)) - - for _, node := range d.children { - var typ fuse.DirentType - switch { - case node.Mode.IsDir(): - typ = fuse.DT_Dir - case node.Mode.IsRegular(): - typ = fuse.DT_File - } - - ret = append(ret, fuse.Dirent{ - Inode: node.Inode, - Type: typ, - Name: node.Name, - }) - } - - return ret, nil -} - -func (d *dir) Lookup(ctx context.Context, name string) (fs.Node, error) { - child, ok := d.children[name] - if !ok { - return nil, fuse.ENOENT - } - switch { - case child.Mode.IsDir(): - return newDir(d.repo, child) - case child.Mode.IsRegular(): - return newFile(d.repo, child) - default: - return nil, fuse.ENOENT - } -} - -// Statically ensure that *file implements the given interface -var _ = fs.HandleReader(&file{}) - -type file struct { - repo *repository.Repository - node *restic.Node - - sizes []uint32 - blobs [][]byte -} - -func newFile(repo *repository.Repository, node *restic.Node) (*file, error) { - sizes := make([]uint32, len(node.Content)) - for i, blobId := range node.Content { - _, _, _, length, err := repo.Index().Lookup(blobId) - if err != nil { - return nil, err - } - sizes[i] = uint32(length) - crypto.Extension - } - - return &file{ - repo: repo, - node: node, - sizes: sizes, - blobs: make([][]byte, len(node.Content)), - }, nil -} - -func (f *file) Attr(ctx context.Context, a *fuse.Attr) error { - a.Inode = f.node.Inode - a.Mode = f.node.Mode - a.Size = f.node.Size - return nil -} - -func (f *file) getBlobAt(i int) (blob []byte, err error) { - if f.blobs[i] != nil { - blob = f.blobs[i] - } else { - blob, err = f.repo.LoadBlob(pack.Data, f.node.Content[i]) - if err != nil { - return nil, err - } - f.blobs[i] = blob - } - - return blob, nil -} - -func (f *file) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { - off := req.Offset - - // Skip blobs before the offset - startContent := 0 - for off > int64(f.sizes[startContent]) { - off -= int64(f.sizes[startContent]) - startContent++ - } - - content := make([]byte, req.Size) - allContent := content - for i := startContent; i < len(f.sizes); i++ { - blob, err := f.getBlobAt(i) - if err != nil { - return err - } - - blob = blob[off:] - off = 0 - - var copied int - if len(blob) > len(content) { - copied = copy(content[0:], blob[:len(content)]) - } else { - copied = copy(content[0:], blob) - } - content = content[copied:] - if len(content) == 0 { - break - } - } - resp.Data = allContent - return nil -} diff --git a/cmd/restic/fuse/dir.go b/cmd/restic/fuse/dir.go new file mode 100644 index 000000000..ef9ad9a73 --- /dev/null +++ b/cmd/restic/fuse/dir.go @@ -0,0 +1,100 @@ +package fuse + +import ( + "encoding/binary" + "os" + + "bazil.org/fuse" + "bazil.org/fuse/fs" + "golang.org/x/net/context" + + "github.com/restic/restic" + "github.com/restic/restic/repository" +) + +// Statically ensure that *dir implement those interface +var _ = fs.HandleReadDirAller(&dir{}) +var _ = fs.NodeStringLookuper(&dir{}) + +type dir struct { + repo *repository.Repository + children map[string]*restic.Node + inode uint64 +} + +func newDir(repo *repository.Repository, node *restic.Node) (*dir, error) { + tree, err := restic.LoadTree(repo, node.Subtree) + if err != nil { + return nil, err + } + children := make(map[string]*restic.Node) + for _, child := range tree.Nodes { + children[child.Name] = child + } + + return &dir{ + repo: repo, + children: children, + inode: node.Inode, + }, nil +} + +func newDirFromSnapshot(repo *repository.Repository, snapshot SnapshotWithId) (*dir, error) { + tree, err := restic.LoadTree(repo, snapshot.Tree) + if err != nil { + return nil, err + } + children := make(map[string]*restic.Node) + for _, node := range tree.Nodes { + children[node.Name] = node + } + + return &dir{ + repo: repo, + children: children, + inode: binary.BigEndian.Uint64(snapshot.ID), + }, nil +} + +func (d *dir) Attr(ctx context.Context, a *fuse.Attr) error { + a.Inode = d.inode + a.Mode = os.ModeDir | 0555 + return nil +} + +func (d *dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { + ret := make([]fuse.Dirent, 0, len(d.children)) + + for _, node := range d.children { + var typ fuse.DirentType + switch { + case node.Mode.IsDir(): + typ = fuse.DT_Dir + case node.Mode.IsRegular(): + typ = fuse.DT_File + } + + ret = append(ret, fuse.Dirent{ + Inode: node.Inode, + Type: typ, + Name: node.Name, + }) + } + + return ret, nil +} + +func (d *dir) Lookup(ctx context.Context, name string) (fs.Node, error) { + child, ok := d.children[name] + if !ok { + return nil, fuse.ENOENT + } + switch { + case child.Mode.IsDir(): + return newDir(d.repo, child) + case child.Mode.IsRegular(): + return newFile(d.repo, child) + default: + return nil, fuse.ENOENT + } +} diff --git a/cmd/restic/fuse/file.go b/cmd/restic/fuse/file.go new file mode 100644 index 000000000..ff28ebf85 --- /dev/null +++ b/cmd/restic/fuse/file.go @@ -0,0 +1,98 @@ +package fuse + +import ( + "github.com/restic/restic" + "github.com/restic/restic/crypto" + "github.com/restic/restic/pack" + "github.com/restic/restic/repository" + + "bazil.org/fuse" + "bazil.org/fuse/fs" + "golang.org/x/net/context" +) + +// Statically ensure that *file implements the given interface +var _ = fs.HandleReader(&file{}) + +type file struct { + repo *repository.Repository + node *restic.Node + + sizes []uint32 + blobs [][]byte +} + +func newFile(repo *repository.Repository, node *restic.Node) (*file, error) { + sizes := make([]uint32, len(node.Content)) + for i, blobId := range node.Content { + _, _, _, length, err := repo.Index().Lookup(blobId) + if err != nil { + return nil, err + } + sizes[i] = uint32(length) - crypto.Extension + } + + return &file{ + repo: repo, + node: node, + sizes: sizes, + blobs: make([][]byte, len(node.Content)), + }, nil +} + +func (f *file) Attr(ctx context.Context, a *fuse.Attr) error { + a.Inode = f.node.Inode + a.Mode = f.node.Mode + a.Size = f.node.Size + return nil +} + +func (f *file) getBlobAt(i int) (blob []byte, err error) { + if f.blobs[i] != nil { + blob = f.blobs[i] + } else { + blob, err = f.repo.LoadBlob(pack.Data, f.node.Content[i]) + if err != nil { + return nil, err + } + f.blobs[i] = blob + } + + return blob, nil +} + +func (f *file) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { + off := req.Offset + + // Skip blobs before the offset + startContent := 0 + for off > int64(f.sizes[startContent]) { + off -= int64(f.sizes[startContent]) + startContent++ + } + + content := make([]byte, req.Size) + allContent := content + for i := startContent; i < len(f.sizes); i++ { + blob, err := f.getBlobAt(i) + if err != nil { + return err + } + + blob = blob[off:] + off = 0 + + var copied int + if len(blob) > len(content) { + copied = copy(content[0:], blob[:len(content)]) + } else { + copied = copy(content[0:], blob) + } + content = content[copied:] + if len(content) == 0 { + break + } + } + resp.Data = allContent + return nil +}