forked from TrueCloudLab/restic
Refactor output of find
to allow for json and normal output.
Rather complicated solution becaused I wanted to retain the streaming character of the output, which means for json I have to manually add headers and footers per snapshot scanned + a list around the whole set. As the json ouput is now partly handcrafted, add proper testing to catch unintentional changes to the output, making it non-json compliant. Closes #869
This commit is contained in:
parent
8a05de537f
commit
a9707a5728
2 changed files with 154 additions and 19 deletions
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -84,7 +85,94 @@ 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, prefix string, snapshotID *string) error {
|
type statefulOutput struct {
|
||||||
|
ListLong bool
|
||||||
|
JSON bool
|
||||||
|
inuse bool
|
||||||
|
newsn *restic.Snapshot
|
||||||
|
oldsn *restic.Snapshot
|
||||||
|
hits int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *statefulOutput) PrintJSON(prefix string, node *restic.Node) {
|
||||||
|
type findNode restic.Node
|
||||||
|
b, err := json.Marshal(struct {
|
||||||
|
// Add these attributes
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
Permissions string `json:"permissions,omitempty"`
|
||||||
|
|
||||||
|
*findNode
|
||||||
|
|
||||||
|
// Make the following attributes disappear
|
||||||
|
Name byte `json:"name,omitempty"`
|
||||||
|
Inode byte `json:"inode,omitempty"`
|
||||||
|
ExtendedAttributes byte `json:"extended_attributes,omitempty"`
|
||||||
|
Device byte `json:"device,omitempty"`
|
||||||
|
Content byte `json:"content,omitempty"`
|
||||||
|
Subtree byte `json:"subtree,omitempty"`
|
||||||
|
}{
|
||||||
|
Path: filepath.Join(prefix, node.Name),
|
||||||
|
Permissions: node.Mode.String(),
|
||||||
|
findNode: (*findNode)(node),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
Warnf("Marshall failed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !s.inuse {
|
||||||
|
Printf("[")
|
||||||
|
s.inuse = true
|
||||||
|
}
|
||||||
|
if s.newsn != s.oldsn {
|
||||||
|
if s.oldsn != nil {
|
||||||
|
Printf("],\"hits\":%d,\"snapshot\":%q},", s.hits, s.oldsn.ID())
|
||||||
|
}
|
||||||
|
Printf(`{"matches":[`)
|
||||||
|
s.oldsn = s.newsn
|
||||||
|
s.hits = 0
|
||||||
|
}
|
||||||
|
if s.hits > 0 {
|
||||||
|
Printf(",")
|
||||||
|
}
|
||||||
|
Printf(string(b))
|
||||||
|
s.hits++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *statefulOutput) PrintNormal(prefix string, node *restic.Node) {
|
||||||
|
if s.newsn != s.oldsn {
|
||||||
|
if s.oldsn != nil {
|
||||||
|
Verbosef("\n")
|
||||||
|
}
|
||||||
|
s.oldsn = s.newsn
|
||||||
|
Verbosef("Found matching entries in snapshot %s\n", s.oldsn.ID())
|
||||||
|
}
|
||||||
|
Printf(formatNode(prefix, node, s.ListLong) + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *statefulOutput) Print(prefix string, node *restic.Node) {
|
||||||
|
if s.JSON {
|
||||||
|
s.PrintJSON(prefix, node)
|
||||||
|
} else {
|
||||||
|
s.PrintNormal(prefix, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *statefulOutput) Finish() {
|
||||||
|
if s.JSON {
|
||||||
|
// do some finishing up
|
||||||
|
if s.oldsn != nil {
|
||||||
|
Printf("],\"hits\":%d,\"snapshot\":%q}", s.hits, s.oldsn.ID())
|
||||||
|
}
|
||||||
|
if s.inuse {
|
||||||
|
Printf("]\n")
|
||||||
|
} else {
|
||||||
|
Printf("[]\n")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findInTree(repo *repository.Repository, pat *findPattern, id restic.ID, prefix string, state *statefulOutput) 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)
|
||||||
|
@ -117,17 +205,13 @@ func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, pref
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if snapshotID != nil {
|
state.Print(prefix, node)
|
||||||
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" {
|
||||||
if err := findInTree(repo, pat, *node.Subtree, filepath.Join(prefix, node.Name), snapshotID); err != nil {
|
if err := findInTree(repo, pat, *node.Subtree, filepath.Join(prefix, node.Name), state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,11 +220,11 @@ func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, pref
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findInSnapshot(repo *repository.Repository, sn *restic.Snapshot, pat findPattern) error {
|
func findInSnapshot(repo *repository.Repository, sn *restic.Snapshot, pat findPattern, state *statefulOutput) error {
|
||||||
debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), pat.oldest, pat.newest)
|
debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), pat.oldest, pat.newest)
|
||||||
|
|
||||||
snapshotID := sn.ID().Str()
|
state.newsn = sn
|
||||||
if err := findInTree(repo, pat, *sn.Tree, string(filepath.Separator), &snapshotID); err != nil {
|
if err := findInTree(repo, &pat, *sn.Tree, string(filepath.Separator), state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -189,11 +273,13 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
state := statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON}
|
||||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) {
|
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) {
|
||||||
if err = findInSnapshot(repo, sn, pat); err != nil {
|
if err = findInSnapshot(repo, sn, pat, &state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
state.Finish()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,18 +149,20 @@ func testRunLs(t testing.TB, gopts GlobalOptions, snapshotID string) []string {
|
||||||
return strings.Split(string(buf.Bytes()), "\n")
|
return strings.Split(string(buf.Bytes()), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunFind(t testing.TB, gopts GlobalOptions, pattern string) []string {
|
func testRunFind(t testing.TB, wantJSON bool, gopts GlobalOptions, pattern string) []byte {
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
globalOptions.stdout = buf
|
globalOptions.stdout = buf
|
||||||
|
globalOptions.JSON = wantJSON
|
||||||
defer func() {
|
defer func() {
|
||||||
globalOptions.stdout = os.Stdout
|
globalOptions.stdout = os.Stdout
|
||||||
|
globalOptions.JSON = false
|
||||||
}()
|
}()
|
||||||
|
|
||||||
opts := FindOptions{}
|
opts := FindOptions{}
|
||||||
|
|
||||||
OK(t, runFind(opts, gopts, []string{pattern}))
|
OK(t, runFind(opts, gopts, []string{pattern}))
|
||||||
|
|
||||||
return strings.Split(string(buf.Bytes()), "\n")
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunSnapshots(t testing.TB, gopts GlobalOptions) (newest *Snapshot, snapmap map[restic.ID]Snapshot) {
|
func testRunSnapshots(t testing.TB, gopts GlobalOptions) (newest *Snapshot, snapmap map[restic.ID]Snapshot) {
|
||||||
|
@ -1037,14 +1039,61 @@ func TestFind(t *testing.T) {
|
||||||
testRunBackup(t, []string{env.testdata}, opts, gopts)
|
testRunBackup(t, []string{env.testdata}, opts, gopts)
|
||||||
testRunCheck(t, gopts)
|
testRunCheck(t, gopts)
|
||||||
|
|
||||||
results := testRunFind(t, gopts, "unexistingfile")
|
results := testRunFind(t, false, gopts, "unexistingfile")
|
||||||
Assert(t, len(results) != 0, "unexisting file found in repo (%v)", datafile)
|
Assert(t, len(results) == 0, "unexisting file found in repo (%v)", datafile)
|
||||||
|
|
||||||
results = testRunFind(t, gopts, "testfile")
|
results = testRunFind(t, false, gopts, "testfile")
|
||||||
Assert(t, len(results) != 1, "file not found in repo (%v)", datafile)
|
lines := strings.Split(string(results), "\n")
|
||||||
|
Assert(t, len(lines) == 2, "expected one file found in repo (%v)", datafile)
|
||||||
|
|
||||||
results = testRunFind(t, gopts, "test")
|
results = testRunFind(t, false, gopts, "testfile*")
|
||||||
Assert(t, len(results) < 2, "less than two file found in repo (%v)", datafile)
|
lines = strings.Split(string(results), "\n")
|
||||||
|
Assert(t, len(lines) == 4, "expected three files found in repo (%v)", datafile)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type testMatch struct {
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
Permissions string `json:"permissions,omitempty"`
|
||||||
|
Size uint64 `json:"size,omitempty"`
|
||||||
|
Date time.Time `json:"date,omitempty"`
|
||||||
|
UID uint32 `json:"uid,omitempty"`
|
||||||
|
GID uint32 `json:"gid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type testMatches struct {
|
||||||
|
Hits int `json:"hits,omitempty"`
|
||||||
|
SnapshotID string `json:"snapshot,omitempty"`
|
||||||
|
Matches []testMatch `json:"matches,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindJSON(t *testing.T) {
|
||||||
|
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
|
||||||
|
datafile := filepath.Join("testdata", "backup-data.tar.gz")
|
||||||
|
testRunInit(t, gopts)
|
||||||
|
SetupTarTestFixture(t, env.testdata, datafile)
|
||||||
|
|
||||||
|
opts := BackupOptions{}
|
||||||
|
|
||||||
|
testRunBackup(t, []string{env.testdata}, opts, gopts)
|
||||||
|
testRunCheck(t, gopts)
|
||||||
|
|
||||||
|
results := testRunFind(t, true, gopts, "unexistingfile")
|
||||||
|
matches := []testMatches{}
|
||||||
|
OK(t, json.Unmarshal(results, &matches))
|
||||||
|
Assert(t, len(matches) == 0, "expected no match in repo (%v)", datafile)
|
||||||
|
|
||||||
|
results = testRunFind(t, true, gopts, "testfile")
|
||||||
|
OK(t, json.Unmarshal(results, &matches))
|
||||||
|
Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
|
||||||
|
Assert(t, len(matches[0].Matches) == 1, "expected a single file to match (%v)", datafile)
|
||||||
|
Assert(t, matches[0].Hits == 1, "expected hits to show 1 match (%v)", datafile)
|
||||||
|
|
||||||
|
results = testRunFind(t, true, gopts, "testfile*")
|
||||||
|
OK(t, json.Unmarshal(results, &matches))
|
||||||
|
Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
|
||||||
|
Assert(t, len(matches[0].Matches) == 3, "expected 3 files to match (%v)", datafile)
|
||||||
|
Assert(t, matches[0].Hits == 3, "expected hits to show 3 matches (%v)", datafile)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue