@@ -40,6 +40,7 @@ const (
40
40
//nolint:lll
41
41
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>[^\)]+)\%\).+)`
42
42
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>[^\)]+)\).`
43
44
extractSnapshotIDRegEx = `Created snapshot with root ([^\s]+) and ID ([^\s]+).*$`
44
45
repoTotalSizeFromBlobStatsRegEx = `Total: (\d+)$`
45
46
repoCountFromBlobStatsRegEx = `Count: (\d+)$`
@@ -207,8 +208,9 @@ type SnapshotCreateStats struct {
207
208
}
208
209
209
210
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 )
212
214
)
213
215
214
216
// SnapshotStatsFromSnapshotCreate parses the output of a `kopia snapshot
@@ -433,6 +435,66 @@ func parseKopiaRestoreProgressLine(line string) (stats *RestoreStats) {
433
435
}
434
436
}
435
437
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
+
436
498
// RepoSizeStatsFromBlobStatsRaw takes a string as input, interprets it as a kopia blob stats
437
499
// output in an expected format (Contains the line "Total: <size>"), and returns the integer
438
500
// size in bytes or an error if parsing is unsuccessful.
0 commit comments