mirror of
https://github.com/restic/restic.git
synced 2024-12-21 23:33:03 +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"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
@ -97,11 +98,12 @@ type Archiver struct {
|
|||
FS fs.FS
|
||||
Options Options
|
||||
|
||||
blobSaver *blobSaver
|
||||
fileSaver *fileSaver
|
||||
treeSaver *treeSaver
|
||||
mu sync.Mutex
|
||||
summary *Summary
|
||||
blobSaver *blobSaver
|
||||
fileSaver *fileSaver
|
||||
treeSaver *treeSaver
|
||||
mu sync.Mutex
|
||||
summary *Summary
|
||||
snapshotTargets *[]string
|
||||
|
||||
// Error is called for all errors that occur during backup.
|
||||
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)
|
||||
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:
|
||||
debug.Log(" %v other", target)
|
||||
|
||||
|
@ -615,6 +638,72 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
|
|||
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
|
||||
// to the contents of node, which describes the same path in the parent backup.
|
||||
// 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{
|
||||
BackupStart: opts.BackupStart,
|
||||
}
|
||||
arch.snapshotTargets = &targets
|
||||
|
||||
cleanTargets, err := resolveRelativeTargets(arch.FS, targets)
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in a new issue