diff --git a/cmd/restic/cmd_mount.go b/cmd/restic/cmd_mount.go index 905cee1d3..b5112f181 100644 --- a/cmd/restic/cmd_mount.go +++ b/cmd/restic/cmd_mount.go @@ -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") } diff --git a/internal/fuse/root.go b/internal/fuse/root.go index bed760f02..8165ddafa 100644 --- a/internal/fuse/root.go +++ b/internal/fuse/root.go @@ -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 } diff --git a/internal/fuse/snapshots_dir.go b/internal/fuse/snapshots_dir.go index 88638d50c..2924919da 100644 --- a/internal/fuse/snapshots_dir.go +++ b/internal/fuse/snapshots_dir.go @@ -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 -} diff --git a/internal/fuse/snapshots_dirstruct.go b/internal/fuse/snapshots_dirstruct.go new file mode 100644 index 000000000..9bf2f5a7e --- /dev/null +++ b/internal/fuse/snapshots_dirstruct.go @@ -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 +} diff --git a/internal/fuse/snapshots_dirstruct_test.go b/internal/fuse/snapshots_dirstruct_test.go new file mode 100644 index 000000000..5b0d88c1b --- /dev/null +++ b/internal/fuse/snapshots_dirstruct_test.go @@ -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) +}