Skip to content

Commit

Permalink
evaluate symlinks for base-dirs when using relative paths in restic
Browse files Browse the repository at this point in the history
  • Loading branch information
jkellerer committed Apr 1, 2024
1 parent 582757d commit 06ce60e
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 6 deletions.
15 changes: 15 additions & 0 deletions config/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ func absolutePath(value string) string {
return value
}

func evaluateSymlinks(value string) string {
suffix, name := "", "-"
for path := value; path != "" && name != ""; {
link := filepath.Clean(path)
path, name = filepath.Split(link)
if prefix, err := filepath.EvalSymlinks(link); err == nil {
return filepath.Join(prefix, suffix)
} else if !errors.Is(err, os.ErrNotExist) {
clog.Errorf("cannot evaluate symlinks for [%s]/%s: %s", link, suffix, err.Error())
}

Check warning on line 119 in config/path.go

View check run for this annotation

Codecov / codecov/patch

config/path.go#L118-L119

Added lines #L118 - L119 were not covered by tests
suffix = filepath.Join(name, suffix)
}
return value
}

// resolveGlob evaluates glob expressions in a slice of paths and returns a resolved slice
func resolveGlob(sources []string) (resolved []string) {
if len(sources) > 0 {
Expand Down
54 changes: 53 additions & 1 deletion config/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import (
"runtime"
"testing"

"github.com/creativeprojects/resticprofile/platform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestFixUnixPaths(t *testing.T) {
if runtime.GOOS == "windows" {
if platform.IsWindows() {
t.SkipNow()
}

Expand Down Expand Up @@ -96,3 +97,54 @@ func TestExpandEnv(t *testing.T) {
assert.Equal(t, "", expandEnv("${__UNDEFINED_ENV_VAR__}"))
assert.Equal(t, "", expandEnv("$__UNDEFINED_ENV_VAR__"))
}

func TestEvalSymlinks(t *testing.T) {
if platform.IsWindows() {
t.SkipNow()
}

var rawDir, dir string
setup := func(t *testing.T) {
var err error
rawDir = t.TempDir()
dir, err = filepath.EvalSymlinks(rawDir)
require.NoError(t, err)
}

link := func(t *testing.T, path, linkname string) {
_ = os.Mkdir(filepath.Join(rawDir, path), 0700)
require.NoError(t, os.Symlink(filepath.Join(rawDir, path), filepath.Join(rawDir, linkname)))
}

t.Run("existing-target", func(t *testing.T) {
setup(t)
link(t, "a", "b")
assert.Equal(t, filepath.Join(dir, "a"), evaluateSymlinks(filepath.Join(rawDir, "b")))
assert.Equal(t, filepath.Join(dir, "a"), evaluateSymlinks(filepath.Join(rawDir, "a")))
assert.Equal(t, filepath.Join(dir, "a"), evaluateSymlinks(filepath.Join(dir, "a")))
})

t.Run("non-existing-target", func(t *testing.T) {
setup(t)
link(t, "a", "b")
assert.Equal(t, filepath.Join(dir, "a", "missing"), evaluateSymlinks(filepath.Join(rawDir, "b", "missing")))
assert.Equal(t, filepath.Join(dir, "missing/path"), evaluateSymlinks(filepath.Join(rawDir, "missing/path")))
})

t.Run("non-existing-targets", func(t *testing.T) {
setup(t)
link(t, "a", "b")
assert.Equal(t, filepath.Join(dir, "a/mis/s/ing"), evaluateSymlinks(filepath.Join(rawDir, "b/mis/s/ing")))
})

t.Run("nested", func(t *testing.T) {
setup(t)
link(t, "a", "b")
link(t, "d", "c")
link(t, "a/nested", "b/c")
link(t, "d", "b/c/toD")
assert.Equal(t, filepath.Join(dir, "a/nested"), evaluateSymlinks(filepath.Join(rawDir, "b/c")))
assert.Equal(t, filepath.Join(dir, "d"), evaluateSymlinks(filepath.Join(rawDir, "b/c/toD")))
assert.Equal(t, filepath.Join(dir, "d"), evaluateSymlinks(filepath.Join(rawDir, "a/nested/toD")))
})
}
15 changes: 12 additions & 3 deletions config/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,15 @@ func (b *BackupSection) resolve(profile *Profile) {
if len(b.StdinCommand) > 0 {
b.UseStdin = true
}
// Resolve symlinks if we send relative paths to restic (to match paths in snapshots)
if b.SourceRelative {
if dir := strings.TrimSpace(profile.BaseDir); dir != "" {
profile.BaseDir = evaluateSymlinks(dir)
}

Check warning on line 202 in config/profile.go

View check run for this annotation

Codecov / codecov/patch

config/profile.go#L201-L202

Added lines #L201 - L202 were not covered by tests
if dir := strings.TrimSpace(b.SourceBase); dir != "" {
b.SourceBase = evaluateSymlinks(dir)
}
}
// Resolve source paths
if b.unresolvedSource == nil {
b.unresolvedSource = b.Source
Expand Down Expand Up @@ -664,8 +673,7 @@ func (p *Profile) resolveSourcePath(sourceBase string, relativePaths bool, sourc
if p.BaseDir != "" {
applyBaseDir = absolutePrefix(p.BaseDir)
}

} else if p.BaseDir == "" && sourceBase == "" && p.config != nil {
} else if p.BaseDir == "" && sourceBase == "" && p.hasConfig() {
p.config.reportChangedPath(".", "<none>", "source-base (for relative source)")

Check warning on line 677 in config/profile.go

View check run for this annotation

Codecov / codecov/patch

config/profile.go#L677

Added line #L677 was not covered by tests
}

Expand Down Expand Up @@ -699,8 +707,9 @@ func (p *Profile) SetTag(tags ...string) {

// SetPath will replace any path value from a boolean to sourcePaths and change paths to absolute
func (p *Profile) SetPath(basePath string, sourcePaths ...string) {
hasAbsoluteBase := len(p.BaseDir) > 0 && filepath.IsAbs(p.BaseDir) || basePath != "" && filepath.IsAbs(basePath)

resolvePath := func(origin string, paths []string, revolver func(string) []string) (resolved []string) {
hasAbsoluteBase := len(p.BaseDir) > 0 && filepath.IsAbs(p.BaseDir) || basePath != "" && filepath.IsAbs(basePath)
for _, path := range paths {
if len(path) > 0 {
for _, rp := range revolver(path) {
Expand Down
2 changes: 1 addition & 1 deletion shell/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package shell
import (
"bytes"
"fmt"
"github.com/creativeprojects/resticprofile/platform"
"io/ioutil"
"os"
"os/exec"
Expand All @@ -16,6 +15,7 @@ import (
"testing"
"time"

"github.com/creativeprojects/resticprofile/platform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down
9 changes: 8 additions & 1 deletion wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,14 @@ func (r *resticWrapper) prepareCommand(command string, args *shell.Args, allowEx
env := r.getEnvironment(true)
env = append(env, r.getProfileEnvironment()...)

clog.Debugf("starting command: %s %s", r.ctx.binary, strings.Join(publicArguments, " "))
clog.Debug(func() string {
wd := ""
if dir != "" {
wd = fmt.Sprintf(" (in %s)", dir)

Check warning on line 412 in wrapper.go

View check run for this annotation

Codecov / codecov/patch

wrapper.go#L412

Added line #L412 was not covered by tests
}
return fmt.Sprintf("starting command: %s %s%s", r.ctx.binary, strings.Join(publicArguments, " "), wd)
})

rCommand := newShellCommand(r.ctx.binary, arguments, env, r.getShell(), r.dryRun, r.sigChan, r.setPID)
rCommand.publicArgs = publicArguments
// stdout are stderr are coming from the default terminal (in case they're redirected)
Expand Down

0 comments on commit 06ce60e

Please sign in to comment.