-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
324 additions
and
10 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
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,143 @@ | ||
package destinations | ||
|
||
import ( | ||
"github.com/sloonz/uback/lib" | ||
|
||
"bytes" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"os" | ||
"os/exec" | ||
"strings" | ||
|
||
"github.com/gobuffalo/flect" | ||
"github.com/sirupsen/logrus" | ||
) | ||
|
||
var ( | ||
ErrCommandMissing = errors.New("command destination: missing command") | ||
commandLog = logrus.WithFields(logrus.Fields{ | ||
"destination": "fs", | ||
}) | ||
) | ||
|
||
type commandDestination struct { | ||
options *uback.Options | ||
command string | ||
env []string | ||
} | ||
|
||
func newCommandDestination(options *uback.Options) (uback.Destination, error) { | ||
command := options.String["Command"] | ||
if command == "" { | ||
return nil, ErrCommandMissing | ||
} | ||
|
||
env := os.Environ() | ||
for k, v := range options.String { | ||
env = append(env, fmt.Sprintf("UBACK_OPT_%s=%s", flect.New(k).Underscore().ToUpper().String(), v)) | ||
} | ||
for k, v := range options.StrSlice { | ||
jsonVal, err := json.Marshal(v) | ||
if err != nil { | ||
return nil, err | ||
} | ||
env = append(env, fmt.Sprintf("UBACK_SOPT_%s=%s", flect.New(k).Underscore().ToUpper().String(), string(jsonVal))) | ||
} | ||
|
||
buf := bytes.NewBuffer(nil) | ||
cmd := exec.Command(command, "destination", "validate-options") | ||
cmd.Stdout = buf | ||
cmd.Stderr = os.Stderr | ||
cmd.Env = env | ||
err := cmd.Run() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &commandDestination{options: options, command: command, env: env}, nil | ||
} | ||
|
||
func (d *commandDestination) ListBackups() ([]uback.Backup, error) { | ||
var res []uback.Backup | ||
|
||
buf := bytes.NewBuffer(nil) | ||
cmd := exec.Command(d.command, "destination", "list-backups") | ||
cmd.Stdout = buf | ||
cmd.Stderr = os.Stderr | ||
cmd.Env = d.env | ||
err := cmd.Run() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
for { | ||
entry, err := buf.ReadString('\n') | ||
if err == io.EOF { | ||
break | ||
} else if err != nil { | ||
return nil, err | ||
} | ||
|
||
entry = strings.TrimSpace(entry) | ||
if entry == "" { | ||
continue | ||
} | ||
|
||
if strings.HasPrefix(entry, ".") || strings.HasPrefix(entry, "_") { | ||
continue | ||
} | ||
|
||
backup, err := uback.ParseBackupFilename(entry, false) | ||
if err != nil { | ||
commandLog.WithFields(logrus.Fields{ | ||
"entry": entry, | ||
}) | ||
logrus.Warnf("invalid backup file: %v", err) | ||
continue | ||
} | ||
|
||
res = append(res, backup) | ||
} | ||
|
||
return res, nil | ||
} | ||
|
||
func (d *commandDestination) RemoveBackup(backup uback.Backup) error { | ||
cmd := exec.Command(d.command, "destination", "remove-backup", backup.FullName()) | ||
cmd.Stdout = os.Stderr | ||
cmd.Stderr = os.Stderr | ||
cmd.Env = d.env | ||
return cmd.Run() | ||
} | ||
|
||
func (d *commandDestination) SendBackup(backup uback.Backup, data io.Reader) error { | ||
cmd := exec.Command(d.command, "destination", "send-backup", backup.FullName()) | ||
cmd.Stdin = data | ||
cmd.Stdout = os.Stderr | ||
cmd.Stderr = os.Stderr | ||
cmd.Env = d.env | ||
return cmd.Run() | ||
} | ||
|
||
func (d *commandDestination) ReceiveBackup(backup uback.Backup) (io.ReadCloser, error) { | ||
pr, pw := io.Pipe() | ||
cmd := exec.Command(d.command, "destination", "receive-backup", backup.FullName()) | ||
cmd.Stdout = pw | ||
cmd.Stderr = os.Stderr | ||
cmd.Env = d.env | ||
|
||
commandLog.Printf("running: %v", cmd.String()) | ||
err := cmd.Start() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
go func() { | ||
pw.CloseWithError(cmd.Wait()) | ||
}() | ||
|
||
return pr, nil | ||
} |
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,85 @@ | ||
# Custom Destinations | ||
|
||
Custom destinations are destintations that are not build-in into `uback`, | ||
but implemented by an external command, typicall a script, under the | ||
control of the user. | ||
|
||
## Custom Destinations for Users | ||
|
||
When creating a backup, you give `command` as the destination type, | ||
and set the `Command` option to the custom source command. It can be | ||
either a full path or relative path if the custom destination command | ||
in in the PATH. | ||
|
||
```shell | ||
$ uback backup $SOURCE_OPTIONS id=test,type=custom,command=uback-fs-dest | ||
``` | ||
|
||
## Custom Destinations for Implementors | ||
|
||
The first argument passed to the command is the kind of command: `source` | ||
for a source, and `destination` for a destination. In the context of | ||
this document, the first argument will always be `destination`. | ||
|
||
`uback` passes the requested operation and arguments as the next arguments | ||
on the command line, and options passed to the destination as environment | ||
variable. For example, the `SnapshotsPath` will be translated into the | ||
`UBACK_OPT_SNAPSHOTS_PATH` environment variable. If the option is a | ||
slice option (for example `@AdditionalArguments`), it will be passed into | ||
`UBACK_SOPT_ADDITIONAL_ARGUMENTS`, and the value will be a JSON-serialized | ||
string array (the `S` in `SOPT` stands for "slice"). | ||
|
||
A custom destination command should follow the usual external process | ||
conventions : use stdout for normal output that will be consumed by | ||
`uback`, use stdin for normal input that will be given by `uback`, | ||
use stderr to print free-format messages destined to the end user. An | ||
exit code of 0 indicates a successful operation, whereas a non-zero one | ||
indicates a failure. | ||
|
||
A destination must implement those operations, which will be described in | ||
the next sections : | ||
|
||
* validate-options | ||
* list-backups | ||
* remove-backup | ||
* send-backup | ||
* receive-backup | ||
|
||
## Operations | ||
|
||
### validate-options | ||
|
||
This operation takes no argument ; it is called before any other operation | ||
and should be used to validate options. | ||
|
||
### list-backups | ||
|
||
This operation takes no argument, and should print all backups currently | ||
present on the destination, one per line. You can either | ||
give a backup name (`20210102T000000.000-full`) or a filename | ||
(`20210102T000000.000-full.ubkp`). | ||
|
||
For convenience purposes, lines starting with a `.` or `_` are ignored. | ||
|
||
### remove-backup | ||
|
||
This operation takes one argument, the full | ||
name of a backup (`20210102T000000.000-full` or | ||
`20210102T000000.000-from-20210101T000000.000`), and should remove the | ||
backup on the destination. | ||
|
||
### send-backup | ||
|
||
This operation takes one argument, the full name of a backup. `uback` | ||
will provide the backup to the command standard input ; the command | ||
should store it. | ||
|
||
### receive-backup | ||
|
||
This operation takes one argument, the full name of a backup. The command | ||
should output the backup on its standard output. | ||
|
||
## Example | ||
|
||
As an example, you can look at the [uback-fs-test](../tests/uback-fs-test) | ||
test script, which reimplement the fs destination as a bash script. |
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,42 @@ | ||
load "helpers/bats-support/load" | ||
load "helpers/bats-assert/load" | ||
|
||
UBACK="$BATS_TEST_DIRNAME/../uback" | ||
TEST_TMPDIR="$BATS_RUN_TMPDIR/dest-command" | ||
|
||
@test "command destination" { | ||
export PATH="$PATH:$BATS_TEST_DIRNAME" | ||
mkdir -p "$TEST_TMPDIR/backups" | ||
$UBACK key gen "$TEST_TMPDIR/backup.key" "$TEST_TMPDIR/backup.pub" | ||
source=type=tar,path="$TEST_TMPDIR/source",key-file="$TEST_TMPDIR/backup.pub",state-file="$TEST_TMPDIR/state.json",snapshots-path="$TEST_TMPDIR/snapshots",full-interval=weekly | ||
dest=id=test,type=command,command=uback-fs-dest,path="$TEST_TMPDIR/backups",@retention-policy=daily=3,key-file="$TEST_TMPDIR/backup.key" | ||
|
||
mkdir -p "$TEST_TMPDIR/restore" | ||
mkdir -p "$TEST_TMPDIR/source" | ||
echo "hello" > "$TEST_TMPDIR/source/a" | ||
|
||
# Full 1 | ||
assert_equal "$($UBACK list backups "$dest" | wc -l)" 0 | ||
$UBACK backup -n -f "$source" "$dest" | ||
assert_equal "$($UBACK list backups "$dest" | wc -l)" 1 | ||
sleep 0.01 | ||
|
||
# Full 2 | ||
$UBACK backup -n -f "$source" "$dest" | ||
assert_equal "$($UBACK list backups "$dest" | wc -l)" 2 | ||
sleep 0.01 | ||
|
||
# Incremental | ||
echo "world" > "$TEST_TMPDIR/source/b" | ||
$UBACK backup -n "$source" "$dest" | ||
assert_equal "$($UBACK list backups "$dest" | wc -l)" 3 | ||
|
||
# Prune (remove full 1) | ||
$UBACK prune backups "$dest" | ||
assert_equal "$($UBACK list backups "$dest" | wc -l)" 2 | ||
|
||
# Restore full 2 + incremental | ||
$UBACK restore -d "$TEST_TMPDIR/restore" "$dest" | ||
assert_equal "$(cat "$TEST_TMPDIR"/restore/*/a)" "hello" | ||
assert_equal "$(cat "$TEST_TMPDIR"/restore/*/b)" "world" | ||
} |
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,36 @@ | ||
#!/bin/bash | ||
|
||
set -e | ||
|
||
if [ "$1" != "destination" ] ; then | ||
echo "Invalid kind" >&2 | ||
exit 1 | ||
fi | ||
|
||
case "$2" in | ||
validate-options) | ||
if [ "$UBACK_OPT_PATH" = "" ] ; then | ||
echo "Missing option: Path" >&2 | ||
exit 1 | ||
fi | ||
mkdir -p -- "$UBACK_OPT_PATH" | ||
;; | ||
list-backups) | ||
ls -- "$UBACK_OPT_PATH" | ||
;; | ||
remove-backup) | ||
rm -f -- "$UBACK_OPT_PATH/$3.ubkp" | ||
;; | ||
send-backup) | ||
cat > "$UBACK_OPT_PATH/_tmp-$3.ubkp" | ||
mv "$UBACK_OPT_PATH/_tmp-$3.ubkp" "$UBACK_OPT_PATH/$3.ubkp" | ||
;; | ||
receive-backup) | ||
cat "$UBACK_OPT_PATH/$3.ubkp" | ||
;; | ||
*) | ||
echo "Invalid operation: $2" >&2 | ||
exit 1 | ||
esac | ||
|
||
exit 0 |