Merge pull request #866 from middelink/widespread-tags

Add --tag filtering to every command, where applicable
This commit is contained in:
Alexander Neumann 2017-03-09 09:55:26 +01:00
commit 93e4e4f4fb
24 changed files with 316 additions and 344 deletions

View file

@ -2,7 +2,6 @@ language: go
sudo: false sudo: false
go: go:
- 1.6.4
- 1.7.5 - 1.7.5
- 1.8 - 1.8
- tip - tip
@ -17,8 +16,6 @@ env:
matrix: matrix:
exclude: exclude:
- os: osx
go: 1.6.4
- os: osx - os: osx
go: 1.7.5 go: 1.7.5
- os: osx - os: osx

View file

@ -77,7 +77,7 @@ Just clone the repository, `cd` to it and run `gb build` to build the binary:
[...] [...]
$ bin/restic version $ bin/restic version
restic compiled manually restic compiled manually
compiled at unknown time with go1.6 compiled at unknown time with go1.7
The following commands can be used to run all the tests: The following commands can be used to run all the tests:

View file

@ -34,7 +34,7 @@ You can download the latest pre-compiled binary from the [restic release page](h
Build restic Build restic
============ ============
Install Go/Golang (at least version 1.6), then run `go run build.go`, Install Go/Golang (at least version 1.7), then run `go run build.go`,
afterwards you'll find the binary in the current directory: afterwards you'll find the binary in the current directory:
$ go run build.go $ go run build.go

2
Vagrantfile vendored
View file

@ -1,7 +1,7 @@
# -*- mode: ruby -*- # -*- mode: ruby -*-
# vi: set ft=ruby : # vi: set ft=ruby :
GO_VERSION = '1.6' GO_VERSION = '1.7'
def packages_freebsd def packages_freebsd
return <<-EOF return <<-EOF

View file

@ -27,7 +27,7 @@ $ pacaur -S restic-git
# Building restic # Building restic
restic is written in the Go programming language and you need at least Go version 1.6. restic is written in the Go programming language and you need at least Go version 1.7.
Building restic may also work with older versions of Go, but that's not supported. Building restic may also work with older versions of Go, but that's not supported.
See the [Getting started](https://golang.org/doc/install) guide of the Go project for See the [Getting started](https://golang.org/doc/install) guide of the Go project for
instructions how to install Go. instructions how to install Go.

View file

@ -67,12 +67,12 @@ func init() {
f := cmdBackup.Flags() f := cmdBackup.Flags()
f.StringVar(&backupOptions.Parent, "parent", "", "use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)") f.StringVar(&backupOptions.Parent, "parent", "", "use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)")
f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories (overrides the "parent" flag)`) f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories (overrides the "parent" flag)`)
f.StringSliceVarP(&backupOptions.Excludes, "exclude", "e", []string{}, "exclude a `pattern` (can be specified multiple times)") f.StringSliceVarP(&backupOptions.Excludes, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
f.StringVar(&backupOptions.ExcludeFile, "exclude-file", "", "read exclude patterns from a file") f.StringVar(&backupOptions.ExcludeFile, "exclude-file", "", "read exclude patterns from a file")
f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems") f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems")
f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin") f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "file name to use when reading from stdin") f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "file name to use when reading from stdin")
f.StringSliceVar(&backupOptions.Tags, "tag", []string{}, "add a `tag` for the new snapshot (can be specified multiple times)") f.StringSliceVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)")
f.StringVar(&backupOptions.Hostname, "hostname", hostname, "set the `hostname` for the snapshot manually") f.StringVar(&backupOptions.Hostname, "hostname", hostname, "set the `hostname` for the snapshot manually")
f.StringVar(&backupOptions.FilesFrom, "files-from", "", "read the files to backup from file (can be combined with file args)") f.StringVar(&backupOptions.FilesFrom, "files-from", "", "read the files to backup from file (can be combined with file args)")
} }
@ -391,7 +391,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
// Find last snapshot to set it as parent, if not already set // Find last snapshot to set it as parent, if not already set
if !opts.Force && parentSnapshotID == nil { if !opts.Force && parentSnapshotID == nil {
id, err := restic.FindLatestSnapshot(repo, target, opts.Hostname) id, err := restic.FindLatestSnapshot(repo, target, opts.Tags, opts.Hostname)
if err == nil { if err == nil {
parentSnapshotID = &id parentSnapshotID = &id
} else if err != restic.ErrNoSnapshotFound { } else if err != restic.ErrNoSnapshotFound {

View file

@ -24,7 +24,7 @@ finds. It can also be used to read all data and therefore simulate a restore.
}, },
} }
// CheckOptions bundle all options for the 'check' command. // CheckOptions bundles all options for the 'check' command.
type CheckOptions struct { type CheckOptions struct {
ReadData bool ReadData bool
CheckUnused bool CheckUnused bool

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
@ -24,12 +25,16 @@ repo. `,
}, },
} }
// FindOptions bundle all options for the find command. // FindOptions bundles all options for the find command.
type FindOptions struct { type FindOptions struct {
Oldest string Oldest string
Newest string Newest string
Snapshot string Snapshots []string
CaseInsensitive bool CaseInsensitive bool
ListLong bool
Host string
Paths []string
Tags []string
} }
var findOptions FindOptions var findOptions FindOptions
@ -40,8 +45,13 @@ func init() {
f := cmdFind.Flags() f := cmdFind.Flags()
f.StringVarP(&findOptions.Oldest, "oldest", "o", "", "oldest modification date/time") f.StringVarP(&findOptions.Oldest, "oldest", "o", "", "oldest modification date/time")
f.StringVarP(&findOptions.Newest, "newest", "n", "", "newest modification date/time") f.StringVarP(&findOptions.Newest, "newest", "n", "", "newest modification date/time")
f.StringVarP(&findOptions.Snapshot, "snapshot", "s", "", "snapshot ID to search in") f.StringSliceVarP(&findOptions.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)")
f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern") f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern")
f.BoolVarP(&findOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
f.StringVarP(&findOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
f.StringSliceVar(&findOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot-ID is given")
f.StringSliceVar(&findOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given")
} }
type findPattern struct { type findPattern struct {
@ -50,11 +60,6 @@ type findPattern struct {
ignoreCase bool ignoreCase bool
} }
type findResult struct {
node *restic.Node
path string
}
var timeFormats = []string{ var timeFormats = []string{
"2006-01-02", "2006-01-02",
"2006-01-02 15:04", "2006-01-02 15:04",
@ -79,14 +84,14 @@ func parseTime(str string) (time.Time, error) {
return time.Time{}, errors.Fatalf("unable to parse time: %q", str) return time.Time{}, errors.Fatalf("unable to parse time: %q", str)
} }
func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, path string) ([]findResult, error) { func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, prefix string, snapshotID *string) error {
debug.Log("checking tree %v\n", id) debug.Log("checking tree %v\n", id)
tree, err := repo.LoadTree(id) tree, err := repo.LoadTree(id)
if err != nil { if err != nil {
return nil, err return err
} }
results := []findResult{}
for _, node := range tree.Nodes { for _, node := range tree.Nodes {
debug.Log(" testing entry %q\n", node.Name) debug.Log(" testing entry %q\n", node.Name)
@ -97,7 +102,7 @@ func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, path
m, err := filepath.Match(pat.pattern, name) m, err := filepath.Match(pat.pattern, name)
if err != nil { if err != nil {
return nil, err return err
} }
if m { if m {
@ -112,46 +117,32 @@ func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, path
continue continue
} }
results = append(results, findResult{node: node, path: path}) if snapshotID != nil {
Verbosef("Found matching entries in snapshot %s\n", *snapshotID)
snapshotID = nil
}
Printf(formatNode(prefix, node, findOptions.ListLong) + "\n")
} else { } else {
debug.Log(" pattern does not match\n") debug.Log(" pattern does not match\n")
} }
if node.Type == "dir" { if node.Type == "dir" {
subdirResults, err := findInTree(repo, pat, *node.Subtree, filepath.Join(path, node.Name)) if err := findInTree(repo, pat, *node.Subtree, filepath.Join(prefix, node.Name), snapshotID); err != nil {
if err != nil { return err
return nil, err
} }
results = append(results, subdirResults...)
} }
} }
return results, nil return nil
} }
func findInSnapshot(repo *repository.Repository, pat findPattern, id restic.ID) error { func findInSnapshot(repo *repository.Repository, sn *restic.Snapshot, pat findPattern) error {
debug.Log("searching in snapshot %s\n for entries within [%s %s]", id.Str(), pat.oldest, pat.newest) debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), pat.oldest, pat.newest)
sn, err := restic.LoadSnapshot(repo, id) snapshotID := sn.ID().Str()
if err != nil { if err := findInTree(repo, pat, *sn.Tree, string(filepath.Separator), &snapshotID); err != nil {
return err return err
} }
results, err := findInTree(repo, pat, *sn.Tree, string(filepath.Separator))
if err != nil {
return err
}
if len(results) == 0 {
return nil
}
Verbosef("found %d matching entries in snapshot %s\n", len(results), id)
for _, res := range results {
res.node.Name = filepath.Join(res.path, res.node.Name)
Printf(" %s\n", res.node)
}
return nil return nil
} }
@ -160,21 +151,21 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
return errors.Fatal("wrong number of arguments") return errors.Fatal("wrong number of arguments")
} }
var ( var err error
err error pat := findPattern{pattern: args[0]}
pat findPattern if opts.CaseInsensitive {
) pat.pattern = strings.ToLower(pat.pattern)
pat.ignoreCase = true
}
if opts.Oldest != "" { if opts.Oldest != "" {
pat.oldest, err = parseTime(opts.Oldest) if pat.oldest, err = parseTime(opts.Oldest); err != nil {
if err != nil {
return err return err
} }
} }
if opts.Newest != "" { if opts.Newest != "" {
pat.newest, err = parseTime(opts.Newest) if pat.newest, err = parseTime(opts.Newest); err != nil {
if err != nil {
return err return err
} }
} }
@ -192,33 +183,14 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
} }
} }
err = repo.LoadIndex() if err = repo.LoadIndex(); err != nil {
if err != nil {
return err return err
} }
pat.pattern = args[0] ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
if opts.CaseInsensitive { for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) {
pat.pattern = strings.ToLower(pat.pattern) if err = findInSnapshot(repo, sn, pat); err != nil {
pat.ignoreCase = true
}
if opts.Snapshot != "" {
snapshotID, err := restic.FindSnapshot(repo, opts.Snapshot)
if err != nil {
return errors.Fatalf("invalid id %q: %v", args[1], err)
}
return findInSnapshot(repo, pat, snapshotID)
}
done := make(chan struct{})
defer close(done)
for snapshotID := range repo.List(restic.SnapshotFile, done) {
err := findInSnapshot(repo, pat, snapshotID)
if err != nil {
return err return err
} }
} }

View file

@ -1,14 +1,12 @@
package main package main
import ( import (
"encoding/hex" "context"
"encoding/json" "encoding/json"
"restic" "restic"
"sort" "sort"
"strings" "strings"
"restic/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -27,13 +25,12 @@ data after 'forget' was run successfully, see the 'prune' command. `,
// ForgetOptions collects all options for the forget command. // ForgetOptions collects all options for the forget command.
type ForgetOptions struct { type ForgetOptions struct {
Last int Last int
Hourly int Hourly int
Daily int Daily int
Weekly int Weekly int
Monthly int Monthly int
Yearly int Yearly int
KeepTags []string KeepTags []string
Host string Host string
@ -83,32 +80,43 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
return err return err
} }
// Process all snapshot IDs given as arguments. // group by hostname and dirs
if len(args) != 0 { type key struct {
for _, s := range args { Hostname string
// Parse argument as hex string. Paths []string
if _, err := hex.DecodeString(s); err != nil { Tags []string
Warnf("argument %q is not a snapshot ID, ignoring\n", s) }
continue snapshotGroups := make(map[string]restic.Snapshots)
}
id, err := restic.FindSnapshot(repo, s)
if err != nil {
Warnf("could not find a snapshot for ID %q, ignoring\n", s)
continue
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
if len(args) > 0 {
// When explicit snapshots args are given, remove them immediately.
if !opts.DryRun { if !opts.DryRun {
h := restic.Handle{Type: restic.SnapshotFile, Name: id.String()} h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
err = repo.Backend().Remove(h) if err = repo.Backend().Remove(h); err != nil {
if err != nil {
return err return err
} }
Verbosef("removed snapshot %v\n", sn.ID().Str())
Verbosef("removed snapshot %v\n", id.Str())
} else { } else {
Verbosef("would remove snapshot %v\n", id.Str()) Verbosef("would have removed snapshot %v\n", sn.ID().Str())
} }
} else {
var tags []string
if opts.GroupByTags {
tags = sn.Tags
sort.StringSlice(tags).Sort()
}
sort.StringSlice(sn.Paths).Sort()
k, err := json.Marshal(key{Hostname: sn.Hostname, Tags: tags, Paths: sn.Paths})
if err != nil {
return err
}
snapshotGroups[string(k)] = append(snapshotGroups[string(k)], sn)
} }
}
if len(args) > 0 {
return nil return nil
} }
@ -122,53 +130,17 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
Tags: opts.KeepTags, Tags: opts.KeepTags,
} }
snapshots, err := restic.LoadAllSnapshots(repo)
if err != nil {
return err
}
// Group snapshots by hostname and dirs.
type key struct {
Hostname string
Paths []string
Tags []string
}
snapshotGroups := make(map[string]restic.Snapshots)
for _, sn := range snapshots {
if opts.Host != "" && sn.Hostname != opts.Host {
continue
}
if !sn.HasTags(opts.Tags) {
continue
}
if !sn.HasPaths(opts.Paths) {
continue
}
var tags []string
if opts.GroupByTags {
sort.StringSlice(sn.Tags).Sort()
tags = sn.Tags
}
sort.StringSlice(sn.Paths).Sort()
k, _ := json.Marshal(key{Hostname: sn.Hostname, Tags: tags, Paths: sn.Paths})
snapshotGroups[string(k)] = append(snapshotGroups[string(k)], sn)
}
if len(snapshotGroups) == 0 {
return errors.Fatal("no snapshots remained after filtering")
}
if policy.Empty() { if policy.Empty() {
Verbosef("no policy was specified, no snapshots will be removed\n") Verbosef("no policy was specified, no snapshots will be removed\n")
return nil
} }
removeSnapshots := 0 removeSnapshots := 0
for k, snapshotGroup := range snapshotGroups { for k, snapshotGroup := range snapshotGroups {
var key key var key key
json.Unmarshal([]byte(k), &key) if json.Unmarshal([]byte(k), &key) != nil {
return err
}
if opts.GroupByTags { if opts.GroupByTags {
Printf("snapshots for host %v, tags [%v], paths: [%v]:\n\n", key.Hostname, strings.Join(key.Tags, ", "), strings.Join(key.Paths, ", ")) Printf("snapshots for host %v, tags [%v], paths: [%v]:\n\n", key.Hostname, strings.Join(key.Tags, ", "), strings.Join(key.Paths, ", "))
} else { } else {

View file

@ -1,13 +1,13 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"restic" "restic"
"github.com/spf13/cobra"
"restic/errors" "restic/errors"
"restic/repository" "restic/repository"
"github.com/spf13/cobra"
) )
var cmdKey = &cobra.Command{ var cmdKey = &cobra.Command{
@ -25,15 +25,12 @@ func init() {
cmdRoot.AddCommand(cmdKey) cmdRoot.AddCommand(cmdKey)
} }
func listKeys(s *repository.Repository) error { func listKeys(ctx context.Context, s *repository.Repository) error {
tab := NewTable() tab := NewTable()
tab.Header = fmt.Sprintf(" %-10s %-10s %-10s %s", "ID", "User", "Host", "Created") tab.Header = fmt.Sprintf(" %-10s %-10s %-10s %s", "ID", "User", "Host", "Created")
tab.RowFormat = "%s%-10s %-10s %-10s %s" tab.RowFormat = "%s%-10s %-10s %-10s %s"
done := make(chan struct{}) for id := range s.List(restic.KeyFile, ctx.Done()) {
defer close(done)
for id := range s.List(restic.KeyFile, done) {
k, err := repository.LoadKey(s, id.String()) k, err := repository.LoadKey(s, id.String())
if err != nil { if err != nil {
Warnf("LoadKey() failed: %v\n", err) Warnf("LoadKey() failed: %v\n", err)
@ -124,6 +121,9 @@ func runKey(gopts GlobalOptions, args []string) error {
return errors.Fatal("wrong number of arguments") return errors.Fatal("wrong number of arguments")
} }
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
repo, err := OpenRepository(gopts) repo, err := OpenRepository(gopts)
if err != nil { if err != nil {
return err return err
@ -137,7 +137,7 @@ func runKey(gopts GlobalOptions, args []string) error {
return err return err
} }
return listKeys(repo) return listKeys(ctx, repo)
case "add": case "add":
lock, err := lockRepo(repo) lock, err := lockRepo(repo)
defer unlockRepo(lock) defer unlockRepo(lock)

View file

@ -1,8 +1,7 @@
package main package main
import ( import (
"fmt" "context"
"os"
"path/filepath" "path/filepath"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -13,7 +12,7 @@ import (
) )
var cmdLs = &cobra.Command{ var cmdLs = &cobra.Command{
Use: "ls [flags] snapshot-ID", Use: "ls [flags] [snapshot-ID ...]",
Short: "list files in a snapshot", Short: "list files in a snapshot",
Long: ` Long: `
The "ls" command allows listing files and directories in a snapshot. The "ls" command allows listing files and directories in a snapshot.
@ -21,7 +20,7 @@ The "ls" command allows listing files and directories in a snapshot.
The special snapshot-ID "latest" can be used to list files and directories of the latest snapshot in the repository. The special snapshot-ID "latest" can be used to list files and directories of the latest snapshot in the repository.
`, `,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runLs(globalOptions, args) return runLs(lsOptions, globalOptions, args)
}, },
} }
@ -29,6 +28,7 @@ The special snapshot-ID "latest" can be used to list files and directories of th
type LsOptions struct { type LsOptions struct {
ListLong bool ListLong bool
Host string Host string
Tags []string
Paths []string Paths []string
} }
@ -40,42 +40,22 @@ func init() {
flags := cmdLs.Flags() flags := cmdLs.Flags()
flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
flags.StringVarP(&lsOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`) flags.StringVarP(&lsOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
flags.StringSliceVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"") flags.StringSliceVar(&lsOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot ID is given")
flags.StringSliceVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given")
} }
func printNode(prefix string, n *restic.Node) string { func printTree(repo *repository.Repository, id *restic.ID, prefix string) error {
if !lsOptions.ListLong { tree, err := repo.LoadTree(*id)
return filepath.Join(prefix, n.Name)
}
switch n.Type {
case "file":
return fmt.Sprintf("%s %5d %5d %6d %s %s",
n.Mode, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name))
case "dir":
return fmt.Sprintf("%s %5d %5d %6d %s %s",
n.Mode|os.ModeDir, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name))
case "symlink":
return fmt.Sprintf("%s %5d %5d %6d %s %s -> %s",
n.Mode|os.ModeSymlink, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name), n.LinkTarget)
default:
return fmt.Sprintf("<Node(%s) %s>", n.Type, n.Name)
}
}
func printTree(prefix string, repo *repository.Repository, id restic.ID) error {
tree, err := repo.LoadTree(id)
if err != nil { if err != nil {
return err return err
} }
for _, entry := range tree.Nodes { for _, entry := range tree.Nodes {
Printf(printNode(prefix, entry) + "\n") Printf(formatNode(prefix, entry, lsOptions.ListLong) + "\n")
if entry.Type == "dir" && entry.Subtree != nil { if entry.Type == "dir" && entry.Subtree != nil {
err = printTree(filepath.Join(prefix, entry.Name), repo, *entry.Subtree) if err = printTree(repo, entry.Subtree, filepath.Join(prefix, entry.Name)); err != nil {
if err != nil {
return err return err
} }
} }
@ -84,9 +64,9 @@ func printTree(prefix string, repo *repository.Repository, id restic.ID) error {
return nil return nil
} }
func runLs(gopts GlobalOptions, args []string) error { func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
if len(args) < 1 || len(args) > 2 { if len(args) == 0 && opts.Host == "" && len(opts.Tags) == 0 && len(opts.Paths) == 0 {
return errors.Fatal("no snapshot ID given") return errors.Fatal("Invalid arguments, either give one or more snapshot IDs or set filters.")
} }
repo, err := OpenRepository(gopts) repo, err := OpenRepository(gopts)
@ -94,32 +74,18 @@ func runLs(gopts GlobalOptions, args []string) error {
return err return err
} }
err = repo.LoadIndex() if err = repo.LoadIndex(); err != nil {
if err != nil {
return err return err
} }
snapshotIDString := args[0] ctx, cancel := context.WithCancel(gopts.ctx)
var id restic.ID defer cancel()
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), sn.Paths, sn.Time)
if snapshotIDString == "latest" { if err = printTree(repo, sn.Tree, string(filepath.Separator)); err != nil {
id, err = restic.FindLatestSnapshot(repo, lsOptions.Paths, lsOptions.Host) return err
if err != nil {
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, lsOptions.Paths, lsOptions.Host)
}
} else {
id, err = restic.FindSnapshot(repo, snapshotIDString)
if err != nil {
Exitf(1, "invalid id %q: %v", snapshotIDString, err)
} }
} }
return nil
sn, err := restic.LoadSnapshot(repo, id)
if err != nil {
return err
}
Verbosef("snapshot of %v at %s:\n", sn.Paths, sn.Time)
return printTree(string(filepath.Separator), repo, *sn.Tree)
} }

View file

@ -35,6 +35,9 @@ type MountOptions struct {
OwnerRoot bool OwnerRoot bool
AllowRoot bool AllowRoot bool
AllowOther bool AllowOther bool
Host string
Tags []string
Paths []string
} }
var mountOptions MountOptions var mountOptions MountOptions
@ -42,9 +45,14 @@ var mountOptions MountOptions
func init() { func init() {
cmdRoot.AddCommand(cmdMount) cmdRoot.AddCommand(cmdMount)
cmdMount.Flags().BoolVar(&mountOptions.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs") mountFlags := cmdMount.Flags()
cmdMount.Flags().BoolVar(&mountOptions.AllowRoot, "allow-root", false, "allow root user to access the data in the mounted directory") mountFlags.BoolVar(&mountOptions.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs")
cmdMount.Flags().BoolVar(&mountOptions.AllowOther, "allow-other", false, "allow other users to access the data in the mounted directory") mountFlags.BoolVar(&mountOptions.AllowRoot, "allow-root", false, "allow root user to access the data in the mounted directory")
mountFlags.BoolVar(&mountOptions.AllowOther, "allow-other", false, "allow other users to access the data in the mounted directory")
mountFlags.StringVarP(&mountOptions.Host, "host", "H", "", `only consider snapshots for this host`)
mountFlags.StringSliceVar(&mountOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`")
mountFlags.StringSliceVar(&mountOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`")
} }
func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error { func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
@ -91,7 +99,7 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
Printf("Don't forget to umount after quitting!\n") Printf("Don't forget to umount after quitting!\n")
root := fs.Tree{} root := fs.Tree{}
root.Add("snapshots", fuse.NewSnapshotsDir(repo, opts.OwnerRoot)) root.Add("snapshots", fuse.NewSnapshotsDir(repo, opts.OwnerRoot, opts.Paths, opts.Tags, opts.Host))
debug.Log("serving mount at %v", mountpoint) debug.Log("serving mount at %v", mountpoint)
err = fs.Serve(c, &root) err = fs.Serve(c, &root)

View file

@ -1,8 +1,8 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"os"
"restic" "restic"
"restic/debug" "restic/debug"
"restic/errors" "restic/errors"
@ -81,8 +81,8 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
return err return err
} }
done := make(chan struct{}) ctx, cancel := context.WithCancel(gopts.ctx)
defer close(done) defer cancel()
var stats struct { var stats struct {
blobs int blobs int
@ -92,7 +92,7 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
} }
Verbosef("counting files in repo\n") Verbosef("counting files in repo\n")
for _ = range repo.List(restic.DataFile, done) { for _ = range repo.List(restic.DataFile, ctx.Done()) {
stats.packs++ stats.packs++
} }
@ -238,35 +238,10 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
bar.Done() bar.Done()
} }
Verbosef("creating new index\n") if err = rebuildIndex(ctx, repo); err != nil {
stats.packs = 0
for _ = range repo.List(restic.DataFile, done) {
stats.packs++
}
bar = newProgressMax(!gopts.Quiet, uint64(stats.packs), "packs")
idx, err = index.New(repo, bar)
if err != nil {
return err return err
} }
var supersedes restic.IDs
for idxID := range repo.List(restic.IndexFile, done) {
h := restic.Handle{Type: restic.IndexFile, Name: idxID.String()}
err := repo.Backend().Remove(h)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to remove index %v: %v\n", idxID.Str(), err)
}
supersedes = append(supersedes, idxID)
}
id, err := idx.Save(repo, supersedes)
if err != nil {
return err
}
Verbosef("saved new index as %v\n", id.Str())
Verbosef("done\n") Verbosef("done\n")
return nil return nil
} }

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"restic" "restic"
"restic/index" "restic/index"
@ -35,25 +36,29 @@ func runRebuildIndex(gopts GlobalOptions) error {
return err return err
} }
done := make(chan struct{}) ctx, cancel := context.WithCancel(gopts.ctx)
defer close(done) defer cancel()
return rebuildIndex(ctx, repo)
}
func rebuildIndex(ctx context.Context, repo restic.Repository) error {
Verbosef("counting files in repo\n") Verbosef("counting files in repo\n")
var packs uint64 var packs uint64
for _ = range repo.List(restic.DataFile, done) { for _ = range repo.List(restic.DataFile, ctx.Done()) {
packs++ packs++
} }
bar := newProgressMax(!gopts.Quiet, packs, "packs") bar := newProgressMax(!globalOptions.Quiet, packs, "packs")
idx, err := index.New(repo, bar) idx, err := index.New(repo, bar)
if err != nil { if err != nil {
return err return err
} }
Verbosef("listing old index files\n") Verbosef("finding old index files\n")
var supersedes restic.IDs var supersedes restic.IDs
for id := range repo.List(restic.IndexFile, done) { for id := range repo.List(restic.IndexFile, ctx.Done()) {
supersedes = append(supersedes, id) supersedes = append(supersedes, id)
} }
@ -67,13 +72,11 @@ func runRebuildIndex(gopts GlobalOptions) error {
Verbosef("remove %d old index files\n", len(supersedes)) Verbosef("remove %d old index files\n", len(supersedes))
for _, id := range supersedes { for _, id := range supersedes {
err := repo.Backend().Remove(restic.Handle{ if err := repo.Backend().Remove(restic.Handle{
Type: restic.IndexFile, Type: restic.IndexFile,
Name: id.String(), Name: id.String(),
}) }); err != nil {
Warnf("error removing old index %v: %v\n", id.Str(), err)
if err != nil {
Warnf("error deleting old index %v: %v\n", id.Str(), err)
} }
} }

View file

@ -31,6 +31,7 @@ type RestoreOptions struct {
Target string Target string
Host string Host string
Paths []string Paths []string
Tags []string
} }
var restoreOptions RestoreOptions var restoreOptions RestoreOptions
@ -44,6 +45,7 @@ func init() {
flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to") flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to")
flags.StringVarP(&restoreOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`) flags.StringVarP(&restoreOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`)
flags.StringSliceVar(&restoreOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` for snapshot ID \"latest\"")
flags.StringSliceVar(&restoreOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"") flags.StringSliceVar(&restoreOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
} }
@ -85,7 +87,7 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
var id restic.ID var id restic.ID
if snapshotIDString == "latest" { if snapshotIDString == "latest" {
id, err = restic.FindLatestSnapshot(repo, opts.Paths, opts.Host) id, err = restic.FindLatestSnapshot(repo, opts.Paths, opts.Tags, opts.Host)
if err != nil { if err != nil {
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host) Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host)
} }

View file

@ -1,19 +1,19 @@
package main package main
import ( import (
"context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"restic/errors"
"sort" "sort"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"encoding/json"
"restic" "restic"
) )
var cmdSnapshots = &cobra.Command{ var cmdSnapshots = &cobra.Command{
Use: "snapshots", Use: "snapshots [snapshotID ...]",
Short: "list all snapshots", Short: "list all snapshots",
Long: ` Long: `
The "snapshots" command lists all snapshots stored in the repository. The "snapshots" command lists all snapshots stored in the repository.
@ -23,9 +23,10 @@ The "snapshots" command lists all snapshots stored in the repository.
}, },
} }
// SnapshotOptions bundle all options for the snapshots command. // SnapshotOptions bundles all options for the snapshots command.
type SnapshotOptions struct { type SnapshotOptions struct {
Host string Host string
Tags []string
Paths []string Paths []string
} }
@ -35,15 +36,12 @@ func init() {
cmdRoot.AddCommand(cmdSnapshots) cmdRoot.AddCommand(cmdSnapshots)
f := cmdSnapshots.Flags() f := cmdSnapshots.Flags()
f.StringVar(&snapshotOptions.Host, "host", "", "only print snapshots for this host") f.StringVarP(&snapshotOptions.Host, "host", "H", "", "only consider snapshots for this `host`")
f.StringSliceVar(&snapshotOptions.Paths, "path", []string{}, "only print snapshots for this `path` (can be specified multiple times)") f.StringSliceVar(&snapshotOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` (can be specified multiple times)")
f.StringSliceVar(&snapshotOptions.Paths, "path", nil, "only consider snapshots for this `path` (can be specified multiple times)")
} }
func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error { func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error {
if len(args) != 0 {
return errors.Fatal("wrong number of arguments")
}
repo, err := OpenRepository(gopts) repo, err := OpenRepository(gopts)
if err != nil { if err != nil {
return err return err
@ -57,32 +55,14 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro
} }
} }
done := make(chan struct{}) ctx, cancel := context.WithCancel(gopts.ctx)
defer close(done) defer cancel()
list := []*restic.Snapshot{}
for id := range repo.List(restic.SnapshotFile, done) {
sn, err := restic.LoadSnapshot(repo, id)
if err != nil {
Warnf("error loading snapshot %s: %v\n", id, err)
continue
}
if (opts.Host == "" || opts.Host == sn.Hostname) && sn.HasPaths(opts.Paths) {
pos := sort.Search(len(list), func(i int) bool {
return list[i].Time.After(sn.Time)
})
if pos < len(list) {
list = append(list, nil)
copy(list[pos+1:], list[pos:])
list[pos] = sn
} else {
list = append(list, sn)
}
}
var list restic.Snapshots
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
list = append(list, sn)
} }
sort.Sort(sort.Reverse(list))
if gopts.JSON { if gopts.JSON {
err := printSnapshotsJSON(gopts.stdout, list) err := printSnapshotsJSON(gopts.stdout, list)
@ -97,7 +77,7 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro
} }
// PrintSnapshots prints a text table of the snapshots in list to stdout. // PrintSnapshots prints a text table of the snapshots in list to stdout.
func PrintSnapshots(stdout io.Writer, list []*restic.Snapshot) { func PrintSnapshots(stdout io.Writer, list restic.Snapshots) {
// Determine the max widths for host and tag. // Determine the max widths for host and tag.
maxHost, maxTag := 10, 6 maxHost, maxTag := 10, 6
@ -165,7 +145,7 @@ func PrintSnapshots(stdout io.Writer, list []*restic.Snapshot) {
tab.Write(stdout) tab.Write(stdout)
} }
// Snapshot helps to print Snaphots as JSON // Snapshot helps to print Snaphots as JSON with their ID included.
type Snapshot struct { type Snapshot struct {
*restic.Snapshot *restic.Snapshot
@ -173,7 +153,7 @@ type Snapshot struct {
} }
// printSnapshotsJSON writes the JSON representation of list to stdout. // printSnapshotsJSON writes the JSON representation of list to stdout.
func printSnapshotsJSON(stdout io.Writer, list []*restic.Snapshot) error { func printSnapshotsJSON(stdout io.Writer, list restic.Snapshots) error {
var snapshots []Snapshot var snapshots []Snapshot
@ -187,5 +167,4 @@ func printSnapshotsJSON(stdout io.Writer, list []*restic.Snapshot) error {
} }
return json.NewEncoder(stdout).Encode(snapshots) return json.NewEncoder(stdout).Encode(snapshots)
} }

View file

@ -1,6 +1,8 @@
package main package main
import ( import (
"context"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"restic" "restic"
@ -45,22 +47,14 @@ func init() {
tagFlags.StringSliceVar(&tagOptions.AddTags, "add", nil, "`tag` which will be added to the existing tags (can be given multiple times)") tagFlags.StringSliceVar(&tagOptions.AddTags, "add", nil, "`tag` which will be added to the existing tags (can be given multiple times)")
tagFlags.StringSliceVar(&tagOptions.RemoveTags, "remove", nil, "`tag` which will be removed from the existing tags (can be given multiple times)") tagFlags.StringSliceVar(&tagOptions.RemoveTags, "remove", nil, "`tag` which will be removed from the existing tags (can be given multiple times)")
tagFlags.StringVarP(&tagOptions.Host, "host", "H", "", `only consider snapshots for this host, when no snapshot ID is given`) tagFlags.StringVarP(&tagOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
tagFlags.StringSliceVar(&tagOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot-ID is given") tagFlags.StringSliceVar(&tagOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot-ID is given")
tagFlags.StringSliceVar(&tagOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given") tagFlags.StringSliceVar(&tagOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given")
} }
func changeTags(repo *repository.Repository, snapshotID restic.ID, setTags, addTags, removeTags, tags, paths []string, host string) (bool, error) { func changeTags(repo *repository.Repository, sn *restic.Snapshot, setTags, addTags, removeTags []string) (bool, error) {
var changed bool var changed bool
sn, err := restic.LoadSnapshot(repo, snapshotID)
if err != nil {
return false, err
}
if (host != "" && host != sn.Hostname) || !sn.HasTags(tags) || !sn.HasPaths(paths) {
return false, nil
}
if len(setTags) != 0 { if len(setTags) != 0 {
// Setting the tag to an empty string really means no tags. // Setting the tag to an empty string really means no tags.
if len(setTags) == 1 && setTags[0] == "" { if len(setTags) == 1 && setTags[0] == "" {
@ -126,37 +120,13 @@ func runTag(opts TagOptions, gopts GlobalOptions, args []string) error {
} }
} }
var ids restic.IDs
if len(args) != 0 {
// When explit snapshot-IDs are given, the filtering does not matter anymore.
opts.Host = ""
opts.Tags = nil
opts.Paths = nil
// Process all snapshot IDs given as arguments.
for _, s := range args {
snapshotID, err := restic.FindSnapshot(repo, s)
if err != nil {
Warnf("could not find a snapshot for ID %q, ignoring: %v\n", s, err)
continue
}
ids = append(ids, snapshotID)
}
ids = ids.Uniq()
} else {
// If there were no arguments, just get all snapshots.
done := make(chan struct{})
defer close(done)
for snapshotID := range repo.List(restic.SnapshotFile, done) {
ids = append(ids, snapshotID)
}
}
changeCnt := 0 changeCnt := 0
for _, id := range ids { ctx, cancel := context.WithCancel(gopts.ctx)
changed, err := changeTags(repo, id, opts.SetTags, opts.AddTags, opts.RemoveTags, opts.Tags, opts.Paths, opts.Host) defer cancel()
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
changed, err := changeTags(repo, sn, opts.SetTags, opts.AddTags, opts.RemoveTags)
if err != nil { if err != nil {
Warnf("unable to modify the tags for snapshot ID %q, ignoring: %v\n", id, err) Warnf("unable to modify the tags for snapshot ID %q, ignoring: %v\n", sn.ID(), err)
continue continue
} }
if changed { if changed {

78
src/cmds/restic/find.go Normal file
View file

@ -0,0 +1,78 @@
package main
import (
"context"
"restic"
"restic/repository"
)
// FindFilteredSnapshots yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots.
func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, host string, tags []string, paths []string, snapshotIDs []string) <-chan *restic.Snapshot {
out := make(chan *restic.Snapshot)
go func() {
defer close(out)
if len(snapshotIDs) != 0 {
var (
id restic.ID
usedFilter bool
err error
)
ids := make(restic.IDs, 0, len(snapshotIDs))
// Process all snapshot IDs given as arguments.
for _, s := range snapshotIDs {
if s == "latest" {
id, err = restic.FindLatestSnapshot(repo, paths, tags, host)
if err != nil {
Warnf("Ignoring %q, no snapshot matched given filter (Paths:%v Tags:%v Host:%v)\n", s, paths, tags, host)
usedFilter = true
continue
}
} else {
id, err = restic.FindSnapshot(repo, s)
if err != nil {
Warnf("Ignoring %q, it is not a snapshot id\n", s)
continue
}
}
ids = append(ids, id)
}
// Give the user some indication their filters are not used.
if !usedFilter && (host != "" || len(tags) != 0 || len(paths) != 0) {
Warnf("Ignoring filters as there are explicit snapshot ids given\n")
}
for _, id := range ids.Uniq() {
sn, err := restic.LoadSnapshot(repo, id)
if err != nil {
Warnf("Ignoring %q, could not load snapshot: %v\n", id, err)
continue
}
select {
case <-ctx.Done():
return
case out <- sn:
}
}
return
}
for id := range repo.List(restic.SnapshotFile, ctx.Done()) {
sn, err := restic.LoadSnapshot(repo, id)
if err != nil {
Warnf("Ignoring %q, could not load snapshot: %v\n", id, err)
continue
}
if (host != "" && host != sn.Hostname) || !sn.HasTags(tags) || !sn.HasPaths(paths) {
continue
}
select {
case <-ctx.Done():
return
case out <- sn:
}
}
}()
return out
}

View file

@ -2,7 +2,11 @@ package main
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"time" "time"
"restic"
) )
func formatBytes(c uint64) string { func formatBytes(c uint64) string {
@ -58,3 +62,23 @@ func formatDuration(d time.Duration) string {
sec := uint64(d / time.Second) sec := uint64(d / time.Second)
return formatSeconds(sec) return formatSeconds(sec)
} }
func formatNode(prefix string, n *restic.Node, long bool) string {
if !long {
return filepath.Join(prefix, n.Name)
}
switch n.Type {
case "file":
return fmt.Sprintf("%s %5d %5d %6d %s %s",
n.Mode, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name))
case "dir":
return fmt.Sprintf("%s %5d %5d %6d %s %s",
n.Mode|os.ModeDir, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name))
case "symlink":
return fmt.Sprintf("%s %5d %5d %6d %s %s -> %s",
n.Mode|os.ModeSymlink, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name), n.LinkTarget)
default:
return fmt.Sprintf("<Node(%s) %s>", n.Type, n.Name)
}
}

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -33,6 +34,7 @@ type GlobalOptions struct {
NoLock bool NoLock bool
JSON bool JSON bool
ctx context.Context
password string password string
stdout io.Writer stdout io.Writer
stderr io.Writer stderr io.Writer
@ -49,6 +51,13 @@ func init() {
globalOptions.password = pw globalOptions.password = pw
} }
var cancel context.CancelFunc
globalOptions.ctx, cancel = context.WithCancel(context.Background())
AddCleanupHandler(func() error {
cancel()
return nil
})
f := cmdRoot.PersistentFlags() f := cmdRoot.PersistentFlags()
f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)") f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", "", "read the repository password from a file") f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", "", "read the repository password from a file")

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -194,6 +195,7 @@ func withTestEnvironment(t testing.TB, f func(*testEnvironment, GlobalOptions))
gopts := GlobalOptions{ gopts := GlobalOptions{
Repo: env.repo, Repo: env.repo,
Quiet: true, Quiet: true,
ctx: context.Background(),
password: TestPassword, password: TestPassword,
stdout: os.Stdout, stdout: os.Stdout,
stderr: os.Stderr, stderr: os.Stderr,

View file

@ -142,7 +142,9 @@ func testRunLs(t testing.TB, gopts GlobalOptions, snapshotID string) []string {
globalOptions.Quiet = quiet globalOptions.Quiet = quiet
}() }()
OK(t, runLs(gopts, []string{snapshotID})) opts := LsOptions{}
OK(t, runLs(opts, gopts, []string{snapshotID}))
return strings.Split(string(buf.Bytes()), "\n") return strings.Split(string(buf.Bytes()), "\n")
} }

View file

@ -32,6 +32,9 @@ var _ = fs.NodeStringLookuper(&SnapshotsDir{})
type SnapshotsDir struct { type SnapshotsDir struct {
repo restic.Repository repo restic.Repository
ownerIsRoot bool ownerIsRoot bool
paths []string
tags []string
host string
// knownSnapshots maps snapshot timestamp to the snapshot // knownSnapshots maps snapshot timestamp to the snapshot
sync.RWMutex sync.RWMutex
@ -40,12 +43,15 @@ type SnapshotsDir struct {
} }
// NewSnapshotsDir returns a new dir object for the snapshots. // NewSnapshotsDir returns a new dir object for the snapshots.
func NewSnapshotsDir(repo restic.Repository, ownerIsRoot bool) *SnapshotsDir { func NewSnapshotsDir(repo restic.Repository, ownerIsRoot bool, paths []string, tags []string, host string) *SnapshotsDir {
debug.Log("fuse mount initiated") debug.Log("fuse mount initiated")
return &SnapshotsDir{ return &SnapshotsDir{
repo: repo, repo: repo,
knownSnapshots: make(map[string]SnapshotWithId),
ownerIsRoot: ownerIsRoot, ownerIsRoot: ownerIsRoot,
paths: paths,
tags: tags,
host: host,
knownSnapshots: make(map[string]SnapshotWithId),
processed: restic.NewIDSet(), processed: restic.NewIDSet(),
} }
} }
@ -79,6 +85,13 @@ func (sn *SnapshotsDir) updateCache(ctx context.Context) error {
return err return err
} }
// Filter snapshots we don't care for.
if (sn.host != "" && sn.host != snapshot.Hostname) ||
!snapshot.HasTags(sn.tags) ||
!snapshot.HasPaths(sn.paths) {
continue
}
timestamp := snapshot.Time.Format(time.RFC3339) timestamp := snapshot.Time.Format(time.RFC3339)
for i := 1; ; i++ { for i := 1; ; i++ {
if _, ok := sn.knownSnapshots[timestamp]; !ok { if _, ok := sn.knownSnapshots[timestamp]; !ok {

View file

@ -177,8 +177,8 @@ func (sn *Snapshot) SamePaths(paths []string) bool {
// ErrNoSnapshotFound is returned when no snapshot for the given criteria could be found. // ErrNoSnapshotFound is returned when no snapshot for the given criteria could be found.
var ErrNoSnapshotFound = errors.New("no snapshot found") var ErrNoSnapshotFound = errors.New("no snapshot found")
// FindLatestSnapshot finds latest snapshot with optional target/directory and hostname filters. // FindLatestSnapshot finds latest snapshot with optional target/directory, tags and hostname filters.
func FindLatestSnapshot(repo Repository, targets []string, hostname string) (ID, error) { func FindLatestSnapshot(repo Repository, targets []string, tags []string, hostname string) (ID, error) {
var ( var (
latest time.Time latest time.Time
latestID ID latestID ID
@ -190,7 +190,7 @@ func FindLatestSnapshot(repo Repository, targets []string, hostname string) (ID,
if err != nil { if err != nil {
return ID{}, errors.Errorf("Error listing snapshot: %v", err) return ID{}, errors.Errorf("Error listing snapshot: %v", err)
} }
if snapshot.Time.After(latest) && snapshot.HasPaths(targets) && (hostname == "" || hostname == snapshot.Hostname) { if snapshot.Time.After(latest) && (hostname == "" || hostname == snapshot.Hostname) && snapshot.HasTags(tags) && snapshot.HasPaths(targets) {
latest = snapshot.Time latest = snapshot.Time
latestID = snapshotID latestID = snapshotID
found = true found = true