Add exclude filter to archiver and 'backup' command
This commit is contained in:
parent
0d8bad273d
commit
7fd52f9f57
9 changed files with 179 additions and 36 deletions
19
archiver.go
19
archiver.go
|
@ -34,8 +34,8 @@ type Archiver struct {
|
|||
|
||||
blobToken chan struct{}
|
||||
|
||||
Error func(dir string, fi os.FileInfo, err error) error
|
||||
Filter func(item string, fi os.FileInfo) bool
|
||||
Error func(dir string, fi os.FileInfo, err error) error
|
||||
SelectFilter pipe.SelectFunc
|
||||
}
|
||||
|
||||
// NewArchiver returns a new archiver.
|
||||
|
@ -50,7 +50,7 @@ func NewArchiver(repo *repository.Repository) *Archiver {
|
|||
}
|
||||
|
||||
arch.Error = archiverAbortOnAllErrors
|
||||
arch.Filter = archiverAllowAllFiles
|
||||
arch.SelectFilter = archiverAllowAllFiles
|
||||
|
||||
return arch
|
||||
}
|
||||
|
@ -577,7 +577,7 @@ func (arch *Archiver) Snapshot(p *Progress, paths []string, parentID backend.ID)
|
|||
pipeCh := make(chan pipe.Job)
|
||||
resCh := make(chan pipe.Result, 1)
|
||||
go func() {
|
||||
err := pipe.Walk(paths, done, pipeCh, resCh)
|
||||
err := pipe.Walk(paths, arch.SelectFilter, done, pipeCh, resCh)
|
||||
if err != nil {
|
||||
debug.Log("Archiver.Snapshot", "pipe.Walk returned error %v", err)
|
||||
return
|
||||
|
@ -659,7 +659,7 @@ func isRegularFile(fi os.FileInfo) bool {
|
|||
|
||||
// Scan traverses the dirs to collect Stat information while emitting progress
|
||||
// information with p.
|
||||
func Scan(dirs []string, p *Progress) (Stat, error) {
|
||||
func Scan(dirs []string, filter pipe.SelectFunc, p *Progress) (Stat, error) {
|
||||
p.Start()
|
||||
defer p.Done()
|
||||
|
||||
|
@ -678,6 +678,15 @@ func Scan(dirs []string, p *Progress) (Stat, error) {
|
|||
fmt.Fprintf(os.Stderr, "error for %v: FileInfo is nil\n", str)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !filter(str, fi) {
|
||||
debug.Log("Scan.Walk", "path %v excluded", str)
|
||||
if fi.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
s := Stat{}
|
||||
if fi.IsDir() {
|
||||
s.Dirs++
|
||||
|
|
|
@ -10,13 +10,15 @@ import (
|
|||
|
||||
"github.com/restic/restic"
|
||||
"github.com/restic/restic/backend"
|
||||
"github.com/restic/restic/filter"
|
||||
"github.com/restic/restic/repository"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
type CmdBackup struct {
|
||||
Parent string `short:"p" long:"parent" description:"use this parent snapshot (default: last snapshot in repo that has the same target)"`
|
||||
Force bool `short:"f" long:"force" description:"Force re-reading the target. Overrides the \"parent\" flag"`
|
||||
Parent string `short:"p" long:"parent" description:"use this parent snapshot (default: last snapshot in repo that has the same target)"`
|
||||
Force bool `short:"f" long:"force" description:"Force re-reading the target. Overrides the \"parent\" flag"`
|
||||
Exclude []string `short:"e" long:"exclude" description:"Exclude a pattern (can be specified multiple times)"`
|
||||
|
||||
global *GlobalOptions
|
||||
}
|
||||
|
@ -282,14 +284,22 @@ func (cmd CmdBackup) Execute(args []string) error {
|
|||
|
||||
cmd.global.Verbosef("scan %v\n", target)
|
||||
|
||||
stat, err := restic.Scan(target, cmd.newScanProgress())
|
||||
selectFilter := func(item string, fi os.FileInfo) bool {
|
||||
matched, err := filter.List(cmd.Exclude, item)
|
||||
if err != nil {
|
||||
cmd.global.Warnf("error for exclude pattern: %v", err)
|
||||
}
|
||||
|
||||
// TODO: add filter
|
||||
// arch.Filter = func(dir string, fi os.FileInfo) bool {
|
||||
// return true
|
||||
// }
|
||||
return !matched
|
||||
}
|
||||
|
||||
stat, err := restic.Scan(target, selectFilter, cmd.newScanProgress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
arch := restic.NewArchiver(repo)
|
||||
arch.SelectFilter = selectFilter
|
||||
|
||||
arch.Error = func(dir string, fi os.FileInfo, err error) error {
|
||||
// TODO: make ignoring errors configurable
|
||||
|
|
|
@ -11,6 +11,8 @@ import (
|
|||
)
|
||||
|
||||
type CmdLs struct {
|
||||
Long bool `short:"l" long:"long" description:"Use a long listing format showing size and mode"`
|
||||
|
||||
global *GlobalOptions
|
||||
}
|
||||
|
||||
|
@ -24,7 +26,11 @@ func init() {
|
|||
}
|
||||
}
|
||||
|
||||
func printNode(prefix string, n *restic.Node) string {
|
||||
func (cmd CmdLs) printNode(prefix string, n *restic.Node) string {
|
||||
if !cmd.Long {
|
||||
return filepath.Join(prefix, n.Name)
|
||||
}
|
||||
|
||||
switch n.Type {
|
||||
case "file":
|
||||
return fmt.Sprintf("%s %5d %5d %6d %s %s",
|
||||
|
@ -40,17 +46,17 @@ func printNode(prefix string, n *restic.Node) string {
|
|||
}
|
||||
}
|
||||
|
||||
func printTree(prefix string, repo *repository.Repository, id backend.ID) error {
|
||||
func (cmd CmdLs) printTree(prefix string, repo *repository.Repository, id backend.ID) error {
|
||||
tree, err := restic.LoadTree(repo, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range tree.Nodes {
|
||||
fmt.Println(printNode(prefix, entry))
|
||||
cmd.global.Printf(cmd.printNode(prefix, entry) + "\n")
|
||||
|
||||
if entry.Type == "dir" && entry.Subtree != nil {
|
||||
err = printTree(filepath.Join(prefix, entry.Name), repo, entry.Subtree)
|
||||
err = cmd.printTree(filepath.Join(prefix, entry.Name), repo, entry.Subtree)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -89,7 +95,7 @@ func (cmd CmdLs) Execute(args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("snapshot of %v at %s:\n", sn.Paths, sn.Time)
|
||||
cmd.global.Verbosef("snapshot of %v at %s:\n", sn.Paths, sn.Time)
|
||||
|
||||
return printTree("", repo, sn.Tree)
|
||||
return cmd.printTree("", repo, sn.Tree)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -44,7 +45,11 @@ func cmdInit(t testing.TB, global GlobalOptions) {
|
|||
}
|
||||
|
||||
func cmdBackup(t testing.TB, global GlobalOptions, target []string, parentID backend.ID) {
|
||||
cmd := &CmdBackup{global: &global}
|
||||
cmdBackupExcludes(t, global, target, parentID, nil)
|
||||
}
|
||||
|
||||
func cmdBackupExcludes(t testing.TB, global GlobalOptions, target []string, parentID backend.ID, excludes []string) {
|
||||
cmd := &CmdBackup{global: &global, Exclude: excludes}
|
||||
cmd.Parent = parentID.String()
|
||||
|
||||
t.Logf("backing up %v", target)
|
||||
|
@ -73,6 +78,16 @@ func cmdCheck(t testing.TB, global GlobalOptions) {
|
|||
OK(t, cmd.Execute(nil))
|
||||
}
|
||||
|
||||
func cmdLs(t testing.TB, global GlobalOptions, snapshotID string) []string {
|
||||
var buf bytes.Buffer
|
||||
global.stdout = &buf
|
||||
|
||||
cmd := &CmdLs{global: &global}
|
||||
OK(t, cmd.Execute([]string{snapshotID}))
|
||||
|
||||
return strings.Split(string(buf.Bytes()), "\n")
|
||||
}
|
||||
|
||||
func TestBackup(t *testing.T) {
|
||||
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) {
|
||||
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
||||
|
@ -237,6 +252,86 @@ func TestBackupMissingFile2(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func includes(haystack []string, needle string) bool {
|
||||
for _, s := range haystack {
|
||||
if s == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func loadSnapshotMap(t testing.TB, global GlobalOptions) map[string]struct{} {
|
||||
snapshotIDs := cmdList(t, global, "snapshots")
|
||||
|
||||
m := make(map[string]struct{})
|
||||
for _, id := range snapshotIDs {
|
||||
m[id.String()] = struct{}{}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func lastSnapshot(old, new map[string]struct{}) (map[string]struct{}, string) {
|
||||
for k := range new {
|
||||
if _, ok := old[k]; !ok {
|
||||
old[k] = struct{}{}
|
||||
return old, k
|
||||
}
|
||||
}
|
||||
|
||||
return old, ""
|
||||
}
|
||||
|
||||
var backupExcludeFilenames = []string{
|
||||
"testfile1",
|
||||
"foo.tar.gz",
|
||||
"private/secret/passwords.txt",
|
||||
"work/source/test.c",
|
||||
}
|
||||
|
||||
func TestBackupExclude(t *testing.T) {
|
||||
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) {
|
||||
cmdInit(t, global)
|
||||
|
||||
datadir := filepath.Join(env.base, "testdata")
|
||||
|
||||
for _, filename := range backupExcludeFilenames {
|
||||
fp := filepath.Join(datadir, filename)
|
||||
OK(t, os.MkdirAll(filepath.Dir(fp), 0755))
|
||||
|
||||
f, err := os.Create(fp)
|
||||
OK(t, err)
|
||||
|
||||
fmt.Fprintf(f, filename)
|
||||
OK(t, f.Close())
|
||||
}
|
||||
|
||||
snapshots := make(map[string]struct{})
|
||||
|
||||
cmdBackup(t, global, []string{datadir}, nil)
|
||||
snapshots, snapshotID := lastSnapshot(snapshots, loadSnapshotMap(t, global))
|
||||
files := cmdLs(t, global, snapshotID)
|
||||
Assert(t, includes(files, filepath.Join("testdata", "foo.tar.gz")),
|
||||
"expected file %q in first snapshot, but it's not included", "foo.tar.gz")
|
||||
|
||||
cmdBackupExcludes(t, global, []string{datadir}, nil, []string{"*.tar.gz"})
|
||||
snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, global))
|
||||
files = cmdLs(t, global, snapshotID)
|
||||
Assert(t, !includes(files, filepath.Join("testdata", "foo.tar.gz")),
|
||||
"expected file %q not in first snapshot, but it's included", "foo.tar.gz")
|
||||
|
||||
cmdBackupExcludes(t, global, []string{datadir}, nil, []string{"*.tar.gz", "private/secret"})
|
||||
snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, global))
|
||||
files = cmdLs(t, global, snapshotID)
|
||||
Assert(t, !includes(files, filepath.Join("testdata", "foo.tar.gz")),
|
||||
"expected file %q not in first snapshot, but it's included", "foo.tar.gz")
|
||||
Assert(t, !includes(files, filepath.Join("testdata", "private", "secret", "passwords.txt")),
|
||||
"expected file %q not in first snapshot, but it's included", "passwords.txt")
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
incrementalFirstWrite = 20 * 1042 * 1024
|
||||
incrementalSecondWrite = 12 * 1042 * 1024
|
||||
|
|
|
@ -91,8 +91,8 @@ func match(patterns, strs []string) (matched bool, err error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// MatchList returns true if str matches one of the patterns.
|
||||
func MatchList(patterns []string, str string) (matched bool, err error) {
|
||||
// List returns true if str matches one of the patterns.
|
||||
func List(patterns []string, str string) (matched bool, err error) {
|
||||
for _, pat := range patterns {
|
||||
matched, err = Match(pat, str)
|
||||
if err != nil {
|
||||
|
|
|
@ -121,7 +121,7 @@ var filterListTests = []struct {
|
|||
|
||||
func TestMatchList(t *testing.T) {
|
||||
for i, test := range filterListTests {
|
||||
match, err := filter.MatchList(test.patterns, test.path)
|
||||
match, err := filter.List(test.patterns, test.path)
|
||||
if err != nil {
|
||||
t.Errorf("test %d failed: expected no error for patterns %q, but error returned: %v",
|
||||
i, test.patterns, err)
|
||||
|
@ -136,7 +136,7 @@ func TestMatchList(t *testing.T) {
|
|||
}
|
||||
|
||||
func ExampleMatchList() {
|
||||
match, _ := filter.MatchList([]string{"*.c", "*.go"}, "/home/user/file.go")
|
||||
match, _ := filter.List([]string{"*.c", "*.go"}, "/home/user/file.go")
|
||||
fmt.Printf("match: %v\n", match)
|
||||
// Output:
|
||||
// match: true
|
||||
|
@ -235,7 +235,7 @@ func BenchmarkFilterPatterns(b *testing.B) {
|
|||
for i := 0; i < b.N; i++ {
|
||||
c = 0
|
||||
for _, line := range lines {
|
||||
match, err := filter.MatchList(patterns, line)
|
||||
match, err := filter.List(patterns, line)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
|
28
pipe/pipe.go
28
pipe/pipe.go
|
@ -82,13 +82,22 @@ func isFile(fi os.FileInfo) bool {
|
|||
|
||||
var errCancelled = errors.New("walk cancelled")
|
||||
|
||||
func walk(basedir, dir string, done chan struct{}, jobs chan<- Job, res chan<- Result) error {
|
||||
// SelectFunc returns true for all items that should be included (files and
|
||||
// dirs). If false is returned, files are ignored and dirs are not even walked.
|
||||
type SelectFunc func(item string, fi os.FileInfo) bool
|
||||
|
||||
func walk(basedir, dir string, selectFunc SelectFunc, done chan struct{}, jobs chan<- Job, res chan<- Result) error {
|
||||
info, err := os.Lstat(dir)
|
||||
if err != nil {
|
||||
debug.Log("pipe.walk", "error for %v: %v", dir, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !selectFunc(dir, info) {
|
||||
debug.Log("pipe.walk", "file %v excluded by filter", dir)
|
||||
return nil
|
||||
}
|
||||
|
||||
relpath, _ := filepath.Rel(basedir, dir)
|
||||
|
||||
if !info.IsDir() {
|
||||
|
@ -114,13 +123,18 @@ func walk(basedir, dir string, done chan struct{}, jobs chan<- Job, res chan<- R
|
|||
for _, name := range names {
|
||||
subpath := filepath.Join(dir, name)
|
||||
|
||||
fi, statErr := os.Lstat(subpath)
|
||||
if !selectFunc(subpath, fi) {
|
||||
debug.Log("pipe.walk", "file %v excluded by filter", subpath)
|
||||
continue
|
||||
}
|
||||
|
||||
ch := make(chan Result, 1)
|
||||
entries = append(entries, ch)
|
||||
|
||||
fi, err := os.Lstat(subpath)
|
||||
if err != nil {
|
||||
if statErr != nil {
|
||||
select {
|
||||
case jobs <- Entry{info: fi, error: err, basedir: basedir, path: filepath.Join(relpath, name), result: ch}:
|
||||
case jobs <- Entry{info: fi, error: statErr, basedir: basedir, path: filepath.Join(relpath, name), result: ch}:
|
||||
case <-done:
|
||||
return errCancelled
|
||||
}
|
||||
|
@ -132,7 +146,7 @@ func walk(basedir, dir string, done chan struct{}, jobs chan<- Job, res chan<- R
|
|||
debug.RunHook("pipe.walk2", filepath.Join(relpath, name))
|
||||
|
||||
if isDir(fi) {
|
||||
err = walk(basedir, subpath, done, jobs, ch)
|
||||
err = walk(basedir, subpath, selectFunc, done, jobs, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -156,7 +170,7 @@ func walk(basedir, dir string, done chan struct{}, jobs chan<- Job, res chan<- R
|
|||
|
||||
// Walk sends a Job for each file and directory it finds below the paths. When
|
||||
// the channel done is closed, processing stops.
|
||||
func Walk(paths []string, done chan struct{}, jobs chan<- Job, res chan<- Result) error {
|
||||
func Walk(paths []string, selectFunc SelectFunc, done chan struct{}, jobs chan<- Job, res chan<- Result) error {
|
||||
defer func() {
|
||||
debug.Log("pipe.Walk", "output channel closed")
|
||||
close(jobs)
|
||||
|
@ -166,7 +180,7 @@ func Walk(paths []string, done chan struct{}, jobs chan<- Job, res chan<- Result
|
|||
for _, path := range paths {
|
||||
debug.Log("pipe.Walk", "start walker for %v", path)
|
||||
ch := make(chan Result, 1)
|
||||
err := walk(filepath.Dir(path), path, done, jobs, ch)
|
||||
err := walk(filepath.Dir(path), path, selectFunc, done, jobs, ch)
|
||||
if err != nil {
|
||||
debug.Log("pipe.Walk", "error for %v: %v", path, err)
|
||||
continue
|
||||
|
|
|
@ -19,6 +19,10 @@ type stats struct {
|
|||
dirs, files int
|
||||
}
|
||||
|
||||
func acceptAll(string, os.FileInfo) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func statPath(path string) (stats, error) {
|
||||
var s stats
|
||||
|
||||
|
@ -118,7 +122,7 @@ func TestPipelineWalkerWithSplit(t *testing.T) {
|
|||
}()
|
||||
|
||||
resCh := make(chan pipe.Result, 1)
|
||||
err = pipe.Walk([]string{TestWalkerPath}, done, jobs, resCh)
|
||||
err = pipe.Walk([]string{TestWalkerPath}, acceptAll, done, jobs, resCh)
|
||||
OK(t, err)
|
||||
|
||||
// wait for all workers to terminate
|
||||
|
@ -198,7 +202,7 @@ func TestPipelineWalker(t *testing.T) {
|
|||
}
|
||||
|
||||
resCh := make(chan pipe.Result, 1)
|
||||
err = pipe.Walk([]string{TestWalkerPath}, done, jobs, resCh)
|
||||
err = pipe.Walk([]string{TestWalkerPath}, acceptAll, done, jobs, resCh)
|
||||
OK(t, err)
|
||||
|
||||
// wait for all workers to terminate
|
||||
|
@ -298,7 +302,7 @@ func BenchmarkPipelineWalker(b *testing.B) {
|
|||
}()
|
||||
|
||||
resCh := make(chan pipe.Result, 1)
|
||||
err := pipe.Walk([]string{TestWalkerPath}, done, jobs, resCh)
|
||||
err := pipe.Walk([]string{TestWalkerPath}, acceptAll, done, jobs, resCh)
|
||||
OK(b, err)
|
||||
|
||||
// wait for all workers to terminate
|
||||
|
@ -375,7 +379,7 @@ func TestPipelineWalkerMultiple(t *testing.T) {
|
|||
}
|
||||
|
||||
resCh := make(chan pipe.Result, 1)
|
||||
err = pipe.Walk(paths, done, jobs, resCh)
|
||||
err = pipe.Walk(paths, acceptAll, done, jobs, resCh)
|
||||
OK(t, err)
|
||||
|
||||
// wait for all workers to terminate
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package restic_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
|
@ -33,7 +34,11 @@ func TestWalkTree(t *testing.T) {
|
|||
// start filesystem walker
|
||||
fsJobs := make(chan pipe.Job)
|
||||
resCh := make(chan pipe.Result, 1)
|
||||
go pipe.Walk(dirs, done, fsJobs, resCh)
|
||||
|
||||
f := func(string, os.FileInfo) bool {
|
||||
return true
|
||||
}
|
||||
go pipe.Walk(dirs, f, done, fsJobs, resCh)
|
||||
|
||||
for {
|
||||
// receive fs job
|
||||
|
|
Loading…
Reference in a new issue