Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Parse Kopia restore result #3011

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 62 additions & 2 deletions pkg/kopia/command/parse_command_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const (
//nolint:lll
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>[^\)]+)\%\).+)`
restoreOutputRegEx = `Processed (?P<processedCount>\d+) \((?P<processedSize>.*)\) of (?P<totalCount>\d+) \((?P<totalSize>.*)\) (?P<dataRate>.*) \((?P<percentage>.*)%\) remaining (?P<remainingTime>.*)\.`
restoreResultOutputRegEx = `Restored (?P<processedFilesCount>\d+) files, (?P<processedDirCount>\d+) directories and (?P<processedSymLinksCount>\d+) symbolic links \((?P<restoredSize>[^\)]+)\).`
extractSnapshotIDRegEx = `Created snapshot with root ([^\s]+) and ID ([^\s]+).*$`
repoTotalSizeFromBlobStatsRegEx = `Total: (\d+)$`
repoCountFromBlobStatsRegEx = `Count: (\d+)$`
Expand Down Expand Up @@ -207,8 +208,9 @@ type SnapshotCreateStats struct {
}

var (
kopiaProgressPattern = regexp.MustCompile(snapshotCreateOutputRegEx)
kopiaRestorePattern = regexp.MustCompile(restoreOutputRegEx)
kopiaProgressPattern = regexp.MustCompile(snapshotCreateOutputRegEx)
kopiaRestorePattern = regexp.MustCompile(restoreOutputRegEx)
kopiaRestoreResultPattern = regexp.MustCompile(restoreResultOutputRegEx)
)

// SnapshotStatsFromSnapshotCreate parses the output of a `kopia snapshot
Expand Down Expand Up @@ -430,6 +432,64 @@ func parseKopiaRestoreProgressLine(line string) (stats *RestoreStats) {
}
}

// RestoreResultFromRestoreOutput parses the output of a `kopia restore`
// line-by-line in search of restore result statistics.
// It returns nil if no final statistics are found.
func RestoreResultFromRestoreOutput(
restoreStderrOutput string,
) (stats *RestoreStats) {
if restoreStderrOutput == "" {
return nil
}
logs := regexp.MustCompile("[\r\n]").Split(restoreStderrOutput, -1)

for _, l := range logs {
lineStats := parseKopiaRestoreResultLine(l)
if lineStats != nil {
stats = lineStats
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand this condition will override stats until the last matching line even if there are more matching lines before that. Is that intended?

}
}

return stats
}

// parseKopiaRestoreResultLine parses final restore stats from the output log line,
// which is expected to be in the following format:
// Restored 1 files, 1 directories and 0 symbolic links (1.1 GB).
func parseKopiaRestoreResultLine(line string) (stats *RestoreStats) {
match := kopiaRestoreResultPattern.FindStringSubmatch(line)
if len(match) < 4 {
return nil
}

groups := make(map[string]string)
for i, name := range kopiaRestoreResultPattern.SubexpNames() {
if i != 0 && name != "" {
groups[name] = match[i]
}
}

processedFilesCount, err := strconv.Atoi(groups["processedFilesCount"])
if err != nil {
log.WithError(err).Print("Skipping entry due to inability to parse number of processed files", field.M{"processedFilesCount": groups["processedFilesCount"]})
return nil
}

restoredSize, err := humanize.ParseBytes(groups["restoredSize"])
if err != nil {
log.WithError(err).Print("Skipping entry due to inability to parse amount of restored bytes", field.M{"restoredSize": groups["restoredSize"]})
return nil
}

return &RestoreStats{
FilesProcessed: int64(processedFilesCount),
SizeProcessedB: int64(restoredSize),
FilesTotal: int64(processedFilesCount),
SizeTotalB: int64(restoredSize),
ProgressPercent: int64(100),
}
}

// RepoSizeStatsFromBlobStatsRaw takes a string as input, interprets it as a kopia blob stats
// output in an expected format (Contains the line "Total: <size>"), and returns the integer
// size in bytes or an error if parsing is unsuccessful.
Expand Down
43 changes: 43 additions & 0 deletions pkg/kopia/command/parse_command_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,49 @@ func (kParse *KopiaParseUtilsTestSuite) TestRestoreStatsFromRestoreOutput(c *C)
}
}

func (kParse *KopiaParseUtilsTestSuite) TestRestoreResultFromRestoreOutput(c *C) {
type args struct {
restoreOutput string
}
tests := []struct {
name string
args args
wantStats *RestoreStats
}{
{
name: "Basic test case",
args: args{
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).",
},
wantStats: &RestoreStats{
FilesProcessed: 1,
SizeProcessedB: 1100000000,
FilesTotal: 1,
SizeTotalB: 1100000000,
ProgressPercent: 100,
},
},
{
name: "Not finished test case",
args: args{
restoreOutput: "Processed 2 (13.7 MB) of 2 (3.1 GB) 8.5 MB/s (0.4%) remaining 6m10s.",
},
wantStats: nil,
},
{
name: "Ignore incomplete stats without during estimation",
args: args{
restoreOutput: "Processed 2 (32.8 KB) of 2 (3.1 GB).",
},
wantStats: nil,
},
}
for _, tt := range tests {
stats := RestoreResultFromRestoreOutput(tt.args.restoreOutput)
c.Check(stats, DeepEquals, tt.wantStats, Commentf("Failed for %s", tt.name))
}
}

func (kParse *KopiaParseUtilsTestSuite) TestPhysicalSizeFromBlobStatsRaw(c *C) {
for _, tc := range []struct {
blobStatsOutput string
Expand Down