mirror of
https://github.com/restic/restic.git
synced 2025-01-03 13:45:20 +00:00
fs: error if a symlink points at a file that is not included in the snapshot
This implements @fd0's first idea here: <https://github.com/restic/restic/issues/542#issuecomment-328263959>. > First, I think it may be a good idea to print a warning message when a > symlinks is saved and the target of the symlink exists and is not > included in the backup. This way, users will know that some data > referenced in the snapshot is not available upon restore. Which I wholeheartedly agree with. In the interest of keeping restic's cli simple, and keeping people's data safe, I've opted to not make this configurable. I suppose you could call this a breaking change, but I personally consider it a fix: restic shouldn't claim it has successfully backed up a directory unless it can actually recreate the structure of that directory. IMO, it's better to fail-fast than to claim success, only to greatly disappoint someone later on.
This commit is contained in:
parent
6808004ad1
commit
ace495ea99
1 changed files with 95 additions and 5 deletions
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -102,6 +103,7 @@ type Archiver struct {
|
||||||
treeSaver *treeSaver
|
treeSaver *treeSaver
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
summary *Summary
|
summary *Summary
|
||||||
|
snapshotTargets *[]string
|
||||||
|
|
||||||
// Error is called for all errors that occur during backup.
|
// Error is called for all errors that occur during backup.
|
||||||
Error ErrorFunc
|
Error ErrorFunc
|
||||||
|
@ -596,6 +598,27 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
|
||||||
debug.Log(" %v is a socket, ignoring", target)
|
debug.Log(" %v is a socket, ignoring", target)
|
||||||
return futureNode{}, true, nil
|
return futureNode{}, true, nil
|
||||||
|
|
||||||
|
case fi.Mode&os.ModeSymlink > 0:
|
||||||
|
debug.Log(" %v symlink", target)
|
||||||
|
|
||||||
|
inSnapshot, err := arch.isSymlinkInSnapshot(target)
|
||||||
|
if err != nil {
|
||||||
|
return futureNode{}, false, err
|
||||||
|
}
|
||||||
|
if !inSnapshot {
|
||||||
|
return futureNode{}, false, errors.Errorf("encountered a symlink pointing to a file that is not included in the snapshot: %s", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := arch.nodeFromFileInfo(snPath, target, meta, false)
|
||||||
|
if err != nil {
|
||||||
|
return futureNode{}, false, err
|
||||||
|
}
|
||||||
|
fn = newFutureNodeWithResult(futureNodeResult{
|
||||||
|
snPath: snPath,
|
||||||
|
target: target,
|
||||||
|
node: node,
|
||||||
|
})
|
||||||
|
|
||||||
default:
|
default:
|
||||||
debug.Log(" %v other", target)
|
debug.Log(" %v other", target)
|
||||||
|
|
||||||
|
@ -615,6 +638,72 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
|
||||||
return fn, false, nil
|
return fn, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Given a symlink, return the absolute path of its target target.
|
||||||
|
// Note: unlike `filepath.EvalSymlinks`, this does not recurse! If the target
|
||||||
|
// is itself a symlink, we just return the target.
|
||||||
|
func (arch *Archiver) resolveSymlink(symlink string) (string, error) {
|
||||||
|
target, err := os.Readlink(symlink)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if filepath.IsAbs(target) {
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the filepath is relative, then resolve it relative to the directory
|
||||||
|
// of the symlink.
|
||||||
|
symlinkDir := filepath.Dir(symlink)
|
||||||
|
absTarget := filepath.Join(symlinkDir, target)
|
||||||
|
absTarget = filepath.Clean(absTarget)
|
||||||
|
|
||||||
|
return absTarget, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arch *Archiver) isSymlinkInSnapshot(symlink string) (bool, error) {
|
||||||
|
target, err := arch.resolveSymlink(symlink)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inSnapshot, err := arch.isFileInSnapshot(target)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !inSnapshot {
|
||||||
|
return false, errors.Errorf("encountered a symlink pointing to a file that is not included in the backup: %s", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If `target` is itself a symlink, verify that it's also in the snapshot.
|
||||||
|
fi, err := fs.Lstat(target)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if fi.Mode()&os.ModeSymlink > 0 {
|
||||||
|
return arch.isSymlinkInSnapshot(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arch *Archiver) isFileInSnapshot(file string) (bool, error) {
|
||||||
|
for _, snapshotTarget := range *arch.snapshotTargets {
|
||||||
|
relativePath, err := filepath.Rel(snapshotTarget, file)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error computing relative path from %s to %s: %s", snapshotTarget, file, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(relativePath, "..") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// <<< TODO: look at `arch.SelectByName` and `arch.Select` >>>
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// fileChanged tries to detect whether a file's content has changed compared
|
// fileChanged tries to detect whether a file's content has changed compared
|
||||||
// to the contents of node, which describes the same path in the parent backup.
|
// to the contents of node, which describes the same path in the parent backup.
|
||||||
// It should only be run for regular files.
|
// It should only be run for regular files.
|
||||||
|
@ -853,6 +942,7 @@ func (arch *Archiver) Snapshot(ctx context.Context, targets []string, opts Snaps
|
||||||
arch.summary = &Summary{
|
arch.summary = &Summary{
|
||||||
BackupStart: opts.BackupStart,
|
BackupStart: opts.BackupStart,
|
||||||
}
|
}
|
||||||
|
arch.snapshotTargets = &targets
|
||||||
|
|
||||||
cleanTargets, err := resolveRelativeTargets(arch.FS, targets)
|
cleanTargets, err := resolveRelativeTargets(arch.FS, targets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in a new issue