Skip to content
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
13 changes: 13 additions & 0 deletions subcommands/restore/plakar-restore.1
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
.Op Fl job Ar job
.Op Fl name Ar name
.Op Fl perimeter Ar perimeter
.Op Fl post-hook Ar command
.Op Fl pre-hook Ar command
.Op Fl skip-permissions
.Op Fl tag Ar tag
.Op Fl to Ar directory
Expand Down Expand Up @@ -52,6 +54,17 @@ Only apply command to snapshots that match
.It Fl tag Ar string
Only apply command to snapshots that match
.Ar tag .
.It Fl pre-hook Ar command
Run
.Ar command
in a shell before starting the restore.
If the command exits with a non-zero status, restore is aborted.
.It Fl post-hook Ar command
Run
.Ar command
in a shell after a successful restore.
If the command exits with a non-zero status, a warning is logged and
the restore still succeeds.
.It Fl skip-permissions
Skip restoring file permissions and ownership during restore,
defaulting to 0750 for directories and 0640 for files.
Expand Down
34 changes: 34 additions & 0 deletions subcommands/restore/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import (
"flag"
"fmt"
"maps"
"os/exec"
"path"
"runtime"
"strings"
"time"

Expand All @@ -44,6 +46,8 @@ type Restore struct {
OptTag string
OptSkipPermissions bool
Opts map[string]string
OptPreHook string
OptPostHook string

Target string
Strip string
Expand Down Expand Up @@ -76,6 +80,8 @@ func (cmd *Restore) Parse(ctx *appcontext.AppContext, args []string) error {

flags.StringVar(&pullPath, "to", "", "base directory where pull will restore")
flags.BoolVar(&cmd.OptSkipPermissions, "skip-permissions", false, "do not restore file permissions")
flags.StringVar(&cmd.OptPreHook, "pre-hook", "", "shell command to run before restore")
flags.StringVar(&cmd.OptPostHook, "post-hook", "", "shell command to run after restore")
flags.Parse(args)

if flags.NArg() != 0 {
Expand Down Expand Up @@ -147,6 +153,10 @@ func (cmd *Restore) Execute(ctx *appcontext.AppContext, repo *repository.Reposit
return 1, fmt.Errorf("multiple snapshots found, please specify one")
}

if err := executeHook(ctx, cmd.OptPreHook); err != nil {
return 1, fmt.Errorf("pre-restore hook failed: %w", err)
}

exporterConfig := map[string]string{
"location": cmd.Target,
}
Expand Down Expand Up @@ -200,5 +210,29 @@ func (cmd *Restore) Execute(ctx *appcontext.AppContext, repo *repository.Reposit

snap.Close()
}

if err := executeHook(ctx, cmd.OptPostHook); err != nil {
ctx.GetLogger().Warn("post-restore hook failed: %s", err)
}

return 0, nil
}

func executeHook(ctx *appcontext.AppContext, hook string) error {
if hook == "" {
return nil
}
ctx.GetLogger().Info("executing hook: %s", hook)

var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd", "/C", hook)
default: // assume unix-esque
cmd = exec.Command("/bin/sh", "-c", hook)
}

cmd.Stdout = ctx.Stdout
cmd.Stderr = ctx.Stderr
return cmd.Run()
}
91 changes: 91 additions & 0 deletions subcommands/restore/restore_extra_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package restore

import (
"bytes"
"encoding/hex"
"fmt"
"os"
Expand Down Expand Up @@ -152,6 +153,96 @@ func TestRestoreExporterOptsParsed(t *testing.T) {
require.Equal(t, "y", cmd.Opts["x"])
}

func TestRestoreHooksFlagsAreParsed(t *testing.T) {
_, _, ctx := generateSnapshot(t)
cmd := &Restore{}
require.NoError(t, cmd.Parse(ctx, []string{
"-pre-hook", "echo 'pre-restore hook'",
"-post-hook", "echo 'post-restore hook'",
"-to", "/tmp/x",
}))
require.Equal(t, "echo 'pre-restore hook'", cmd.OptPreHook)
require.Equal(t, "echo 'post-restore hook'", cmd.OptPostHook)
}

func TestRestoreWithHooks(t *testing.T) {
repo, snap, ctx := generateSnapshot(t)
defer snap.Close()

dir := mkRestoreDir(t)
bufOut := bytes.NewBuffer(nil)
bufErr := bytes.NewBuffer(nil)
ctx.Stdout = bufOut
ctx.Stderr = bufErr

id := snap.Header.GetIndexID()
cmd := &Restore{}
require.NoError(t, cmd.Parse(ctx, []string{
"-to", dir,
"-pre-hook", "echo 'pre-restore hook executed'",
"-post-hook", "echo 'post-restore hook executed'",
hex.EncodeToString(id[:]) + ":",
}))

status, err := cmd.Execute(ctx, repo)
require.NoError(t, err)
require.Equal(t, 0, status)

output := bufOut.String()
require.Contains(t, output, "pre-restore hook executed")
require.Contains(t, output, "post-restore hook executed")

checkRestored(t, dir)
}

func TestRestorePreHookFailureAbortsRestore(t *testing.T) {
repo, snap, ctx := generateSnapshot(t)
defer snap.Close()

dir := mkRestoreDir(t)
id := snap.Header.GetIndexID()
cmd := &Restore{}
require.NoError(t, cmd.Parse(ctx, []string{
"-to", dir,
"-pre-hook", "exit 7",
hex.EncodeToString(id[:]) + ":",
}))

status, err := cmd.Execute(ctx, repo)
require.Error(t, err)
require.Equal(t, 1, status)
require.Contains(t, err.Error(), "pre-restore hook failed")

rest, readErr := os.ReadDir(dir)
require.NoError(t, readErr)
require.Empty(t, rest)
}

func TestRestorePostHookFailureIsNotFatal(t *testing.T) {
repo, snap, ctx := generateSnapshot(t)
defer snap.Close()

dir := mkRestoreDir(t)
bufOut := bytes.NewBuffer(nil)
bufErr := bytes.NewBuffer(nil)
ctx.Stdout = bufOut
ctx.Stderr = bufErr

id := snap.Header.GetIndexID()
cmd := &Restore{}
require.NoError(t, cmd.Parse(ctx, []string{
"-to", dir,
"-post-hook", "exit 9",
hex.EncodeToString(id[:]) + ":",
}))

status, err := cmd.Execute(ctx, repo)
require.NoError(t, err)
require.Equal(t, 0, status)

checkRestored(t, dir)
}

func TestRestoreInvalidSnapshotID(t *testing.T) {
// A garbage snapshot ID should yield "no snapshots found" (because it
// matches nothing in the locate filter).
Expand Down
Loading