From 1a83a739dcfc2a6246ec445b9e749d5869c30353 Mon Sep 17 00:00:00 2001 From: Tobias Klein Date: Thu, 14 Sep 2017 19:44:03 +0200 Subject: [PATCH] fuse: added symlink 'latest' to snapshots-dir --- cmd/restic/integration_fuse_test.go | 26 +++++++++---- internal/fuse/snapshots_dir.go | 60 +++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/cmd/restic/integration_fuse_test.go b/cmd/restic/integration_fuse_test.go index 4d70212a5..0a3bde4dc 100644 --- a/cmd/restic/integration_fuse_test.go +++ b/cmd/restic/integration_fuse_test.go @@ -82,7 +82,7 @@ func listSnapshots(t testing.TB, dir string) []string { return names } -func checkSnapshots(t testing.TB, global GlobalOptions, repo *repository.Repository, mountpoint, repodir string, snapshotIDs restic.IDs) { +func checkSnapshots(t testing.TB, global GlobalOptions, repo *repository.Repository, mountpoint, repodir string, snapshotIDs restic.IDs, expectedSnapshotsInFuseDir int) { t.Logf("checking for %d snapshots: %v", len(snapshotIDs), snapshotIDs) go testRunMount(t, global, mountpoint) @@ -99,14 +99,24 @@ func checkSnapshots(t testing.TB, global GlobalOptions, repo *repository.Reposit namesInSnapshots := listSnapshots(t, mountpoint) t.Logf("found %v snapshots in fuse mount: %v", len(namesInSnapshots), namesInSnapshots) Assert(t, - len(namesInSnapshots) == len(snapshotIDs), - "Invalid number of snapshots: expected %d, got %d", len(snapshotIDs), len(namesInSnapshots)) + expectedSnapshotsInFuseDir == len(namesInSnapshots), + "Invalid number of snapshots: expected %d, got %d", expectedSnapshotsInFuseDir, len(namesInSnapshots)) namesMap := make(map[string]bool) for _, name := range namesInSnapshots { namesMap[name] = false } + // Is "latest" present? + if len(namesMap) != 0 { + _, ok := namesMap["latest"] + if !ok { + t.Errorf("Symlink latest isn't present in fuse dir") + } else { + namesMap["latest"] = true + } + } + for _, id := range snapshotIDs { snapshot, err := restic.LoadSnapshot(context.TODO(), repo, id) OK(t, err) @@ -153,7 +163,7 @@ func TestMount(t *testing.T) { // We remove the mountpoint now to check that cmdMount creates it RemoveAll(t, env.mountpoint) - checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, []restic.ID{}) + checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, []restic.ID{}, 0) SetupTarTestFixture(t, env.testdata, filepath.Join("testdata", "backup-data.tar.gz")) @@ -163,7 +173,7 @@ func TestMount(t *testing.T) { Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs) - checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, snapshotIDs) + checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, snapshotIDs, 2) // second backup, implicit incremental testRunBackup(t, []string{env.testdata}, BackupOptions{}, env.gopts) @@ -171,7 +181,7 @@ func TestMount(t *testing.T) { Assert(t, len(snapshotIDs) == 2, "expected two snapshots, got %v", snapshotIDs) - checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, snapshotIDs) + checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, snapshotIDs, 3) // third backup, explicit incremental bopts := BackupOptions{Parent: snapshotIDs[0].String()} @@ -180,7 +190,7 @@ func TestMount(t *testing.T) { Assert(t, len(snapshotIDs) == 3, "expected three snapshots, got %v", snapshotIDs) - checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, snapshotIDs) + checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, snapshotIDs, 4) } func TestMountSameTimestamps(t *testing.T) { @@ -202,5 +212,5 @@ func TestMountSameTimestamps(t *testing.T) { restic.TestParseID("5fd0d8b2ef0fa5d23e58f1e460188abb0f525c0f0c4af8365a1280c807a80a1b"), } - checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, ids) + checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, ids, 4) } diff --git a/internal/fuse/snapshots_dir.go b/internal/fuse/snapshots_dir.go index 8a4ba1861..fc0f63825 100644 --- a/internal/fuse/snapshots_dir.go +++ b/internal/fuse/snapshots_dir.go @@ -23,11 +23,13 @@ type SnapshotsDir struct { root *Root snapshots restic.Snapshots names map[string]*restic.Snapshot + latest string } // ensure that *SnapshotsDir implements these interfaces var _ = fs.HandleReadDirAller(&SnapshotsDir{}) var _ = fs.NodeStringLookuper(&SnapshotsDir{}) +var _ = fs.NodeReadlinker(&snapshotLink{}) // NewSnapshotsDir returns a new directory containing snapshots. func NewSnapshotsDir(root *Root, inode uint64, snapshots restic.Snapshots) *SnapshotsDir { @@ -39,8 +41,16 @@ func NewSnapshotsDir(root *Root, inode uint64, snapshots restic.Snapshots) *Snap names: make(map[string]*restic.Snapshot, len(snapshots)), } + // Track latest Snapshot + var latestTime time.Time + d.latest = "" + for _, sn := range snapshots { name := sn.Time.Format(time.RFC3339) + if d.latest == "" || !sn.Time.Before(latestTime) { + latestTime = sn.Time + d.latest = name + } for i := 1; ; i++ { if _, ok := d.names[name]; !ok { break @@ -93,15 +103,65 @@ func (d *SnapshotsDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) { }) } + // Latest + if d.latest != "" { + items = append(items, fuse.Dirent{ + Inode: fs.GenerateDynamicInode(d.inode, "latest"), + Name: "latest", + Type: fuse.DT_Link, + }) + } return items, nil } +type snapshotLink struct { + root *Root + inode uint64 + target string + snapshot *restic.Snapshot +} + +func newSnapshotLink(ctx context.Context, root *Root, inode uint64, target string, snapshot *restic.Snapshot) (*snapshotLink, error) { + return &snapshotLink{root: root, inode: inode, target: target, snapshot: snapshot}, nil +} + +func (l *snapshotLink) Readlink(ctx context.Context, req *fuse.ReadlinkRequest) (string, error) { + return l.target, nil +} + +func (l *snapshotLink) Attr(ctx context.Context, a *fuse.Attr) error { + a.Inode = l.inode + a.Mode = os.ModeSymlink | 0777 + + if !l.root.cfg.OwnerIsRoot { + a.Uid = uint32(os.Getuid()) + a.Gid = uint32(os.Getgid()) + } + a.Atime = l.snapshot.Time + a.Ctime = l.snapshot.Time + a.Mtime = l.snapshot.Time + + a.Nlink = 1 + + return 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 { + if name == "latest" && d.latest != "" { + sn2, ok2 := d.names[d.latest] + + // internal error + if !ok2 { + return nil, fuse.ENOENT + } + + return newSnapshotLink(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), d.latest, sn2) + } return nil, fuse.ENOENT }