Skip to content

Commit

Permalink
fix: make backup and restore operations more robust to non-JSON outpu…
Browse files Browse the repository at this point in the history
…t events
  • Loading branch information
garethgeorge committed Jun 14, 2024
1 parent 5427d75 commit 91e0fda
Show file tree
Hide file tree
Showing 2 changed files with 17 additions and 48 deletions.
10 changes: 4 additions & 6 deletions pkg/restic/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"os/exec"
"strings"
)

const outputBufferLimit = 1000
Expand Down Expand Up @@ -61,13 +60,12 @@ func (e *ErrorWithOutput) Is(target error) bool {

// newErrorWithOutput creates a new error with the given output.
func newErrorWithOutput(err error, output string) error {
firstNewLine := strings.Index(output, "\n")
if firstNewLine > 0 {
output = output[:firstNewLine]
if output == "" {
return err
}

if len(output) == 0 {
return err
if len(output) > outputBufferLimit {
output = output[:outputBufferLimit] + fmt.Sprintf("\n... %d bytes truncated ...\n", len(output)-outputBufferLimit)
}

return &ErrorWithOutput{
Expand Down
55 changes: 13 additions & 42 deletions pkg/restic/outputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package restic

import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -96,34 +97,19 @@ func readBackupProgressEntries(output io.Reader, callback func(event *BackupProg
scanner := bufio.NewScanner(output)
scanner.Split(bufio.ScanLines)

var summary *BackupProgressEntry
nonJSONOutput := bytes.NewBuffer(nil)

// first event is handled specially to detect non-JSON output and fast-path out.
if scanner.Scan() {
var event BackupProgressEntry
if err := json.Unmarshal(scanner.Bytes(), &event); err != nil {
return nil, fmt.Errorf("command output was not JSON: %w", err)
}
if err := event.Validate(); err != nil {
return nil, err
}
if callback != nil {
callback(&event)
}
if event.MessageType == "summary" {
summary = &event
}
}
var summary *BackupProgressEntry

// remaining events are parsed as JSON
for scanner.Scan() {
var event BackupProgressEntry
if err := json.Unmarshal(scanner.Bytes(), &event); err != nil {
// skip it. This is a best-effort attempt to parse the output.
nonJSONOutput.Write(scanner.Bytes())
continue
}
if err := event.Validate(); err != nil {
// skip it. This is a best-effort attempt to parse the output.
nonJSONOutput.Write(scanner.Bytes())
continue
}
if callback != nil {
Expand All @@ -134,10 +120,10 @@ func readBackupProgressEntries(output io.Reader, callback func(event *BackupProg
}
}
if err := scanner.Err(); err != nil {
return summary, fmt.Errorf("scanner encountered error: %w", err)
return summary, newErrorWithOutput(err, nonJSONOutput.String())
}
if summary == nil {
return nil, fmt.Errorf("no summary event found")
return nil, newErrorWithOutput(errors.New("no summary event found"), nonJSONOutput.String())
}
return summary, nil
}
Expand Down Expand Up @@ -235,35 +221,20 @@ func readRestoreProgressEntries(output io.Reader, callback func(event *RestorePr
scanner := bufio.NewScanner(output)
scanner.Split(bufio.ScanLines)

var summary *RestoreProgressEntry

// first event is handled specially to detect non-JSON output and fast-path out.
if scanner.Scan() {
var event RestoreProgressEntry
nonJSONOutput := bytes.NewBuffer(nil)

if err := json.Unmarshal(scanner.Bytes(), &event); err != nil {
return nil, fmt.Errorf("command output was not JSON: %w", err)
}
if err := event.Validate(); err != nil {
return nil, err
}
if callback != nil {
callback(&event)
}
if event.MessageType == "summary" {
summary = &event
}
}
var summary *RestoreProgressEntry

// remaining events are parsed as JSON
for scanner.Scan() {
var event RestoreProgressEntry
if err := json.Unmarshal(scanner.Bytes(), &event); err != nil {
// skip it. Best effort parsing, restic will return with a non-zero exit code if it fails.
nonJSONOutput.Write(scanner.Bytes())
continue
}
if err := event.Validate(); err != nil {
// skip it. Best effort parsing, restic will return with a non-zero exit code if it fails.
nonJSONOutput.Write(scanner.Bytes())
continue
}

Expand All @@ -276,11 +247,11 @@ func readRestoreProgressEntries(output io.Reader, callback func(event *RestorePr
}

if err := scanner.Err(); err != nil {
return summary, fmt.Errorf("scanner encountered error: %w", err)
return summary, newErrorWithOutput(err, nonJSONOutput.String())
}

if summary == nil {
return nil, fmt.Errorf("no summary event found")
return nil, newErrorWithOutput(errors.New("no summary event found"), nonJSONOutput.String())
}

return summary, nil
Expand Down

0 comments on commit 91e0fda

Please sign in to comment.