Skip to content

Commit 4d35f63

Browse files
e-suminaaron-kasten
authored andcommitted
chore: Parse Kopia restore result (kanisterio#3011)
Signed-off-by: Daniil Fedotov <[email protected]> Signed-off-by: Aaron Alpar <[email protected]>
1 parent 1cb3a3f commit 4d35f63

File tree

2 files changed

+107
-2
lines changed

2 files changed

+107
-2
lines changed

pkg/kopia/command/parse_command_output.go

+64-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const (
4040
//nolint:lll
4141
snapshotCreateOutputRegEx = `(?P<spinner>[|/\-\\\*]).+[^\d](?P<numHashed>\d+) hashed \((?P<hashedSize>[^\)]+)\), (?P<numCached>\d+) cached \((?P<cachedSize>[^\)]+)\), uploaded (?P<uploadedSize>[^\)]+)(?: \([^)]*\))*, (?:estimating...|estimated (?P<estimatedSize>[^\)]+) \((?P<estimatedProgress>[^\)]+)\%\).+)`
4242
restoreOutputRegEx = `Processed (?P<processedCount>\d+) \((?P<processedSize>.*)\) of (?P<totalCount>\d+) \((?P<totalSize>.*)\) (?P<dataRate>.*) \((?P<percentage>.*)%\) remaining (?P<remainingTime>.*)\.`
43+
restoreResultOutputRegEx = `Restored (?P<processedFilesCount>\d+) files, (?P<processedDirCount>\d+) directories and (?P<processedSymLinksCount>\d+) symbolic links \((?P<restoredSize>[^\)]+)\).`
4344
extractSnapshotIDRegEx = `Created snapshot with root ([^\s]+) and ID ([^\s]+).*$`
4445
repoTotalSizeFromBlobStatsRegEx = `Total: (\d+)$`
4546
repoCountFromBlobStatsRegEx = `Count: (\d+)$`
@@ -207,8 +208,9 @@ type SnapshotCreateStats struct {
207208
}
208209

209210
var (
210-
kopiaProgressPattern = regexp.MustCompile(snapshotCreateOutputRegEx)
211-
kopiaRestorePattern = regexp.MustCompile(restoreOutputRegEx)
211+
kopiaProgressPattern = regexp.MustCompile(snapshotCreateOutputRegEx)
212+
kopiaRestorePattern = regexp.MustCompile(restoreOutputRegEx)
213+
kopiaRestoreResultPattern = regexp.MustCompile(restoreResultOutputRegEx)
212214
)
213215

214216
// SnapshotStatsFromSnapshotCreate parses the output of a `kopia snapshot
@@ -433,6 +435,66 @@ func parseKopiaRestoreProgressLine(line string) (stats *RestoreStats) {
433435
}
434436
}
435437

438+
// RestoreResultFromRestoreOutput parses the output of a `kopia restore`
439+
// line-by-line in search of restore result statistics.
440+
// It returns nil if no final statistics are found.
441+
func RestoreResultFromRestoreOutput(
442+
restoreStderrOutput string,
443+
) (stats *RestoreStats) {
444+
if restoreStderrOutput == "" {
445+
return nil
446+
}
447+
logs := regexp.MustCompile("[\r\n]").Split(restoreStderrOutput, -1)
448+
449+
for _, l := range logs {
450+
lineStats := parseKopiaRestoreResultLine(l)
451+
if lineStats != nil {
452+
// NOTE: overwriting result with the last matching line
453+
// even if there was a matching line before that
454+
stats = lineStats
455+
}
456+
}
457+
458+
return stats
459+
}
460+
461+
// parseKopiaRestoreResultLine parses final restore stats from the output log line,
462+
// which is expected to be in the following format:
463+
// Restored 1 files, 1 directories and 0 symbolic links (1.1 GB).
464+
func parseKopiaRestoreResultLine(line string) (stats *RestoreStats) {
465+
match := kopiaRestoreResultPattern.FindStringSubmatch(line)
466+
if len(match) < 4 {
467+
return nil
468+
}
469+
470+
groups := make(map[string]string)
471+
for i, name := range kopiaRestoreResultPattern.SubexpNames() {
472+
if i != 0 && name != "" {
473+
groups[name] = match[i]
474+
}
475+
}
476+
477+
processedFilesCount, err := strconv.Atoi(groups["processedFilesCount"])
478+
if err != nil {
479+
log.WithError(err).Print("Skipping entry due to inability to parse number of processed files", field.M{"processedFilesCount": groups["processedFilesCount"]})
480+
return nil
481+
}
482+
483+
restoredSize, err := humanize.ParseBytes(groups["restoredSize"])
484+
if err != nil {
485+
log.WithError(err).Print("Skipping entry due to inability to parse amount of restored bytes", field.M{"restoredSize": groups["restoredSize"]})
486+
return nil
487+
}
488+
489+
return &RestoreStats{
490+
FilesProcessed: int64(processedFilesCount),
491+
SizeProcessedB: int64(restoredSize),
492+
FilesTotal: int64(processedFilesCount),
493+
SizeTotalB: int64(restoredSize),
494+
ProgressPercent: int64(100),
495+
}
496+
}
497+
436498
// RepoSizeStatsFromBlobStatsRaw takes a string as input, interprets it as a kopia blob stats
437499
// output in an expected format (Contains the line "Total: <size>"), and returns the integer
438500
// size in bytes or an error if parsing is unsuccessful.

pkg/kopia/command/parse_command_output_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,49 @@ func (kParse *KopiaParseUtilsTestSuite) TestRestoreStatsFromRestoreOutput(c *che
513513
}
514514
}
515515

516+
func (kParse *KopiaParseUtilsTestSuite) TestRestoreResultFromRestoreOutput(c *check.C) {
517+
type args struct {
518+
restoreOutput string
519+
}
520+
tests := []struct {
521+
name string
522+
args args
523+
wantStats *RestoreStats
524+
}{
525+
{
526+
name: "Basic test case",
527+
args: args{
528+
restoreOutput: "Processed 2 (397.5 MB) of 3 (3.1 GB) 14.9 MB/s (12.6%) remaining 3m3s.\r\nRestored 1 files, 1 directories and 0 symbolic links (1.1 GB).",
529+
},
530+
wantStats: &RestoreStats{
531+
FilesProcessed: 1,
532+
SizeProcessedB: 1100000000,
533+
FilesTotal: 1,
534+
SizeTotalB: 1100000000,
535+
ProgressPercent: 100,
536+
},
537+
},
538+
{
539+
name: "Not finished test case",
540+
args: args{
541+
restoreOutput: "Processed 2 (13.7 MB) of 2 (3.1 GB) 8.5 MB/s (0.4%) remaining 6m10s.",
542+
},
543+
wantStats: nil,
544+
},
545+
{
546+
name: "Ignore incomplete stats without during estimation",
547+
args: args{
548+
restoreOutput: "Processed 2 (32.8 KB) of 2 (3.1 GB).",
549+
},
550+
wantStats: nil,
551+
},
552+
}
553+
for _, tt := range tests {
554+
stats := RestoreResultFromRestoreOutput(tt.args.restoreOutput)
555+
c.Check(stats, check.DeepEquals, tt.wantStats, check.Commentf("Failed for %s", tt.name))
556+
}
557+
}
558+
516559
func (kParse *KopiaParseUtilsTestSuite) TestPhysicalSizeFromBlobStatsRaw(c *check.C) {
517560
for _, tc := range []struct {
518561
blobStatsOutput string

0 commit comments

Comments
 (0)