diff --git a/changelog/unreleased/issue-2907 b/changelog/unreleased/issue-2907 new file mode 100644 index 000000000..5d5c9d8e8 --- /dev/null +++ b/changelog/unreleased/issue-2907 @@ -0,0 +1,7 @@ +Enhancement: Make snapshot directory structure of mount command custimizable + +We've added the possibility to customize the snapshot directory structure of the mount command. +This includes using subdirectories which is now also possible within time template and tags. + +https://github.com/restic/restic/issues/2907 +https://github.com/restic/restic/pull/2913 diff --git a/cmd/restic/cmd_mount.go b/cmd/restic/cmd_mount.go index b5112f181..747316f9f 100644 --- a/cmd/restic/cmd_mount.go +++ b/cmd/restic/cmd_mount.go @@ -5,6 +5,7 @@ package main import ( "os" + "strings" "time" "github.com/spf13/cobra" @@ -30,10 +31,13 @@ read-only mount. Snapshot Directories ==================== -If you need a different template for all directories that contain snapshots, -you can pass a template via --snapshot-template. Example without colons: +If you need a different template for directories that contain snapshots, +you can pass a time template via --time-template and path templates via +--path-template. - --snapshot-template "2006-01-02_15-04-05" +Example time template without colons: + + --time-template "2006-01-02_15-04-05" You need to specify a sample format for exactly the following timestamp: @@ -42,6 +46,20 @@ You need to specify a sample format for exactly the following timestamp: For details please see the documentation for time.Format() at: https://godoc.org/time#Time.Format +For path templates, you can use the following patterns which will be replaced: + %i by short snapshot ID + %I by long snapshot ID + %u by username + %h by hostname + %t by tags + %T by timestamp as specified by --time-template + +The default path templates are: + "ids/%i" + "snapshots/%T" + "hosts/%h/%T" + "tags/%t/%T" + EXIT STATUS =========== @@ -61,7 +79,8 @@ type MountOptions struct { Hosts []string Tags restic.TagLists Paths []string - SnapshotTemplate string + TimeTemplate string + PathTemplates []string } var mountOptions MountOptions @@ -78,13 +97,21 @@ func init() { mountFlags.Var(&mountOptions.Tags, "tag", "only consider snapshots which include this `taglist`") mountFlags.StringArrayVar(&mountOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`") - mountFlags.StringVar(&mountOptions.SnapshotTemplate, "snapshot-template", time.RFC3339, "set `template` to use for snapshot dirs") + mountFlags.StringArrayVar(&mountOptions.PathTemplates, "path-template", nil, "set `template` for path names (can be specified multiple times)") + mountFlags.StringVar(&mountOptions.TimeTemplate, "snapshot-template", time.RFC3339, "set `template` to use for snapshot dirs") + mountFlags.StringVar(&mountOptions.TimeTemplate, "time-template", time.RFC3339, "set `template` to use for times") + _ = mountFlags.MarkDeprecated("snapshot-template", "use --time-template") } func runMount(opts MountOptions, gopts GlobalOptions, args []string) error { - if opts.SnapshotTemplate == "" { - return errors.Fatal("snapshot template string cannot be empty") + if opts.TimeTemplate == "" { + return errors.Fatal("time template string cannot be empty") } + + if strings.HasPrefix(opts.TimeTemplate, "/") || strings.HasSuffix(opts.TimeTemplate, "/") { + return errors.Fatal("time template string cannot start or end with '/'") + } + if len(args) == 0 { return errors.Fatal("wrong number of parameters") } @@ -150,11 +177,12 @@ func runMount(opts MountOptions, gopts GlobalOptions, args []string) error { } cfg := fuse.Config{ - OwnerIsRoot: opts.OwnerRoot, - Hosts: opts.Hosts, - Tags: opts.Tags, - Paths: opts.Paths, - SnapshotTemplate: opts.SnapshotTemplate, + OwnerIsRoot: opts.OwnerRoot, + Hosts: opts.Hosts, + Tags: opts.Tags, + Paths: opts.Paths, + TimeTemplate: opts.TimeTemplate, + PathTemplates: opts.PathTemplates, } root := fuse.NewRoot(repo, cfg) diff --git a/cmd/restic/integration_fuse_test.go b/cmd/restic/integration_fuse_test.go index 156a8abae..6a95ac87d 100644 --- a/cmd/restic/integration_fuse_test.go +++ b/cmd/restic/integration_fuse_test.go @@ -55,7 +55,7 @@ func waitForMount(t testing.TB, dir string) { func testRunMount(t testing.TB, gopts GlobalOptions, dir string) { opts := MountOptions{ - SnapshotTemplate: time.RFC3339, + TimeTemplate: time.RFC3339, } rtest.OK(t, runMount(opts, gopts, []string{dir})) } diff --git a/internal/fuse/root.go b/internal/fuse/root.go index 8165ddafa..5a14b374f 100644 --- a/internal/fuse/root.go +++ b/internal/fuse/root.go @@ -15,11 +15,12 @@ import ( // Config holds settings for the fuse mount. type Config struct { - OwnerIsRoot bool - Hosts []string - Tags []restic.TagList - Paths []string - SnapshotTemplate string + OwnerIsRoot bool + Hosts []string + Tags []restic.TagList + Paths []string + TimeTemplate string + PathTemplates []string } // Root is the root node of the fuse mount of a repository. @@ -59,14 +60,17 @@ func NewRoot(repo restic.Repository, cfg Config) *Root { root.gid = uint32(os.Getgid()) } - paths := []string{ - "ids/%i", - "snapshots/%T", - "hosts/%h/%T", - "tags/%t/%T", + // set defaults, if PathTemplates is not set + if len(cfg.PathTemplates) == 0 { + cfg.PathTemplates = []string{ + "ids/%i", + "snapshots/%T", + "hosts/%h/%T", + "tags/%t/%T", + } } - root.SnapshotsDir = NewSnapshotsDir(root, rootInode, NewSnapshotsDirStructure(root, paths, cfg.SnapshotTemplate), "") + root.SnapshotsDir = NewSnapshotsDir(root, rootInode, NewSnapshotsDirStructure(root, cfg.PathTemplates, cfg.TimeTemplate), "") return root }