1
0
Fork 0
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:
Jeremy Fleischman 2024-12-20 02:42:37 -08:00
parent 6808004ad1
commit ace495ea99
No known key found for this signature in database
GPG key ID: 19319CD8416A642B

View file

@ -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 {