-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4410 from Enrico204/restic-stdin-command
add --stdin-from-command flag to backup command
- Loading branch information
Showing
7 changed files
with
305 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
Enhancement: Support reading backup from a program's standard output | ||
|
||
When reading data from stdin, the `backup` command could not verify whether the | ||
corresponding command completed successfully. | ||
|
||
The `backup` command now supports starting an arbitrary command and sourcing | ||
the backup content from its standard output. This enables restic to verify that | ||
the command completes with exit code zero. A non-zero exit code causes the | ||
backup to fail. | ||
|
||
Example: `restic backup --stdin-from-command mysqldump [...]` | ||
|
||
https://github.com/restic/restic/issues/4251 | ||
https://github.com/restic/restic/pull/4410 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
package fs | ||
|
||
import ( | ||
"bufio" | ||
"context" | ||
"fmt" | ||
"io" | ||
"os/exec" | ||
|
||
"github.com/restic/restic/internal/errors" | ||
) | ||
|
||
// CommandReader wrap a command such that its standard output can be read using | ||
// a io.ReadCloser. Close() waits for the command to terminate, reporting | ||
// any error back to the caller. | ||
type CommandReader struct { | ||
cmd *exec.Cmd | ||
stdout io.ReadCloser | ||
|
||
// cmd.Wait() must only be called once. Prevent duplicate executions in | ||
// Read() and Close(). | ||
waitHandled bool | ||
|
||
// alreadyClosedReadErr is the error that we should return if we try to | ||
// read the pipe again after closing. This works around a Read() call that | ||
// is issued after a previous Read() with `io.EOF` (but some bytes were | ||
// read in the past). | ||
alreadyClosedReadErr error | ||
} | ||
|
||
func NewCommandReader(ctx context.Context, args []string, logOutput io.Writer) (*CommandReader, error) { | ||
// Prepare command and stdout | ||
command := exec.CommandContext(ctx, args[0], args[1:]...) | ||
stdout, err := command.StdoutPipe() | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to setup stdout pipe: %w", err) | ||
} | ||
|
||
// Use a Go routine to handle the stderr to avoid deadlocks | ||
stderr, err := command.StderrPipe() | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to setup stderr pipe: %w", err) | ||
} | ||
go func() { | ||
sc := bufio.NewScanner(stderr) | ||
for sc.Scan() { | ||
_, _ = fmt.Fprintf(logOutput, "subprocess %v: %v\n", command.Args[0], sc.Text()) | ||
} | ||
}() | ||
|
||
if err := command.Start(); err != nil { | ||
return nil, fmt.Errorf("failed to start command: %w", err) | ||
} | ||
|
||
return &CommandReader{ | ||
cmd: command, | ||
stdout: stdout, | ||
}, nil | ||
} | ||
|
||
// Read populate the array with data from the process stdout. | ||
func (fp *CommandReader) Read(p []byte) (int, error) { | ||
if fp.alreadyClosedReadErr != nil { | ||
return 0, fp.alreadyClosedReadErr | ||
} | ||
b, err := fp.stdout.Read(p) | ||
|
||
// If the error is io.EOF, the program terminated. We need to check the | ||
// exit code here because, if the program terminated with no output, the | ||
// error in `Close()` is ignored. | ||
if errors.Is(err, io.EOF) { | ||
fp.waitHandled = true | ||
// check if the command terminated successfully, If not return the error. | ||
if errw := fp.wait(); errw != nil { | ||
err = errw | ||
} | ||
} | ||
fp.alreadyClosedReadErr = err | ||
return b, err | ||
} | ||
|
||
func (fp *CommandReader) wait() error { | ||
err := fp.cmd.Wait() | ||
if err != nil { | ||
// Use a fatal error to abort the snapshot. | ||
return errors.Fatal(fmt.Errorf("command failed: %w", err).Error()) | ||
} | ||
return nil | ||
} | ||
|
||
func (fp *CommandReader) Close() error { | ||
if fp.waitHandled { | ||
return nil | ||
} | ||
|
||
return fp.wait() | ||
} |
Oops, something went wrong.