mirror of
https://github.com/restic/restic.git
synced 2025-08-12 11:47:43 +00:00
Moves files
This commit is contained in:
36
internal/fuse/blob_size_cache.go
Normal file
36
internal/fuse/blob_size_cache.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// +build !openbsd
|
||||
// +build !windows
|
||||
|
||||
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] = uint(restic.PlaintextLength(int(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
|
||||
}
|
210
internal/fuse/dir.go
Normal file
210
internal/fuse/dir.go
Normal file
@@ -0,0 +1,210 @@
|
||||
// +build !openbsd
|
||||
// +build !windows
|
||||
|
||||
package fuse
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"bazil.org/fuse"
|
||||
"bazil.org/fuse/fs"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"restic"
|
||||
"restic/debug"
|
||||
)
|
||||
|
||||
// Statically ensure that *dir implement those interface
|
||||
var _ = fs.HandleReadDirAller(&dir{})
|
||||
var _ = fs.NodeStringLookuper(&dir{})
|
||||
|
||||
type dir struct {
|
||||
root *Root
|
||||
items map[string]*restic.Node
|
||||
inode uint64
|
||||
parentInode uint64
|
||||
node *restic.Node
|
||||
|
||||
blobsize *BlobSizeCache
|
||||
}
|
||||
|
||||
func newDir(ctx context.Context, root *Root, inode, parentInode uint64, node *restic.Node) (*dir, error) {
|
||||
debug.Log("new dir for %v (%v)", node.Name, node.Subtree.Str())
|
||||
tree, err := root.repo.LoadTree(ctx, *node.Subtree)
|
||||
if err != nil {
|
||||
debug.Log(" error loading tree %v: %v", node.Subtree.Str(), err)
|
||||
return nil, err
|
||||
}
|
||||
items := make(map[string]*restic.Node)
|
||||
for _, node := range tree.Nodes {
|
||||
items[node.Name] = node
|
||||
}
|
||||
|
||||
return &dir{
|
||||
root: root,
|
||||
node: node,
|
||||
items: items,
|
||||
inode: inode,
|
||||
parentInode: parentInode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// replaceSpecialNodes replaces nodes with name "." and "/" by their contents.
|
||||
// Otherwise, the node is returned.
|
||||
func replaceSpecialNodes(ctx context.Context, repo restic.Repository, node *restic.Node) ([]*restic.Node, error) {
|
||||
if node.Type != "dir" || node.Subtree == nil {
|
||||
return []*restic.Node{node}, nil
|
||||
}
|
||||
|
||||
if node.Name != "." && node.Name != "/" {
|
||||
return []*restic.Node{node}, nil
|
||||
}
|
||||
|
||||
tree, err := repo.LoadTree(ctx, *node.Subtree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tree.Nodes, nil
|
||||
}
|
||||
|
||||
func newDirFromSnapshot(ctx context.Context, root *Root, inode uint64, snapshot *restic.Snapshot) (*dir, error) {
|
||||
debug.Log("new dir for snapshot %v (%v)", snapshot.ID().Str(), snapshot.Tree.Str())
|
||||
tree, err := root.repo.LoadTree(ctx, *snapshot.Tree)
|
||||
if err != nil {
|
||||
debug.Log(" loadTree(%v) failed: %v", snapshot.ID().Str(), err)
|
||||
return nil, err
|
||||
}
|
||||
items := make(map[string]*restic.Node)
|
||||
for _, n := range tree.Nodes {
|
||||
nodes, err := replaceSpecialNodes(ctx, root.repo, n)
|
||||
if err != nil {
|
||||
debug.Log(" replaceSpecialNodes(%v) failed: %v", n, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
items[node.Name] = node
|
||||
}
|
||||
}
|
||||
|
||||
return &dir{
|
||||
root: root,
|
||||
node: &restic.Node{
|
||||
UID: uint32(os.Getuid()),
|
||||
GID: uint32(os.Getgid()),
|
||||
AccessTime: snapshot.Time,
|
||||
ModTime: snapshot.Time,
|
||||
ChangeTime: snapshot.Time,
|
||||
Mode: os.ModeDir | 0555,
|
||||
},
|
||||
items: items,
|
||||
inode: inode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *dir) Attr(ctx context.Context, a *fuse.Attr) error {
|
||||
debug.Log("called")
|
||||
a.Inode = d.inode
|
||||
a.Mode = os.ModeDir | d.node.Mode
|
||||
|
||||
if !d.root.cfg.OwnerIsRoot {
|
||||
a.Uid = d.node.UID
|
||||
a.Gid = d.node.GID
|
||||
}
|
||||
a.Atime = d.node.AccessTime
|
||||
a.Ctime = d.node.ChangeTime
|
||||
a.Mtime = d.node.ModTime
|
||||
|
||||
a.Nlink = d.calcNumberOfLinks()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dir) calcNumberOfLinks() uint32 {
|
||||
// a directory d has 2 hardlinks + the number
|
||||
// of directories contained by d
|
||||
var count uint32
|
||||
count = 2
|
||||
for _, node := range d.items {
|
||||
if node.Type == "dir" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (d *dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
|
||||
debug.Log("called")
|
||||
ret := make([]fuse.Dirent, 0, len(d.items)+2)
|
||||
|
||||
ret = append(ret, fuse.Dirent{
|
||||
Inode: d.inode,
|
||||
Name: ".",
|
||||
Type: fuse.DT_Dir,
|
||||
})
|
||||
|
||||
ret = append(ret, fuse.Dirent{
|
||||
Inode: d.parentInode,
|
||||
Name: "..",
|
||||
Type: fuse.DT_Dir,
|
||||
})
|
||||
|
||||
for _, node := range d.items {
|
||||
var typ fuse.DirentType
|
||||
switch node.Type {
|
||||
case "dir":
|
||||
typ = fuse.DT_Dir
|
||||
case "file":
|
||||
typ = fuse.DT_File
|
||||
case "symlink":
|
||||
typ = fuse.DT_Link
|
||||
}
|
||||
|
||||
ret = append(ret, fuse.Dirent{
|
||||
Inode: fs.GenerateDynamicInode(d.inode, node.Name),
|
||||
Type: typ,
|
||||
Name: node.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (d *dir) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
||||
debug.Log("Lookup(%v)", name)
|
||||
node, ok := d.items[name]
|
||||
if !ok {
|
||||
debug.Log(" Lookup(%v) -> not found", name)
|
||||
return nil, fuse.ENOENT
|
||||
}
|
||||
switch node.Type {
|
||||
case "dir":
|
||||
return newDir(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), d.inode, node)
|
||||
case "file":
|
||||
return newFile(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), node)
|
||||
case "symlink":
|
||||
return newLink(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), node)
|
||||
default:
|
||||
debug.Log(" node %v has unknown type %v", name, node.Type)
|
||||
return nil, fuse.ENOENT
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dir) Listxattr(ctx context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error {
|
||||
debug.Log("Listxattr(%v, %v)", d.node.Name, req.Size)
|
||||
for _, attr := range d.node.ExtendedAttributes {
|
||||
resp.Append(attr.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dir) Getxattr(ctx context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
|
||||
debug.Log("Getxattr(%v, %v, %v)", d.node.Name, req.Name, req.Size)
|
||||
attrval := d.node.GetExtendedAttribute(req.Name)
|
||||
if attrval != nil {
|
||||
resp.Xattr = attrval
|
||||
return nil
|
||||
}
|
||||
return fuse.ErrNoXattr
|
||||
}
|
178
internal/fuse/file.go
Normal file
178
internal/fuse/file.go
Normal file
@@ -0,0 +1,178 @@
|
||||
// +build !openbsd
|
||||
// +build !windows
|
||||
|
||||
package fuse
|
||||
|
||||
import (
|
||||
"restic/errors"
|
||||
|
||||
"restic"
|
||||
"restic/debug"
|
||||
|
||||
"bazil.org/fuse"
|
||||
"bazil.org/fuse/fs"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// The default block size to report in stat
|
||||
const blockSize = 512
|
||||
|
||||
// Statically ensure that *file implements the given interface
|
||||
var _ = fs.HandleReader(&file{})
|
||||
var _ = fs.HandleReleaser(&file{})
|
||||
|
||||
type file struct {
|
||||
root *Root
|
||||
node *restic.Node
|
||||
inode uint64
|
||||
|
||||
sizes []int
|
||||
blobs [][]byte
|
||||
}
|
||||
|
||||
func newFile(ctx context.Context, root *Root, inode uint64, node *restic.Node) (fusefile *file, err error) {
|
||||
debug.Log("create new file for %v with %d blobs", node.Name, len(node.Content))
|
||||
var bytes uint64
|
||||
sizes := make([]int, len(node.Content))
|
||||
for i, id := range node.Content {
|
||||
size, ok := root.blobSizeCache.Lookup(id)
|
||||
if !ok {
|
||||
size, err = root.repo.LookupBlobSize(id, restic.DataBlob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
sizes[i] = int(size)
|
||||
bytes += uint64(size)
|
||||
}
|
||||
|
||||
if bytes != node.Size {
|
||||
debug.Log("sizes do not match: node.Size %v != size %v, using real size", node.Size, bytes)
|
||||
node.Size = bytes
|
||||
}
|
||||
|
||||
return &file{
|
||||
inode: inode,
|
||||
root: root,
|
||||
node: node,
|
||||
sizes: sizes,
|
||||
blobs: make([][]byte, len(node.Content)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *file) Attr(ctx context.Context, a *fuse.Attr) error {
|
||||
debug.Log("Attr(%v)", f.node.Name)
|
||||
a.Inode = f.inode
|
||||
a.Mode = f.node.Mode
|
||||
a.Size = f.node.Size
|
||||
a.Blocks = (f.node.Size / blockSize) + 1
|
||||
a.BlockSize = blockSize
|
||||
a.Nlink = uint32(f.node.Links)
|
||||
|
||||
if !f.root.cfg.OwnerIsRoot {
|
||||
a.Uid = f.node.UID
|
||||
a.Gid = f.node.GID
|
||||
}
|
||||
a.Atime = f.node.AccessTime
|
||||
a.Ctime = f.node.ChangeTime
|
||||
a.Mtime = f.node.ModTime
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (f *file) getBlobAt(ctx context.Context, i int) (blob []byte, err error) {
|
||||
debug.Log("getBlobAt(%v, %v)", f.node.Name, i)
|
||||
if f.blobs[i] != nil {
|
||||
return f.blobs[i], nil
|
||||
}
|
||||
|
||||
// release earlier blobs
|
||||
for j := 0; j < i; j++ {
|
||||
f.blobs[j] = nil
|
||||
}
|
||||
|
||||
buf := restic.NewBlobBuffer(f.sizes[i])
|
||||
n, err := f.root.repo.LoadBlob(ctx, restic.DataBlob, f.node.Content[i], buf)
|
||||
if err != nil {
|
||||
debug.Log("LoadBlob(%v, %v) failed: %v", f.node.Name, f.node.Content[i], err)
|
||||
return nil, err
|
||||
}
|
||||
f.blobs[i] = buf[:n]
|
||||
|
||||
return buf[:n], nil
|
||||
}
|
||||
|
||||
func (f *file) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
|
||||
debug.Log("Read(%v, %v, %v), file size %v", f.node.Name, req.Size, req.Offset, f.node.Size)
|
||||
offset := req.Offset
|
||||
|
||||
if uint64(offset) > f.node.Size {
|
||||
debug.Log("Read(%v): offset is greater than file size: %v > %v",
|
||||
f.node.Name, req.Offset, f.node.Size)
|
||||
return errors.New("offset greater than files size")
|
||||
}
|
||||
|
||||
// handle special case: file is empty
|
||||
if f.node.Size == 0 {
|
||||
resp.Data = resp.Data[:0]
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip blobs before the offset
|
||||
startContent := 0
|
||||
for offset > int64(f.sizes[startContent]) {
|
||||
offset -= int64(f.sizes[startContent])
|
||||
startContent++
|
||||
}
|
||||
|
||||
dst := resp.Data[0:req.Size]
|
||||
readBytes := 0
|
||||
remainingBytes := req.Size
|
||||
for i := startContent; remainingBytes > 0 && i < len(f.sizes); i++ {
|
||||
blob, err := f.getBlobAt(ctx, i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if offset > 0 {
|
||||
blob = blob[offset:]
|
||||
offset = 0
|
||||
}
|
||||
|
||||
copied := copy(dst, blob)
|
||||
remainingBytes -= copied
|
||||
readBytes += copied
|
||||
|
||||
dst = dst[copied:]
|
||||
}
|
||||
resp.Data = resp.Data[:readBytes]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *file) Release(ctx context.Context, req *fuse.ReleaseRequest) error {
|
||||
for i := range f.blobs {
|
||||
f.blobs[i] = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *file) Listxattr(ctx context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error {
|
||||
debug.Log("Listxattr(%v, %v)", f.node.Name, req.Size)
|
||||
for _, attr := range f.node.ExtendedAttributes {
|
||||
resp.Append(attr.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *file) Getxattr(ctx context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
|
||||
debug.Log("Getxattr(%v, %v, %v)", f.node.Name, req.Name, req.Size)
|
||||
attrval := f.node.GetExtendedAttribute(req.Name)
|
||||
if attrval != nil {
|
||||
resp.Xattr = attrval
|
||||
return nil
|
||||
}
|
||||
return fuse.ErrNoXattr
|
||||
}
|
146
internal/fuse/file_test.go
Normal file
146
internal/fuse/file_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// +build !openbsd
|
||||
// +build !windows
|
||||
|
||||
package fuse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"restic/repository"
|
||||
|
||||
"bazil.org/fuse"
|
||||
"bazil.org/fuse/fs"
|
||||
|
||||
"restic"
|
||||
. "restic/test"
|
||||
)
|
||||
|
||||
func testRead(t testing.TB, f *file, offset, length int, data []byte) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
req := &fuse.ReadRequest{
|
||||
Offset: int64(offset),
|
||||
Size: length,
|
||||
}
|
||||
resp := &fuse.ReadResponse{
|
||||
Data: data,
|
||||
}
|
||||
OK(t, f.Read(ctx, req, resp))
|
||||
}
|
||||
|
||||
func firstSnapshotID(t testing.TB, repo restic.Repository) (first restic.ID) {
|
||||
for id := range repo.List(context.TODO(), restic.SnapshotFile) {
|
||||
if first.IsNull() {
|
||||
first = id
|
||||
}
|
||||
}
|
||||
return first
|
||||
}
|
||||
|
||||
func loadFirstSnapshot(t testing.TB, repo restic.Repository) *restic.Snapshot {
|
||||
id := firstSnapshotID(t, repo)
|
||||
sn, err := restic.LoadSnapshot(context.TODO(), repo, id)
|
||||
OK(t, err)
|
||||
return sn
|
||||
}
|
||||
|
||||
func loadTree(t testing.TB, repo restic.Repository, id restic.ID) *restic.Tree {
|
||||
tree, err := repo.LoadTree(context.TODO(), id)
|
||||
OK(t, err)
|
||||
return tree
|
||||
}
|
||||
|
||||
func TestFuseFile(t *testing.T) {
|
||||
repo, cleanup := repository.TestRepository(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
timestamp, err := time.Parse(time.RFC3339, "2017-01-24T10:42:56+01:00")
|
||||
OK(t, err)
|
||||
restic.TestCreateSnapshot(t, repo, timestamp, 2, 0.1)
|
||||
|
||||
sn := loadFirstSnapshot(t, repo)
|
||||
tree := loadTree(t, repo, *sn.Tree)
|
||||
|
||||
var content restic.IDs
|
||||
for _, node := range tree.Nodes {
|
||||
content = append(content, node.Content...)
|
||||
}
|
||||
t.Logf("tree loaded, content: %v", content)
|
||||
|
||||
var (
|
||||
filesize uint64
|
||||
memfile []byte
|
||||
)
|
||||
for _, id := range content {
|
||||
size, err := repo.LookupBlobSize(id, restic.DataBlob)
|
||||
OK(t, err)
|
||||
filesize += uint64(size)
|
||||
|
||||
buf := restic.NewBlobBuffer(int(size))
|
||||
n, err := repo.LoadBlob(context.TODO(), restic.DataBlob, id, buf)
|
||||
OK(t, err)
|
||||
|
||||
if uint(n) != size {
|
||||
t.Fatalf("not enough bytes read for id %v: want %v, got %v", id.Str(), size, n)
|
||||
}
|
||||
|
||||
if uint(len(buf)) != size {
|
||||
t.Fatalf("buffer has wrong length for id %v: want %v, got %v", id.Str(), size, len(buf))
|
||||
}
|
||||
|
||||
memfile = append(memfile, buf...)
|
||||
}
|
||||
|
||||
t.Logf("filesize is %v, memfile has size %v", filesize, len(memfile))
|
||||
|
||||
node := &restic.Node{
|
||||
Name: "foo",
|
||||
Inode: 23,
|
||||
Mode: 0742,
|
||||
Size: filesize,
|
||||
Content: content,
|
||||
}
|
||||
root := &Root{
|
||||
blobSizeCache: NewBlobSizeCache(context.TODO(), repo.Index()),
|
||||
repo: repo,
|
||||
}
|
||||
|
||||
t.Logf("blob cache has %d entries", len(root.blobSizeCache.m))
|
||||
|
||||
inode := fs.GenerateDynamicInode(1, "foo")
|
||||
f, err := newFile(context.TODO(), root, inode, node)
|
||||
OK(t, err)
|
||||
|
||||
attr := fuse.Attr{}
|
||||
OK(t, f.Attr(ctx, &attr))
|
||||
|
||||
Equals(t, inode, attr.Inode)
|
||||
Equals(t, node.Mode, attr.Mode)
|
||||
Equals(t, node.Size, attr.Size)
|
||||
Equals(t, (node.Size/uint64(attr.BlockSize))+1, attr.Blocks)
|
||||
|
||||
for i := 0; i < 200; i++ {
|
||||
offset := rand.Intn(int(filesize))
|
||||
length := rand.Intn(int(filesize)-offset) + 100
|
||||
|
||||
b := memfile[offset : offset+length]
|
||||
|
||||
buf := make([]byte, length)
|
||||
|
||||
testRead(t, f, offset, length, buf)
|
||||
if !bytes.Equal(b, buf) {
|
||||
t.Errorf("test %d failed, wrong data returned (offset %v, length %v)", i, offset, length)
|
||||
}
|
||||
}
|
||||
|
||||
OK(t, f.Release(ctx, nil))
|
||||
}
|
46
internal/fuse/link.go
Normal file
46
internal/fuse/link.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// +build !openbsd
|
||||
// +build !windows
|
||||
|
||||
package fuse
|
||||
|
||||
import (
|
||||
"restic"
|
||||
|
||||
"bazil.org/fuse"
|
||||
"bazil.org/fuse/fs"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Statically ensure that *file implements the given interface
|
||||
var _ = fs.NodeReadlinker(&link{})
|
||||
|
||||
type link struct {
|
||||
root *Root
|
||||
node *restic.Node
|
||||
inode uint64
|
||||
}
|
||||
|
||||
func newLink(ctx context.Context, root *Root, inode uint64, node *restic.Node) (*link, error) {
|
||||
return &link{root: root, inode: inode, node: node}, nil
|
||||
}
|
||||
|
||||
func (l *link) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) (string, error) {
|
||||
return l.node.LinkTarget, nil
|
||||
}
|
||||
|
||||
func (l *link) Attr(ctx context.Context, a *fuse.Attr) error {
|
||||
a.Inode = l.inode
|
||||
a.Mode = l.node.Mode
|
||||
|
||||
if !l.root.cfg.OwnerIsRoot {
|
||||
a.Uid = l.node.UID
|
||||
a.Gid = l.node.GID
|
||||
}
|
||||
a.Atime = l.node.AccessTime
|
||||
a.Ctime = l.node.ChangeTime
|
||||
a.Mtime = l.node.ModTime
|
||||
|
||||
a.Nlink = uint32(l.node.Links)
|
||||
|
||||
return nil
|
||||
}
|
87
internal/fuse/meta_dir.go
Normal file
87
internal/fuse/meta_dir.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// +build !openbsd
|
||||
// +build !windows
|
||||
|
||||
package fuse
|
||||
|
||||
import (
|
||||
"os"
|
||||
"restic/debug"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"bazil.org/fuse"
|
||||
"bazil.org/fuse/fs"
|
||||
)
|
||||
|
||||
// ensure that *DirSnapshots implements these interfaces
|
||||
var _ = fs.HandleReadDirAller(&MetaDir{})
|
||||
var _ = fs.NodeStringLookuper(&MetaDir{})
|
||||
|
||||
// MetaDir is a fuse directory which contains other directories.
|
||||
type MetaDir struct {
|
||||
inode uint64
|
||||
root *Root
|
||||
entries map[string]fs.Node
|
||||
}
|
||||
|
||||
// NewMetaDir returns a new meta dir.
|
||||
func NewMetaDir(root *Root, inode uint64, entries map[string]fs.Node) *MetaDir {
|
||||
debug.Log("new meta dir with %d entries, inode %d", len(entries), inode)
|
||||
|
||||
return &MetaDir{
|
||||
root: root,
|
||||
inode: inode,
|
||||
entries: entries,
|
||||
}
|
||||
}
|
||||
|
||||
// Attr returns the attributes for the root node.
|
||||
func (d *MetaDir) 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 *MetaDir) 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.entries {
|
||||
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 *MetaDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
||||
debug.Log("Lookup(%s)", name)
|
||||
|
||||
if dir, ok := d.entries[name]; ok {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
return nil, fuse.ENOENT
|
||||
}
|
110
internal/fuse/root.go
Normal file
110
internal/fuse/root.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// +build !openbsd
|
||||
// +build !windows
|
||||
|
||||
package fuse
|
||||
|
||||
import (
|
||||
"restic"
|
||||
"restic/debug"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"bazil.org/fuse/fs"
|
||||
)
|
||||
|
||||
// Config holds settings for the fuse mount.
|
||||
type Config struct {
|
||||
OwnerIsRoot bool
|
||||
Host string
|
||||
Tags []restic.TagList
|
||||
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
|
||||
blobSizeCache *BlobSizeCache
|
||||
|
||||
*MetaDir
|
||||
}
|
||||
|
||||
// ensure that *Root implements these interfaces
|
||||
var _ = fs.HandleReadDirAller(&Root{})
|
||||
var _ = fs.NodeStringLookuper(&Root{})
|
||||
|
||||
const rootInode = 1
|
||||
|
||||
// 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,
|
||||
inode: rootInode,
|
||||
cfg: cfg,
|
||||
snapshots: snapshots,
|
||||
blobSizeCache: NewBlobSizeCache(ctx, repo.Index()),
|
||||
}
|
||||
|
||||
entries := map[string]fs.Node{
|
||||
"snapshots": NewSnapshotsDir(root, fs.GenerateDynamicInode(root.inode, "snapshots"), snapshots),
|
||||
"tags": NewTagsDir(root, fs.GenerateDynamicInode(root.inode, "tags"), snapshots),
|
||||
"hosts": NewHostsDir(root, fs.GenerateDynamicInode(root.inode, "hosts"), snapshots),
|
||||
}
|
||||
|
||||
root.MetaDir = NewMetaDir(root, rootInode, entries)
|
||||
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// NewTagsDir returns a new directory containing entries, which in turn contains
|
||||
// snapshots with this tag set.
|
||||
func NewTagsDir(root *Root, inode uint64, snapshots restic.Snapshots) fs.Node {
|
||||
tags := make(map[string]restic.Snapshots)
|
||||
for _, sn := range snapshots {
|
||||
for _, tag := range sn.Tags {
|
||||
tags[tag] = append(tags[tag], sn)
|
||||
}
|
||||
}
|
||||
|
||||
debug.Log("create tags dir with %d tags, inode %d", len(tags), inode)
|
||||
|
||||
entries := make(map[string]fs.Node)
|
||||
for name, snapshots := range tags {
|
||||
debug.Log(" tag %v has %v snapshots", name, len(snapshots))
|
||||
entries[name] = NewSnapshotsDir(root, fs.GenerateDynamicInode(inode, name), snapshots)
|
||||
}
|
||||
|
||||
return NewMetaDir(root, inode, entries)
|
||||
}
|
||||
|
||||
// NewHostsDir returns a new directory containing hostnames, which in
|
||||
// turn contains snapshots of a single host each.
|
||||
func NewHostsDir(root *Root, inode uint64, snapshots restic.Snapshots) fs.Node {
|
||||
hosts := make(map[string]restic.Snapshots)
|
||||
for _, sn := range snapshots {
|
||||
hosts[sn.Hostname] = append(hosts[sn.Hostname], sn)
|
||||
}
|
||||
|
||||
debug.Log("create hosts dir with %d snapshots, inode %d", len(hosts), inode)
|
||||
|
||||
entries := make(map[string]fs.Node)
|
||||
for name, snapshots := range hosts {
|
||||
debug.Log(" host %v has %v snapshots", name, len(snapshots))
|
||||
entries[name] = NewSnapshotsDir(root, fs.GenerateDynamicInode(inode, name), snapshots)
|
||||
}
|
||||
|
||||
return NewMetaDir(root, inode, entries)
|
||||
}
|
||||
|
||||
// Root is just there to satisfy fs.Root, it returns itself.
|
||||
func (r *Root) Root() (fs.Node, error) {
|
||||
debug.Log("Root()")
|
||||
return r, nil
|
||||
}
|
108
internal/fuse/snapshots_dir.go
Normal file
108
internal/fuse/snapshots_dir.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// +build !openbsd
|
||||
// +build !windows
|
||||
|
||||
package fuse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"restic"
|
||||
"restic/debug"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"bazil.org/fuse"
|
||||
"bazil.org/fuse/fs"
|
||||
)
|
||||
|
||||
// SnapshotsDir is a fuse directory which contains snapshots.
|
||||
type SnapshotsDir struct {
|
||||
inode uint64
|
||||
root *Root
|
||||
snapshots restic.Snapshots
|
||||
names map[string]*restic.Snapshot
|
||||
}
|
||||
|
||||
// ensure that *DirSnapshots implements these interfaces
|
||||
var _ = fs.HandleReadDirAller(&SnapshotsDir{})
|
||||
var _ = fs.NodeStringLookuper(&SnapshotsDir{})
|
||||
|
||||
// NewSnapshotsDir returns a new directory containing snapshots.
|
||||
func NewSnapshotsDir(root *Root, inode uint64, snapshots restic.Snapshots) *SnapshotsDir {
|
||||
debug.Log("create snapshots dir with %d snapshots, inode %d", len(snapshots), inode)
|
||||
d := &SnapshotsDir{
|
||||
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 *SnapshotsDir) 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 *SnapshotsDir) 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 *SnapshotsDir) 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, fs.GenerateDynamicInode(d.inode, name), sn)
|
||||
}
|
Reference in New Issue
Block a user