restic/internal/fuse/dir.go
Michael Eischer 0e9716a6e6 fuse: forget fs.Node instances on request by the kernel
Forget fs.Node instances once the kernel frees the corresponding nodeId.
This ensures that restic does not run out of memory on large snapshots.
2024-11-03 21:42:19 +01:00

254 lines
5.7 KiB
Go

//go:build darwin || freebsd || linux
// +build darwin freebsd linux
package fuse
import (
"context"
"errors"
"os"
"path/filepath"
"sync"
"syscall"
"github.com/anacrolix/fuse"
"github.com/anacrolix/fuse/fs"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/restic"
)
// Statically ensure that *dir implement those interface
var _ = fs.HandleReadDirAller(&dir{})
var _ = fs.NodeForgetter(&dir{})
var _ = fs.NodeGetxattrer(&dir{})
var _ = fs.NodeListxattrer(&dir{})
var _ = fs.NodeStringLookuper(&dir{})
type dir struct {
root *Root
forget forgetFn
items map[string]*restic.Node
inode uint64
parentInode uint64
node *restic.Node
m sync.Mutex
cache treeCache
}
func cleanupNodeName(name string) string {
return filepath.Base(name)
}
func newDir(root *Root, forget forgetFn, inode, parentInode uint64, node *restic.Node) (*dir, error) {
debug.Log("new dir for %v (%v)", node.Name, node.Subtree)
return &dir{
root: root,
forget: forget,
node: node,
inode: inode,
parentInode: parentInode,
cache: *newTreeCache(),
}, nil
}
// returning a wrapped context.Canceled error will instead result in returning
// an input / output error to the user. Thus unwrap the error to match the
// expectations of bazil/fuse
func unwrapCtxCanceled(err error) error {
if errors.Is(err, context.Canceled) {
return context.Canceled
}
return err
}
// replaceSpecialNodes replaces nodes with name "." and "/" by their contents.
// Otherwise, the node is returned.
func replaceSpecialNodes(ctx context.Context, repo restic.BlobLoader, 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 := restic.LoadTree(ctx, repo, *node.Subtree)
if err != nil {
return nil, unwrapCtxCanceled(err)
}
return tree.Nodes, nil
}
func newDirFromSnapshot(root *Root, forget forgetFn, inode uint64, snapshot *restic.Snapshot) (*dir, error) {
debug.Log("new dir for snapshot %v (%v)", snapshot.ID(), snapshot.Tree)
return &dir{
root: root,
forget: forget,
node: &restic.Node{
AccessTime: snapshot.Time,
ModTime: snapshot.Time,
ChangeTime: snapshot.Time,
Mode: os.ModeDir | 0555,
Subtree: snapshot.Tree,
},
inode: inode,
cache: *newTreeCache(),
}, nil
}
func (d *dir) open(ctx context.Context) error {
d.m.Lock()
defer d.m.Unlock()
if d.items != nil {
return nil
}
debug.Log("open dir %v (%v)", d.node.Name, d.node.Subtree)
tree, err := restic.LoadTree(ctx, d.root.repo, *d.node.Subtree)
if err != nil {
debug.Log(" error loading tree %v: %v", d.node.Subtree, err)
return unwrapCtxCanceled(err)
}
items := make(map[string]*restic.Node)
for _, n := range tree.Nodes {
if ctx.Err() != nil {
return ctx.Err()
}
nodes, err := replaceSpecialNodes(ctx, d.root.repo, n)
if err != nil {
debug.Log(" replaceSpecialNodes(%v) failed: %v", n, err)
return err
}
for _, node := range nodes {
items[cleanupNodeName(node.Name)] = node
}
}
d.items = items
return nil
}
func (d *dir) Attr(_ context.Context, a *fuse.Attr) error {
debug.Log("Attr()")
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
count := uint32(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("ReadDirAll()")
err := d.open(ctx)
if err != nil {
return nil, err
}
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 {
if ctx.Err() != nil {
return nil, ctx.Err()
}
name := cleanupNodeName(node.Name)
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: inodeFromNode(d.inode, node),
Type: typ,
Name: name,
})
}
return ret, nil
}
func (d *dir) Lookup(ctx context.Context, name string) (fs.Node, error) {
debug.Log("Lookup(%v)", name)
err := d.open(ctx)
if err != nil {
return nil, err
}
return d.cache.lookupOrCreate(name, func(forget forgetFn) (fs.Node, error) {
node, ok := d.items[name]
if !ok {
debug.Log(" Lookup(%v) -> not found", name)
return nil, syscall.ENOENT
}
inode := inodeFromNode(d.inode, node)
switch node.Type {
case "dir":
return newDir(d.root, forget, inode, d.inode, node)
case "file":
return newFile(d.root, forget, inode, node)
case "symlink":
return newLink(d.root, forget, inode, node)
case "dev", "chardev", "fifo", "socket":
return newOther(d.root, forget, inode, node)
default:
debug.Log(" node %v has unknown type %v", name, node.Type)
return nil, syscall.ENOENT
}
})
}
func (d *dir) Listxattr(_ context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error {
nodeToXattrList(d.node, req, resp)
return nil
}
func (d *dir) Getxattr(_ context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
return nodeGetXattr(d.node, req, resp)
}
func (d *dir) Forget() {
d.forget()
}