mirror of
https://github.com/restic/restic.git
synced 2025-12-04 01:11:56 +00:00
Generalize fuse snapshot dirs implemetation
+ allow "/" in tags and snapshot template
This commit is contained in:
committed by
Michael Eischer
parent
696c18e031
commit
57f4003f2f
@@ -5,9 +5,8 @@ package fuse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
@@ -16,152 +15,32 @@ import (
|
||||
"bazil.org/fuse/fs"
|
||||
)
|
||||
|
||||
// SnapshotsDir is a fuse directory which contains snapshots named by timestamp.
|
||||
// SnapshotsDir is a actual fuse directory generated from SnapshotsDirStructure
|
||||
// It uses the saved prefix to filter out the relevant subtrees or entries
|
||||
// from SnapshotsDirStructure.names and .latest, respectively.
|
||||
type SnapshotsDir struct {
|
||||
inode uint64
|
||||
root *Root
|
||||
names map[string]*restic.Snapshot
|
||||
latest string
|
||||
tag string
|
||||
host string
|
||||
snCount int
|
||||
|
||||
template string
|
||||
}
|
||||
|
||||
// SnapshotsIDSDir is a fuse directory which contains snapshots named by ids.
|
||||
type SnapshotsIDSDir struct {
|
||||
inode uint64
|
||||
root *Root
|
||||
names map[string]*restic.Snapshot
|
||||
snCount int
|
||||
}
|
||||
|
||||
// HostsDir is a fuse directory which contains hosts.
|
||||
type HostsDir struct {
|
||||
inode uint64
|
||||
root *Root
|
||||
hosts map[string]bool
|
||||
snCount int
|
||||
}
|
||||
|
||||
// TagsDir is a fuse directory which contains tags.
|
||||
type TagsDir struct {
|
||||
inode uint64
|
||||
root *Root
|
||||
tags map[string]bool
|
||||
snCount int
|
||||
}
|
||||
|
||||
// SnapshotLink
|
||||
type snapshotLink struct {
|
||||
root *Root
|
||||
inode uint64
|
||||
target string
|
||||
snapshot *restic.Snapshot
|
||||
root *Root
|
||||
inode uint64
|
||||
dirStruct *SnapshotsDirStructure
|
||||
prefix string
|
||||
}
|
||||
|
||||
// ensure that *SnapshotsDir implements these interfaces
|
||||
var _ = fs.HandleReadDirAller(&SnapshotsDir{})
|
||||
var _ = fs.NodeStringLookuper(&SnapshotsDir{})
|
||||
var _ = fs.HandleReadDirAller(&SnapshotsIDSDir{})
|
||||
var _ = fs.NodeStringLookuper(&SnapshotsIDSDir{})
|
||||
var _ = fs.HandleReadDirAller(&TagsDir{})
|
||||
var _ = fs.NodeStringLookuper(&TagsDir{})
|
||||
var _ = fs.HandleReadDirAller(&HostsDir{})
|
||||
var _ = fs.NodeStringLookuper(&HostsDir{})
|
||||
var _ = fs.NodeReadlinker(&snapshotLink{})
|
||||
|
||||
// read tag names from the current repository-state.
|
||||
func updateTagNames(d *TagsDir) {
|
||||
if d.snCount != d.root.snCount {
|
||||
d.snCount = d.root.snCount
|
||||
d.tags = make(map[string]bool, len(d.root.snapshots))
|
||||
for _, snapshot := range d.root.snapshots {
|
||||
for _, tag := range snapshot.Tags {
|
||||
if tag != "" {
|
||||
d.tags[tag] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read host names from the current repository-state.
|
||||
func updateHostsNames(d *HostsDir) {
|
||||
if d.snCount != d.root.snCount {
|
||||
d.snCount = d.root.snCount
|
||||
d.hosts = make(map[string]bool, len(d.root.snapshots))
|
||||
for _, snapshot := range d.root.snapshots {
|
||||
d.hosts[snapshot.Hostname] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read snapshot id names from the current repository-state.
|
||||
func updateSnapshotIDSNames(d *SnapshotsIDSDir) {
|
||||
if d.snCount != d.root.snCount {
|
||||
d.snCount = d.root.snCount
|
||||
for _, sn := range d.root.snapshots {
|
||||
name := sn.ID().Str()
|
||||
d.names[name] = sn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewSnapshotsDir returns a new directory containing snapshots.
|
||||
func NewSnapshotsDir(root *Root, inode uint64, tag string, host string) *SnapshotsDir {
|
||||
// NewSnapshotsDir returns a new directory structure containing snapshots and "latest" links
|
||||
func NewSnapshotsDir(root *Root, inode uint64, dirStruct *SnapshotsDirStructure, prefix string) *SnapshotsDir {
|
||||
debug.Log("create snapshots dir, inode %d", inode)
|
||||
d := &SnapshotsDir{
|
||||
root: root,
|
||||
inode: inode,
|
||||
names: make(map[string]*restic.Snapshot),
|
||||
latest: "",
|
||||
tag: tag,
|
||||
host: host,
|
||||
template: root.cfg.SnapshotTemplate,
|
||||
return &SnapshotsDir{
|
||||
root: root,
|
||||
inode: inode,
|
||||
dirStruct: dirStruct,
|
||||
prefix: prefix,
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// NewSnapshotsIDSDir returns a new directory containing snapshots named by ids.
|
||||
func NewSnapshotsIDSDir(root *Root, inode uint64) *SnapshotsIDSDir {
|
||||
debug.Log("create snapshots ids dir, inode %d", inode)
|
||||
d := &SnapshotsIDSDir{
|
||||
root: root,
|
||||
inode: inode,
|
||||
names: make(map[string]*restic.Snapshot),
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// NewHostsDir returns a new directory containing host names
|
||||
func NewHostsDir(root *Root, inode uint64) *HostsDir {
|
||||
debug.Log("create hosts dir, inode %d", inode)
|
||||
d := &HostsDir{
|
||||
root: root,
|
||||
inode: inode,
|
||||
hosts: make(map[string]bool),
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// NewTagsDir returns a new directory containing tag names
|
||||
func NewTagsDir(root *Root, inode uint64) *TagsDir {
|
||||
debug.Log("create tags dir, inode %d", inode)
|
||||
d := &TagsDir{
|
||||
root: root,
|
||||
inode: inode,
|
||||
tags: make(map[string]bool),
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// Attr returns the attributes for the root node.
|
||||
// Attr returns the attributes for any dir in the snapshots directory structure
|
||||
func (d *SnapshotsDir) Attr(ctx context.Context, attr *fuse.Attr) error {
|
||||
attr.Inode = d.inode
|
||||
attr.Mode = os.ModeDir | 0555
|
||||
@@ -172,118 +51,16 @@ func (d *SnapshotsDir) Attr(ctx context.Context, attr *fuse.Attr) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Attr returns the attributes for the SnapshotsDir.
|
||||
func (d *SnapshotsIDSDir) Attr(ctx context.Context, attr *fuse.Attr) error {
|
||||
attr.Inode = d.inode
|
||||
attr.Mode = os.ModeDir | 0555
|
||||
attr.Uid = d.root.uid
|
||||
attr.Gid = d.root.gid
|
||||
|
||||
debug.Log("attr: %v", attr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Attr returns the attributes for the HostsDir.
|
||||
func (d *HostsDir) Attr(ctx context.Context, attr *fuse.Attr) error {
|
||||
attr.Inode = d.inode
|
||||
attr.Mode = os.ModeDir | 0555
|
||||
attr.Uid = d.root.uid
|
||||
attr.Gid = d.root.gid
|
||||
|
||||
debug.Log("attr: %v", attr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Attr returns the attributes for the TagsDir.
|
||||
func (d *TagsDir) Attr(ctx context.Context, attr *fuse.Attr) error {
|
||||
attr.Inode = d.inode
|
||||
attr.Mode = os.ModeDir | 0555
|
||||
attr.Uid = d.root.uid
|
||||
attr.Gid = d.root.gid
|
||||
|
||||
debug.Log("attr: %v", attr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// search element in string list.
|
||||
func isElem(e string, list []string) bool {
|
||||
for _, x := range list {
|
||||
if e == x {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const minSnapshotsReloadTime = 60 * time.Second
|
||||
|
||||
// update snapshots if repository has changed
|
||||
func updateSnapshots(ctx context.Context, root *Root) error {
|
||||
if time.Since(root.lastCheck) < minSnapshotsReloadTime {
|
||||
return nil
|
||||
}
|
||||
|
||||
snapshots, err := restic.FindFilteredSnapshots(ctx, root.repo.Backend(), root.repo, root.cfg.Hosts, root.cfg.Tags, root.cfg.Paths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if root.snCount != len(snapshots) {
|
||||
root.snCount = len(snapshots)
|
||||
err := root.repo.LoadIndex(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root.snapshots = snapshots
|
||||
}
|
||||
root.lastCheck = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// read snapshot timestamps from the current repository-state.
|
||||
func updateSnapshotNames(d *SnapshotsDir, template string) {
|
||||
if d.snCount != d.root.snCount {
|
||||
d.snCount = d.root.snCount
|
||||
var latestTime time.Time
|
||||
d.latest = ""
|
||||
d.names = make(map[string]*restic.Snapshot, len(d.root.snapshots))
|
||||
for _, sn := range d.root.snapshots {
|
||||
if d.tag == "" || isElem(d.tag, sn.Tags) {
|
||||
if d.host == "" || d.host == sn.Hostname {
|
||||
name := sn.Time.Format(template)
|
||||
if d.latest == "" || !sn.Time.Before(latestTime) {
|
||||
latestTime = sn.Time
|
||||
d.latest = name
|
||||
}
|
||||
for i := 1; ; i++ {
|
||||
if _, ok := d.names[name]; !ok {
|
||||
break
|
||||
}
|
||||
|
||||
name = fmt.Sprintf("%s-%d", sn.Time.Format(template), i)
|
||||
}
|
||||
|
||||
d.names[name] = sn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReadDirAll returns all entries of the SnapshotsDir.
|
||||
func (d *SnapshotsDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
|
||||
debug.Log("ReadDirAll()")
|
||||
|
||||
// update snapshots
|
||||
err := updateSnapshots(ctx, d.root)
|
||||
err := d.dirStruct.updateSnapshots(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update snapshot names
|
||||
updateSnapshotNames(d, d.root.cfg.SnapshotTemplate)
|
||||
|
||||
items := []fuse.Dirent{
|
||||
{
|
||||
Inode: d.inode,
|
||||
@@ -297,135 +74,86 @@ func (d *SnapshotsDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
|
||||
},
|
||||
}
|
||||
|
||||
for name := range d.names {
|
||||
// map to ensure that all names are only listed once
|
||||
hasName := make(map[string]struct{})
|
||||
|
||||
for name := range d.dirStruct.names {
|
||||
if !strings.HasPrefix(name, d.prefix) {
|
||||
continue
|
||||
}
|
||||
shortname := strings.Split(name[len(d.prefix):], "/")[0]
|
||||
if shortname == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := hasName[shortname]; ok {
|
||||
continue
|
||||
}
|
||||
hasName[shortname] = struct{}{}
|
||||
items = append(items, fuse.Dirent{
|
||||
Inode: fs.GenerateDynamicInode(d.inode, name),
|
||||
Name: name,
|
||||
Inode: fs.GenerateDynamicInode(d.inode, shortname),
|
||||
Name: shortname,
|
||||
Type: fuse.DT_Dir,
|
||||
})
|
||||
}
|
||||
|
||||
// Latest
|
||||
if d.latest != "" {
|
||||
if _, ok := d.dirStruct.latest[d.prefix]; ok {
|
||||
items = append(items, fuse.Dirent{
|
||||
Inode: fs.GenerateDynamicInode(d.inode, "latest"),
|
||||
Name: "latest",
|
||||
Type: fuse.DT_Link,
|
||||
})
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// ReadDirAll returns all entries of the SnapshotsIDSDir.
|
||||
func (d *SnapshotsIDSDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
|
||||
debug.Log("ReadDirAll()")
|
||||
// Lookup returns a specific entry from the SnapshotsDir.
|
||||
func (d *SnapshotsDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
||||
debug.Log("Lookup(%s)", name)
|
||||
|
||||
// update snapshots
|
||||
err := updateSnapshots(ctx, d.root)
|
||||
err := d.dirStruct.updateSnapshots(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update snapshot ids
|
||||
updateSnapshotIDSNames(d)
|
||||
fullname := d.prefix + name
|
||||
|
||||
items := []fuse.Dirent{
|
||||
{
|
||||
Inode: d.inode,
|
||||
Name: ".",
|
||||
Type: fuse.DT_Dir,
|
||||
},
|
||||
{
|
||||
Inode: d.root.inode,
|
||||
Name: "..",
|
||||
Type: fuse.DT_Dir,
|
||||
},
|
||||
// check if this is already a complete snapshot path
|
||||
sn := d.dirStruct.names[fullname]
|
||||
if sn != nil {
|
||||
return newDirFromSnapshot(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), sn)
|
||||
}
|
||||
|
||||
for name := range d.names {
|
||||
items = append(items, fuse.Dirent{
|
||||
Inode: fs.GenerateDynamicInode(d.inode, name),
|
||||
Name: name,
|
||||
Type: fuse.DT_Dir,
|
||||
})
|
||||
// handle latest case
|
||||
if name == "latest" {
|
||||
link := d.dirStruct.latest[d.prefix]
|
||||
sn := d.dirStruct.names[d.prefix+link]
|
||||
if sn != nil {
|
||||
return newSnapshotLink(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), link, sn)
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
// check if this is a valid subdir
|
||||
fullname = fullname + "/"
|
||||
for name := range d.dirStruct.names {
|
||||
if strings.HasPrefix(name, fullname) {
|
||||
return NewSnapshotsDir(d.root, fs.GenerateDynamicInode(d.inode, name), d.dirStruct, fullname), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fuse.ENOENT
|
||||
}
|
||||
|
||||
// ReadDirAll returns all entries of the HostsDir.
|
||||
func (d *HostsDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
|
||||
debug.Log("ReadDirAll()")
|
||||
|
||||
// update snapshots
|
||||
err := updateSnapshots(ctx, d.root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update host names
|
||||
updateHostsNames(d)
|
||||
|
||||
items := []fuse.Dirent{
|
||||
{
|
||||
Inode: d.inode,
|
||||
Name: ".",
|
||||
Type: fuse.DT_Dir,
|
||||
},
|
||||
{
|
||||
Inode: d.root.inode,
|
||||
Name: "..",
|
||||
Type: fuse.DT_Dir,
|
||||
},
|
||||
}
|
||||
|
||||
for host := range d.hosts {
|
||||
items = append(items, fuse.Dirent{
|
||||
Inode: fs.GenerateDynamicInode(d.inode, host),
|
||||
Name: host,
|
||||
Type: fuse.DT_Dir,
|
||||
})
|
||||
}
|
||||
|
||||
return items, nil
|
||||
// SnapshotLink
|
||||
type snapshotLink struct {
|
||||
root *Root
|
||||
inode uint64
|
||||
target string
|
||||
snapshot *restic.Snapshot
|
||||
}
|
||||
|
||||
// ReadDirAll returns all entries of the TagsDir.
|
||||
func (d *TagsDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
|
||||
debug.Log("ReadDirAll()")
|
||||
|
||||
// update snapshots
|
||||
err := updateSnapshots(ctx, d.root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update tag names
|
||||
updateTagNames(d)
|
||||
|
||||
items := []fuse.Dirent{
|
||||
{
|
||||
Inode: d.inode,
|
||||
Name: ".",
|
||||
Type: fuse.DT_Dir,
|
||||
},
|
||||
{
|
||||
Inode: d.root.inode,
|
||||
Name: "..",
|
||||
Type: fuse.DT_Dir,
|
||||
},
|
||||
}
|
||||
|
||||
for tag := range d.tags {
|
||||
items = append(items, fuse.Dirent{
|
||||
Inode: fs.GenerateDynamicInode(d.inode, tag),
|
||||
Name: tag,
|
||||
Type: fuse.DT_Dir,
|
||||
})
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
var _ = fs.NodeReadlinker(&snapshotLink{})
|
||||
|
||||
// newSnapshotLink
|
||||
func newSnapshotLink(ctx context.Context, root *Root, inode uint64, target string, snapshot *restic.Snapshot) (*snapshotLink, error) {
|
||||
@@ -453,117 +181,3 @@ func (l *snapshotLink) Attr(ctx context.Context, a *fuse.Attr) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lookup returns a specific entry from the SnapshotsDir.
|
||||
func (d *SnapshotsDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
||||
debug.Log("Lookup(%s)", name)
|
||||
|
||||
sn, ok := d.names[name]
|
||||
if !ok {
|
||||
// could not find entry. Updating repository-state
|
||||
err := updateSnapshots(ctx, d.root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update snapshot names
|
||||
updateSnapshotNames(d, d.root.cfg.SnapshotTemplate)
|
||||
|
||||
sn, ok := d.names[name]
|
||||
if ok {
|
||||
return newDirFromSnapshot(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), sn)
|
||||
}
|
||||
|
||||
if name == "latest" && d.latest != "" {
|
||||
sn, ok := d.names[d.latest]
|
||||
|
||||
// internal error
|
||||
if !ok {
|
||||
return nil, fuse.ENOENT
|
||||
}
|
||||
|
||||
return newSnapshotLink(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), d.latest, sn)
|
||||
}
|
||||
return nil, fuse.ENOENT
|
||||
}
|
||||
|
||||
return newDirFromSnapshot(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), sn)
|
||||
}
|
||||
|
||||
// Lookup returns a specific entry from the SnapshotsIDSDir.
|
||||
func (d *SnapshotsIDSDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
||||
debug.Log("Lookup(%s)", name)
|
||||
|
||||
sn, ok := d.names[name]
|
||||
if !ok {
|
||||
// could not find entry. Updating repository-state
|
||||
err := updateSnapshots(ctx, d.root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update snapshot ids
|
||||
updateSnapshotIDSNames(d)
|
||||
|
||||
sn, ok := d.names[name]
|
||||
if ok {
|
||||
return newDirFromSnapshot(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), sn)
|
||||
}
|
||||
|
||||
return nil, fuse.ENOENT
|
||||
}
|
||||
|
||||
return newDirFromSnapshot(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), sn)
|
||||
}
|
||||
|
||||
// Lookup returns a specific entry from the HostsDir.
|
||||
func (d *HostsDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
||||
debug.Log("Lookup(%s)", name)
|
||||
|
||||
_, ok := d.hosts[name]
|
||||
if !ok {
|
||||
// could not find entry. Updating repository-state
|
||||
err := updateSnapshots(ctx, d.root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update host names
|
||||
updateHostsNames(d)
|
||||
|
||||
_, ok := d.hosts[name]
|
||||
if ok {
|
||||
return NewSnapshotsDir(d.root, fs.GenerateDynamicInode(d.root.inode, name), "", name), nil
|
||||
}
|
||||
|
||||
return nil, fuse.ENOENT
|
||||
}
|
||||
|
||||
return NewSnapshotsDir(d.root, fs.GenerateDynamicInode(d.root.inode, name), "", name), nil
|
||||
}
|
||||
|
||||
// Lookup returns a specific entry from the TagsDir.
|
||||
func (d *TagsDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
||||
debug.Log("Lookup(%s)", name)
|
||||
|
||||
_, ok := d.tags[name]
|
||||
if !ok {
|
||||
// could not find entry. Updating repository-state
|
||||
err := updateSnapshots(ctx, d.root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update tag names
|
||||
updateTagNames(d)
|
||||
|
||||
_, ok := d.tags[name]
|
||||
if ok {
|
||||
return NewSnapshotsDir(d.root, fs.GenerateDynamicInode(d.root.inode, name), name, ""), nil
|
||||
}
|
||||
|
||||
return nil, fuse.ENOENT
|
||||
}
|
||||
|
||||
return NewSnapshotsDir(d.root, fs.GenerateDynamicInode(d.root.inode, name), name, ""), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user