mirror of
https://github.com/octoleo/restic.git
synced 2024-12-22 10:58:55 +00:00
Generalize fuse snapshot dirs implemetation
+ allow "/" in tags and snapshot template
This commit is contained in:
parent
696c18e031
commit
57f4003f2f
@ -5,7 +5,6 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@ -86,9 +85,6 @@ func runMount(opts MountOptions, gopts GlobalOptions, args []string) error {
|
||||
if opts.SnapshotTemplate == "" {
|
||||
return errors.Fatal("snapshot template string cannot be empty")
|
||||
}
|
||||
if strings.ContainsAny(opts.SnapshotTemplate, `\/`) {
|
||||
return errors.Fatal("snapshot template string contains a slash (/) or backslash (\\) character")
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return errors.Fatal("wrong number of parameters")
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ package fuse
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/bloblru"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
@ -28,13 +27,9 @@ type Root struct {
|
||||
repo restic.Repository
|
||||
cfg Config
|
||||
inode uint64
|
||||
snapshots restic.Snapshots
|
||||
blobCache *bloblru.Cache
|
||||
|
||||
snCount int
|
||||
lastCheck time.Time
|
||||
|
||||
*MetaDir
|
||||
*SnapshotsDir
|
||||
|
||||
uid, gid uint32
|
||||
}
|
||||
@ -64,14 +59,14 @@ func NewRoot(repo restic.Repository, cfg Config) *Root {
|
||||
root.gid = uint32(os.Getgid())
|
||||
}
|
||||
|
||||
entries := map[string]fs.Node{
|
||||
"snapshots": NewSnapshotsDir(root, fs.GenerateDynamicInode(root.inode, "snapshots"), "", ""),
|
||||
"tags": NewTagsDir(root, fs.GenerateDynamicInode(root.inode, "tags")),
|
||||
"hosts": NewHostsDir(root, fs.GenerateDynamicInode(root.inode, "hosts")),
|
||||
"ids": NewSnapshotsIDSDir(root, fs.GenerateDynamicInode(root.inode, "ids")),
|
||||
paths := []string{
|
||||
"ids/%i",
|
||||
"snapshots/%T",
|
||||
"hosts/%h/%T",
|
||||
"tags/%t/%T",
|
||||
}
|
||||
|
||||
root.MetaDir = NewMetaDir(root, rootInode, entries)
|
||||
root.SnapshotsDir = NewSnapshotsDir(root, rootInode, NewSnapshotsDirStructure(root, paths, cfg.SnapshotTemplate), "")
|
||||
|
||||
return root
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
202
internal/fuse/snapshots_dirstruct.go
Normal file
202
internal/fuse/snapshots_dirstruct.go
Normal file
@ -0,0 +1,202 @@
|
||||
//go:build darwin || freebsd || linux
|
||||
// +build darwin freebsd linux
|
||||
|
||||
package fuse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
// SnapshotsDirStructure contains the directory structure for snapshots.
|
||||
// It uses a paths and time template to generate a map of pathnames
|
||||
// pointing to the actual snapshots. For templates that end with a time,
|
||||
// also "latest" links are generated.
|
||||
type SnapshotsDirStructure struct {
|
||||
root *Root
|
||||
pathTemplates []string
|
||||
timeTemplate string
|
||||
|
||||
names map[string]*restic.Snapshot
|
||||
latest map[string]string
|
||||
snCount int
|
||||
lastCheck time.Time
|
||||
}
|
||||
|
||||
// NewSnapshotsDirStructure returns a new directory structure for snapshots.
|
||||
func NewSnapshotsDirStructure(root *Root, pathTemplates []string, timeTemplate string) *SnapshotsDirStructure {
|
||||
return &SnapshotsDirStructure{
|
||||
root: root,
|
||||
pathTemplates: pathTemplates,
|
||||
timeTemplate: timeTemplate,
|
||||
snCount: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// uniqueName returns a unique name to be used for prefix+name.
|
||||
// It appends -number to make the name unique.
|
||||
func (d *SnapshotsDirStructure) uniqueName(prefix, name string) (newname string) {
|
||||
newname = name
|
||||
for i := 1; ; i++ {
|
||||
if _, ok := d.names[prefix+newname]; !ok {
|
||||
break
|
||||
}
|
||||
newname = fmt.Sprintf("%s-%d", name, i)
|
||||
}
|
||||
return newname
|
||||
}
|
||||
|
||||
// pathsFromSn generates the paths from pathTemplate and timeTemplate
|
||||
// where the variables are replaced by the snapshot data.
|
||||
// The time is given as suffix if the pathTemplate ends with "%T".
|
||||
func pathsFromSn(pathTemplate string, timeTemplate string, sn *restic.Snapshot) (paths []string, timeSuffix string) {
|
||||
timeformat := sn.Time.Format(timeTemplate)
|
||||
|
||||
inVerb := false
|
||||
writeTime := false
|
||||
out := make([]strings.Builder, 1)
|
||||
for _, c := range pathTemplate {
|
||||
if writeTime {
|
||||
for i := range out {
|
||||
out[i].WriteString(timeformat)
|
||||
}
|
||||
writeTime = false
|
||||
}
|
||||
|
||||
if !inVerb {
|
||||
if c == '%' {
|
||||
inVerb = true
|
||||
} else {
|
||||
for i := range out {
|
||||
out[i].WriteRune(c)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var repl string
|
||||
inVerb = false
|
||||
switch c {
|
||||
case 'T':
|
||||
// lazy write; time might be returned as suffix
|
||||
writeTime = true
|
||||
continue
|
||||
|
||||
case 't':
|
||||
if len(sn.Tags) != 1 {
|
||||
// needs special treatment: Rebuild the string builders
|
||||
newout := make([]strings.Builder, len(out)*len(sn.Tags))
|
||||
for i, tag := range sn.Tags {
|
||||
for j := range out {
|
||||
newout[i*len(out)+j].WriteString(out[j].String() + tag)
|
||||
}
|
||||
}
|
||||
out = newout
|
||||
continue
|
||||
}
|
||||
repl = sn.Tags[0]
|
||||
|
||||
case 'i':
|
||||
repl = sn.ID().Str()
|
||||
|
||||
case 'I':
|
||||
repl = sn.ID().String()
|
||||
|
||||
case 'u':
|
||||
repl = sn.Username
|
||||
|
||||
case 'h':
|
||||
repl = sn.Hostname
|
||||
}
|
||||
|
||||
// write replacement string to all string builders
|
||||
for i := range out {
|
||||
out[i].WriteString(repl)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range out {
|
||||
paths = append(paths, out[i].String())
|
||||
}
|
||||
|
||||
if writeTime {
|
||||
timeSuffix = timeformat
|
||||
}
|
||||
|
||||
return paths, timeSuffix
|
||||
}
|
||||
|
||||
// makeDirs inserts all paths generated from pathTemplates and
|
||||
// TimeTemplate for all given snapshots into d.names.
|
||||
// Also adds d.latest links if "%T" is at end of a path template
|
||||
func (d *SnapshotsDirStructure) makeDirs(snapshots restic.Snapshots) {
|
||||
d.names = make(map[string]*restic.Snapshot)
|
||||
d.latest = make(map[string]string)
|
||||
|
||||
// insert pure directories; needed to get empty structure even if there
|
||||
// are no snapshots in these dirs
|
||||
for _, p := range d.pathTemplates {
|
||||
for _, pattern := range []string{"%i", "%I", "%u", "%h", "%t", "%T"} {
|
||||
p = strings.ReplaceAll(p, pattern, "")
|
||||
}
|
||||
d.names[path.Clean(p)+"/"] = nil
|
||||
}
|
||||
|
||||
latestTime := make(map[string]time.Time)
|
||||
for _, sn := range snapshots {
|
||||
for _, templ := range d.pathTemplates {
|
||||
paths, timeSuffix := pathsFromSn(templ, d.timeTemplate, sn)
|
||||
for _, p := range paths {
|
||||
suffix := d.uniqueName(p, timeSuffix)
|
||||
d.names[path.Clean(p+suffix)] = sn
|
||||
if timeSuffix != "" {
|
||||
lt, ok := latestTime[p]
|
||||
if !ok || !sn.Time.Before(lt) {
|
||||
debug.Log("link (update) %v -> %v\n", p, suffix)
|
||||
d.latest[p] = suffix
|
||||
latestTime[p] = sn.Time
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const minSnapshotsReloadTime = 60 * time.Second
|
||||
|
||||
// update snapshots if repository has changed
|
||||
func (d *SnapshotsDirStructure) updateSnapshots(ctx context.Context) error {
|
||||
if time.Since(d.lastCheck) < minSnapshotsReloadTime {
|
||||
return nil
|
||||
}
|
||||
|
||||
snapshots, err := restic.FindFilteredSnapshots(ctx, d.root.repo.Backend(), d.root.repo, d.root.cfg.Hosts, d.root.cfg.Tags, d.root.cfg.Paths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// sort snapshots ascending by time (default order is descending)
|
||||
sort.Sort(sort.Reverse(snapshots))
|
||||
|
||||
d.lastCheck = time.Now()
|
||||
|
||||
if d.snCount == len(snapshots) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = d.root.repo.LoadIndex(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.snCount = len(snapshots)
|
||||
|
||||
d.makeDirs(snapshots)
|
||||
return nil
|
||||
}
|
157
internal/fuse/snapshots_dirstruct_test.go
Normal file
157
internal/fuse/snapshots_dirstruct_test.go
Normal file
@ -0,0 +1,157 @@
|
||||
//go:build darwin || freebsd || linux
|
||||
// +build darwin freebsd linux
|
||||
|
||||
package fuse
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
func TestPathsFromSn(t *testing.T) {
|
||||
id1, _ := restic.ParseID("1234567812345678123456781234567812345678123456781234567812345678")
|
||||
time1, _ := time.Parse("2006-01-02T15:04:05", "2021-01-01T00:00:01")
|
||||
sn1 := &restic.Snapshot{Hostname: "host", Username: "user", Tags: []string{"tag1", "tag2"}, Time: time1}
|
||||
sn1.SetID(id1)
|
||||
|
||||
var p []string
|
||||
var s string
|
||||
|
||||
p, s = pathsFromSn("ids/%i", "2006-01-02T15:04:05", sn1)
|
||||
test.Equals(t, []string{"ids/12345678"}, p)
|
||||
test.Equals(t, "", s)
|
||||
|
||||
p, s = pathsFromSn("snapshots/%T", "2006-01-02T15:04:05", sn1)
|
||||
test.Equals(t, []string{"snapshots/"}, p)
|
||||
test.Equals(t, "2021-01-01T00:00:01", s)
|
||||
|
||||
p, s = pathsFromSn("hosts/%h/%T", "2006-01-02T15:04:05", sn1)
|
||||
test.Equals(t, []string{"hosts/host/"}, p)
|
||||
test.Equals(t, "2021-01-01T00:00:01", s)
|
||||
|
||||
p, s = pathsFromSn("tags/%t/%T", "2006-01-02T15:04:05", sn1)
|
||||
test.Equals(t, []string{"tags/tag1/", "tags/tag2/"}, p)
|
||||
test.Equals(t, "2021-01-01T00:00:01", s)
|
||||
|
||||
p, s = pathsFromSn("users/%u/%T", "2006-01-02T15:04:05", sn1)
|
||||
test.Equals(t, []string{"users/user/"}, p)
|
||||
test.Equals(t, "2021-01-01T00:00:01", s)
|
||||
|
||||
p, s = pathsFromSn("longids/%I", "2006-01-02T15:04:05", sn1)
|
||||
test.Equals(t, []string{"longids/1234567812345678123456781234567812345678123456781234567812345678"}, p)
|
||||
test.Equals(t, "", s)
|
||||
|
||||
p, s = pathsFromSn("%T/%h", "2006/01/02", sn1)
|
||||
test.Equals(t, []string{"2021/01/01/host"}, p)
|
||||
test.Equals(t, "", s)
|
||||
|
||||
p, s = pathsFromSn("%T/%i", "2006/01", sn1)
|
||||
test.Equals(t, []string{"2021/01/12345678"}, p)
|
||||
test.Equals(t, "", s)
|
||||
}
|
||||
|
||||
func TestMakeDirs(t *testing.T) {
|
||||
pathTemplates := []string{"ids/%i", "snapshots/%T", "hosts/%h/%T",
|
||||
"tags/%t/%T", "users/%u/%T", "longids/%I", "%T/%h", "%T/%i",
|
||||
}
|
||||
timeTemplate := "2006/01/02"
|
||||
|
||||
sds := &SnapshotsDirStructure{
|
||||
pathTemplates: pathTemplates,
|
||||
timeTemplate: timeTemplate,
|
||||
}
|
||||
|
||||
id0, _ := restic.ParseID("0000000012345678123456781234567812345678123456781234567812345678")
|
||||
time0, _ := time.Parse("2006-01-02T15:04:05", "2020-12-31T00:00:01")
|
||||
sn0 := &restic.Snapshot{Hostname: "host", Username: "user", Tags: []string{"tag1", "tag2"}, Time: time0}
|
||||
sn0.SetID(id0)
|
||||
|
||||
id1, _ := restic.ParseID("1234567812345678123456781234567812345678123456781234567812345678")
|
||||
time1, _ := time.Parse("2006-01-02T15:04:05", "2021-01-01T00:00:01")
|
||||
sn1 := &restic.Snapshot{Hostname: "host", Username: "user", Tags: []string{"tag1", "tag2"}, Time: time1}
|
||||
sn1.SetID(id1)
|
||||
|
||||
id2, _ := restic.ParseID("8765432112345678123456781234567812345678123456781234567812345678")
|
||||
time2, _ := time.Parse("2006-01-02T15:04:05", "2021-01-01T01:02:03")
|
||||
sn2 := &restic.Snapshot{Hostname: "host2", Username: "user2", Tags: []string{"tag2", "tag3", "tag4"}, Time: time2}
|
||||
sn2.SetID(id2)
|
||||
|
||||
id3, _ := restic.ParseID("aaaaaaaa12345678123456781234567812345678123456781234567812345678")
|
||||
time3, _ := time.Parse("2006-01-02T15:04:05", "2021-01-01T01:02:03")
|
||||
sn3 := &restic.Snapshot{Hostname: "host", Username: "user2", Tags: []string{}, Time: time3}
|
||||
sn3.SetID(id3)
|
||||
|
||||
sds.makeDirs(restic.Snapshots{sn0, sn1, sn2, sn3})
|
||||
|
||||
expNames := make(map[string]*restic.Snapshot)
|
||||
expLatest := make(map[string]string)
|
||||
|
||||
// empty entries for dir structure
|
||||
expNames["ids/"] = nil
|
||||
expNames["snapshots/"] = nil
|
||||
expNames["hosts/"] = nil
|
||||
expNames["tags/"] = nil
|
||||
expNames["users/"] = nil
|
||||
expNames["longids/"] = nil
|
||||
expNames["//"] = nil
|
||||
|
||||
// entries for sn0
|
||||
expNames["ids/00000000"] = sn0
|
||||
expNames["snapshots/2020/12/31"] = sn0
|
||||
expNames["hosts/host/2020/12/31"] = sn0
|
||||
expNames["tags/tag1/2020/12/31"] = sn0
|
||||
expNames["tags/tag2/2020/12/31"] = sn0
|
||||
expNames["users/user/2020/12/31"] = sn0
|
||||
expNames["longids/0000000012345678123456781234567812345678123456781234567812345678"] = sn0
|
||||
expNames["2020/12/31/host"] = sn0
|
||||
expNames["2020/12/31/00000000"] = sn0
|
||||
|
||||
// entries for sn1
|
||||
expNames["ids/12345678"] = sn1
|
||||
expNames["snapshots/2021/01/01"] = sn1
|
||||
expNames["hosts/host/2021/01/01"] = sn1
|
||||
expNames["tags/tag1/2021/01/01"] = sn1
|
||||
expNames["tags/tag2/2021/01/01"] = sn1
|
||||
expNames["users/user/2021/01/01"] = sn1
|
||||
expNames["longids/1234567812345678123456781234567812345678123456781234567812345678"] = sn1
|
||||
expNames["2021/01/01/host"] = sn1
|
||||
expNames["2021/01/01/12345678"] = sn1
|
||||
|
||||
// entries for sn2
|
||||
expNames["ids/87654321"] = sn2
|
||||
expNames["snapshots/2021/01/01-1"] = sn2 // sn1 and sn2 have same time string
|
||||
expNames["hosts/host2/2021/01/01"] = sn2
|
||||
expNames["tags/tag2/2021/01/01-1"] = sn2 // sn1 and sn2 have same time string
|
||||
expNames["tags/tag3/2021/01/01"] = sn2
|
||||
expNames["tags/tag4/2021/01/01"] = sn2
|
||||
expNames["users/user2/2021/01/01"] = sn2
|
||||
expNames["longids/8765432112345678123456781234567812345678123456781234567812345678"] = sn2
|
||||
expNames["2021/01/01/host2"] = sn2
|
||||
expNames["2021/01/01/87654321"] = sn2
|
||||
|
||||
// entries for sn3
|
||||
expNames["ids/aaaaaaaa"] = sn3
|
||||
expNames["snapshots/2021/01/01-2"] = sn3 // sn1 - sn3 have same time string
|
||||
expNames["hosts/host/2021/01/01-1"] = sn3 // sn1 and sn3 have same time string
|
||||
expNames["users/user2/2021/01/01-1"] = sn3 // sn2 and sn3 have same time string
|
||||
expNames["longids/aaaaaaaa12345678123456781234567812345678123456781234567812345678"] = sn3
|
||||
expNames["2021/01/01/host-1"] = sn3 // sn1 and sn3 have same time string and identical host
|
||||
expNames["2021/01/01/aaaaaaaa"] = sn3
|
||||
|
||||
// latest links
|
||||
expLatest["snapshots/"] = "2021/01/01-2" // sn1 - sn3 have same time string
|
||||
expLatest["hosts/host/"] = "2021/01/01-1"
|
||||
expLatest["hosts/host2/"] = "2021/01/01"
|
||||
expLatest["tags/tag1/"] = "2021/01/01"
|
||||
expLatest["tags/tag2/"] = "2021/01/01-1" // sn1 and sn2 have same time string
|
||||
expLatest["tags/tag3/"] = "2021/01/01"
|
||||
expLatest["tags/tag4/"] = "2021/01/01"
|
||||
expLatest["users/user/"] = "2021/01/01"
|
||||
expLatest["users/user2/"] = "2021/01/01-1" // sn2 and sn3 have same time string
|
||||
|
||||
test.Equals(t, expNames, sds.names)
|
||||
test.Equals(t, expLatest, sds.latest)
|
||||
}
|
Loading…
Reference in New Issue
Block a user