Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to set working directory for restic backup #354

Merged
merged 7 commits into from
Apr 3, 2024
23 changes: 14 additions & 9 deletions config/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ type BackupSection struct {
CheckAfter bool `mapstructure:"check-after" description:"Check the repository after the backup command succeeded"`
UseStdin bool `mapstructure:"stdin" argument:"stdin"`
StdinCommand []string `mapstructure:"stdin-command" description:"Shell command(s) that generate content to redirect into the stdin of restic. When set, the flag \"stdin\" is always set to \"true\"."`
SourceRelative bool `mapstructure:"source-relative" description:"Enable backup with relative source paths. This will change the working directory of the \"restic backup\" command to \"source-base\", and will not expand \"source\" to an absolute path."`
SourceBase string `mapstructure:"source-base" examples:"/;$PWD;C:\\;%cd%" description:"The base path to resolve relative backup paths against. Defaults to current directory if unset or empty (see also \"base-dir\" in profile)"`
Source []string `mapstructure:"source" examples:"/opt/;/home/user/;C:\\Users\\User\\Documents" description:"The paths to backup"`
Exclude []string `mapstructure:"exclude" argument:"exclude" argument-type:"no-glob"`
Expand Down Expand Up @@ -651,15 +652,19 @@ func (p *Profile) SetRootPath(rootPath string) {
func (p *Profile) resolveSourcePath(sourceBase string, sourcePaths ...string) []string {
var applySourceBase, applyBaseDir pathFix

// Backup source is NOT relative to the configuration, but to PWD or sourceBase (if not empty)
// Applying "sourceBase" if set
if sourceBase = strings.TrimSpace(sourceBase); sourceBase != "" {
seiuneko marked this conversation as resolved.
Show resolved Hide resolved
sourceBase = fixPath(sourceBase, expandEnv, expandUserHome)
applySourceBase = absolutePrefix(sourceBase)
}
// Applying a custom PWD eagerly so that own commands (e.g. "show") display correct paths
if p.BaseDir != "" {
applyBaseDir = absolutePrefix(p.BaseDir)
sourceBase = fixPath(sourceBase, expandEnv, expandUserHome)
// When "source-relative" is set, the source paths are relative to the "source-base"
if p.Backup == nil || !p.Backup.SourceRelative {
seiuneko marked this conversation as resolved.
Show resolved Hide resolved
// Backup source is NOT relative to the configuration, but to PWD or sourceBase (if not empty)
// Applying "sourceBase" if set
if sourceBase = strings.TrimSpace(sourceBase); sourceBase != "" {
applySourceBase = absolutePrefix(sourceBase)
}
// Applying a custom PWD eagerly so that own commands (e.g. "show") display correct paths
if p.BaseDir != "" {
applyBaseDir = absolutePrefix(p.BaseDir)
}

}
seiuneko marked this conversation as resolved.
Show resolved Hide resolved

// prefix paths starting with "-" with a "./" to distinguish a source path from a flag
Expand Down
20 changes: 15 additions & 5 deletions config/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -636,9 +636,10 @@ func TestResolveSourcesWithFlagPrefixInBackup(t *testing.T) {
}

func TestResolveSourcesAgainstBase(t *testing.T) {
backupSource := func(base, source string) []string {
backupSource := func(base, source string, changeWorkingDir bool) []string {
config := `
[profile.backup]
source-relative = ` + strconv.FormatBool(changeWorkingDir) + `
source-base = "` + filepath.ToSlash(base) + `"
source = "` + filepath.ToSlash(source) + `"
`
Expand All @@ -653,18 +654,27 @@ func TestResolveSourcesAgainstBase(t *testing.T) {
assert.NoError(t, err)

t.Run("no-base", func(t *testing.T) {
assert.Equal(t, []string{"src"}, backupSource("", "src"))
assert.Equal(t, []string{"src"}, backupSource("", "src", false))
})
t.Run("relative-base", func(t *testing.T) {
assert.Equal(t, []string{filepath.Join("rel", "src")}, backupSource("rel", "src"))
assert.Equal(t, []string{filepath.Join("rel", "src")}, backupSource("rel", "src", false))
})
t.Run("absolute-base", func(t *testing.T) {
assert.Equal(t, []string{filepath.Join(cwd, "src")}, backupSource(cwd, "src"))
assert.Equal(t, []string{filepath.Join(cwd, "src")}, backupSource(cwd, "src", false))
})
t.Run("env-var-base", func(t *testing.T) {
assert.NoError(t, os.Setenv("RP_TEST_CWD", cwd))
defer os.Unsetenv("RP_TEST_CWD")
assert.Equal(t, []string{filepath.Join(cwd, "path", "src")}, backupSource("${RP_TEST_CWD}/path", "src"))
assert.Equal(t, []string{filepath.Join(cwd, "path", "src")}, backupSource("${RP_TEST_CWD}/path", "src", false))
})
t.Run("change-relative-working-dir", func(t *testing.T) {
assert.Equal(t, []string{"."}, backupSource("path", ".", true))
assert.Equal(t, []string{filepath.Join("path", ".")}, backupSource("path", ".", false))
})
t.Run("change-env-var-working-dir", func(t *testing.T) {
assert.NoError(t, os.Setenv("RP_TEST_ENV", "."))
defer os.Unsetenv("RP_TEST_ENV")
assert.Equal(t, []string{"."}, backupSource("path", "${RP_TEST_ENV}", true))
})
}

Expand Down
1 change: 1 addition & 0 deletions docs/content/configuration/path.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ default {
{{% notice hint %}}
Set `base-dir` to an absolute path to resolve `files` and `local:backup` relative to it.
Set `source-base` if you need a separate base path for backup sources.
When you want to use relative source paths for backup, set the `source-relative` option. This will change the working directory of the `restic backup` command to `source-base` and will not expand `source` to an absolute path.
{{% /notice %}}

## How the configuration file is resolved
Expand Down
2 changes: 2 additions & 0 deletions shell/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ func (c *Command) Run() (monitor.Summary, string, error) {
cmd.Env = append(cmd.Env, c.Environ...)
}

cmd.Dir = c.Dir

start := time.Now()

// spawn the child process
Expand Down
21 changes: 21 additions & 0 deletions shell/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package shell
import (
"bytes"
"fmt"
"github.com/creativeprojects/resticprofile/platform"
"io/ioutil"
"os"
"os/exec"
Expand Down Expand Up @@ -285,6 +286,26 @@ func TestSelectCustomShell(t *testing.T) {
assert.Empty(t, shell)
}

func TestRunShellWorkingDir(t *testing.T) {
command := func() string {
if platform.IsWindows() {
return "@echo %CD%"
}
return "pwd"
}()
temp := t.TempDir()
buffer := new(strings.Builder)
cmd := NewCommand(command, nil)
cmd.Stdout = buffer
cmd.Dir = temp
_, _, err := cmd.Run()
if err != nil {
t.Fatal(err)
}

assert.Contains(t, strings.TrimSpace(buffer.String()), temp)
}

func TestRunShellEcho(t *testing.T) {
buffer := &bytes.Buffer{}
cmd := NewCommand("echo", []string{"TestRunShellEcho"})
Expand Down
5 changes: 5 additions & 0 deletions shell_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type shellCommandDefinition struct {
publicArgs []string
env []string
shell []string
dir string
stdin io.ReadCloser
stdout io.Writer
stderr io.Writer
Expand Down Expand Up @@ -85,6 +86,10 @@ func runShellCommand(command shellCommandDefinition) (summary monitor.Summary, s
shellCmd.Environ = append(shellCmd.Environ, command.env...)
}

// If Dir is the empty string, Run runs the command in the
// calling process's current directory.
shellCmd.Dir = command.dir

// scan output
if command.scanOutput != nil {
shellCmd.ScanStdout = command.scanOutput
Expand Down
5 changes: 5 additions & 0 deletions wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,12 @@ func (r *resticWrapper) prepareCommand(command string, args *shell.Args, allowEx
args.AddArgs(moreArgs, shell.ArgCommandLineEscape)

// Special case for backup command
var dir string
if command == constants.CommandBackup {
args.AddArgs(r.profile.GetBackupSource(), shell.ArgConfigBackupSource)
if r.profile.Backup != nil {
dir = r.profile.Backup.SourceBase
}
}
// Special case for copy command
if command == constants.CommandCopy {
Expand Down Expand Up @@ -409,6 +413,7 @@ func (r *resticWrapper) prepareCommand(command string, args *shell.Args, allowEx
rCommand.stdout = term.GetOutput()
rCommand.stderr = term.GetErrorOutput()
rCommand.streamError = r.profile.StreamError
rCommand.dir = dir

return rCommand
}
Expand Down
Loading