Merge pull request #2875 from fgma/issue2699

issue2699: restore symlinks on windows when run as admin user
This commit is contained in:
Michael Eischer 2022-11-12 20:06:45 +01:00 committed by GitHub
commit 4b5234924b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 33 additions and 65 deletions

View file

@ -0,0 +1,9 @@
Bugfix: Restore symbolic links on windows
We've added support to restore symbolic links on windows.
Because of windows specific restrictions this is only possible when running
restic having SeCreateSymbolicLinkPrivilege privilege or when running as admin.
https://github.com/restic/restic/issues/1078
https://github.com/restic/restic/issues/2699
https://github.com/restic/restic/pull/2875

View file

@ -56,6 +56,10 @@ There are case insensitive variants of ``--exclude`` and ``--include`` called
``--iexclude`` and ``--iinclude``. These options will behave the same way but ``--iexclude`` and ``--iinclude``. These options will behave the same way but
ignore the casing of paths. ignore the casing of paths.
Restoring symbolic links on windows is only possible when the user has
``SeCreateSymbolicLinkPrivilege`` privilege or is running as admin. This is a
restriction of windows not restic.
Restore using mount Restore using mount
=================== ===================

View file

@ -78,10 +78,6 @@ func TestCreateFiles(t testing.TB, target string, dir TestDir) {
t.Fatal(err) t.Fatal(err)
} }
case TestSymlink: case TestSymlink:
if runtime.GOOS == "windows" {
continue
}
err := fs.Symlink(filepath.FromSlash(it.Target), targetPath) err := fs.Symlink(filepath.FromSlash(it.Target), targetPath)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -139,16 +135,6 @@ func TestEnsureFiles(t testing.TB, target string, dir TestDir) {
// first, test that all items are there // first, test that all items are there
TestWalkFiles(t, target, dir, func(path string, item interface{}) error { TestWalkFiles(t, target, dir, func(path string, item interface{}) error {
// ignore symlinks on Windows
if _, ok := item.(TestSymlink); ok && runtime.GOOS == "windows" {
// mark paths and parents as checked
pathsChecked[path] = struct{}{}
for parent := filepath.Dir(path); parent != target; parent = filepath.Dir(parent) {
pathsChecked[parent] = struct{}{}
}
return nil
}
fi, err := fs.Lstat(path) fi, err := fs.Lstat(path)
if err != nil { if err != nil {
return err return err
@ -298,10 +284,6 @@ func TestEnsureTree(ctx context.Context, t testing.TB, prefix string, repo resti
} }
TestEnsureFileContent(ctx, t, repo, nodePrefix, node, e) TestEnsureFileContent(ctx, t, repo, nodePrefix, node, e)
case TestSymlink: case TestSymlink:
// skip symlinks on windows
if runtime.GOOS == "windows" {
continue
}
if node.Type != "symlink" { if node.Type != "symlink" {
t.Errorf("tree node %v has wrong type %q, want %q", nodePrefix, node.Type, "file") t.Errorf("tree node %v has wrong type %q, want %q", nodePrefix, node.Type, "file")
} }
@ -313,12 +295,6 @@ func TestEnsureTree(ctx context.Context, t testing.TB, prefix string, repo resti
} }
for name := range dir { for name := range dir {
// skip checking symlinks on Windows
entry := dir[name]
if _, ok := entry.(TestSymlink); ok && runtime.GOOS == "windows" {
continue
}
_, ok := checked[name] _, ok := checked[name]
if !ok { if !ok {
t.Errorf("tree %v: expected node %q not found, has: %v", prefix, name, nodeNames) t.Errorf("tree %v: expected node %q not found, has: %v", prefix, name, nodeNames)

View file

@ -6,7 +6,6 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"testing" "testing"
"time" "time"
@ -68,10 +67,6 @@ func createFilesAt(t testing.TB, targetdir string, files map[string]interface{})
t.Fatal(err) t.Fatal(err)
} }
case TestSymlink: case TestSymlink:
// ignore symlinks on windows
if runtime.GOOS == "windows" {
continue
}
err := fs.Symlink(filepath.FromSlash(it.Target), target) err := fs.Symlink(filepath.FromSlash(it.Target), target)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -93,7 +88,7 @@ func TestTestCreateFiles(t *testing.T) {
}, },
"sub": TestDir{ "sub": TestDir{
"subsub": TestDir{ "subsub": TestDir{
"link": TestSymlink{Target: "x/y/z"}, "link": TestSymlink{Target: filepath.Clean("x/y/z")},
}, },
}, },
}, },
@ -101,7 +96,7 @@ func TestTestCreateFiles(t *testing.T) {
"foo": TestFile{Content: "foo"}, "foo": TestFile{Content: "foo"},
"subdir": TestDir{}, "subdir": TestDir{},
"subdir/subfile": TestFile{Content: "bar"}, "subdir/subfile": TestFile{Content: "bar"},
"sub/subsub/link": TestSymlink{Target: "x/y/z"}, "sub/subsub/link": TestSymlink{Target: filepath.Clean("x/y/z")},
}, },
}, },
} }
@ -120,13 +115,6 @@ func TestTestCreateFiles(t *testing.T) {
TestCreateFiles(t, tempdir, test.dir) TestCreateFiles(t, tempdir, test.dir)
for name, item := range test.files { for name, item := range test.files {
// don't check symlinks on windows
if runtime.GOOS == "windows" {
if _, ok := item.(TestSymlink); ok {
continue
}
}
targetPath := filepath.Join(tempdir, filepath.FromSlash(name)) targetPath := filepath.Join(tempdir, filepath.FromSlash(name))
fi, err := fs.Lstat(targetPath) fi, err := fs.Lstat(targetPath)
if err != nil { if err != nil {
@ -233,13 +221,12 @@ func TestTestEnsureFiles(t *testing.T) {
expectFailure bool expectFailure bool
files map[string]interface{} files map[string]interface{}
want TestDir want TestDir
unixOnly bool
}{ }{
{ {
files: map[string]interface{}{ files: map[string]interface{}{
"foo": TestFile{Content: "foo"}, "foo": TestFile{Content: "foo"},
"subdir/subfile": TestFile{Content: "bar"}, "subdir/subfile": TestFile{Content: "bar"},
"x/y/link": TestSymlink{Target: "../../foo"}, "x/y/link": TestSymlink{Target: filepath.Clean("../../foo")},
}, },
want: TestDir{ want: TestDir{
"foo": TestFile{Content: "foo"}, "foo": TestFile{Content: "foo"},
@ -248,7 +235,7 @@ func TestTestEnsureFiles(t *testing.T) {
}, },
"x": TestDir{ "x": TestDir{
"y": TestDir{ "y": TestDir{
"link": TestSymlink{Target: "../../foo"}, "link": TestSymlink{Target: filepath.Clean("../../foo")},
}, },
}, },
}, },
@ -295,7 +282,6 @@ func TestTestEnsureFiles(t *testing.T) {
}, },
{ {
expectFailure: true, expectFailure: true,
unixOnly: true,
files: map[string]interface{}{ files: map[string]interface{}{
"foo": TestFile{Content: "foo"}, "foo": TestFile{Content: "foo"},
}, },
@ -305,7 +291,6 @@ func TestTestEnsureFiles(t *testing.T) {
}, },
{ {
expectFailure: true, expectFailure: true,
unixOnly: true,
files: map[string]interface{}{ files: map[string]interface{}{
"foo": TestSymlink{Target: "xxx"}, "foo": TestSymlink{Target: "xxx"},
}, },
@ -339,11 +324,6 @@ func TestTestEnsureFiles(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run("", func(t *testing.T) { t.Run("", func(t *testing.T) {
if test.unixOnly && runtime.GOOS == "windows" {
t.Skip("skip on Windows")
return
}
tempdir, cleanup := restictest.TempDir(t) tempdir, cleanup := restictest.TempDir(t)
defer cleanup() defer cleanup()
@ -368,7 +348,6 @@ func TestTestEnsureSnapshot(t *testing.T) {
expectFailure bool expectFailure bool
files map[string]interface{} files map[string]interface{}
want TestDir want TestDir
unixOnly bool
}{ }{
{ {
files: map[string]interface{}{ files: map[string]interface{}{
@ -451,7 +430,6 @@ func TestTestEnsureSnapshot(t *testing.T) {
}, },
{ {
expectFailure: true, expectFailure: true,
unixOnly: true,
files: map[string]interface{}{ files: map[string]interface{}{
"foo": TestSymlink{Target: filepath.FromSlash("x/y/z")}, "foo": TestSymlink{Target: filepath.FromSlash("x/y/z")},
}, },
@ -476,11 +454,6 @@ func TestTestEnsureSnapshot(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run("", func(t *testing.T) { t.Run("", func(t *testing.T) {
if test.unixOnly && runtime.GOOS == "windows" {
t.Skip("skip on Windows")
return
}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()

View file

@ -51,7 +51,7 @@ func Rename(oldpath, newpath string) error {
// Symlink creates newname as a symbolic link to oldname. // Symlink creates newname as a symbolic link to oldname.
// If there is an error, it will be of type *LinkError. // If there is an error, it will be of type *LinkError.
func Symlink(oldname, newname string) error { func Symlink(oldname, newname string) error {
return os.Symlink(fixpath(oldname), fixpath(newname)) return os.Symlink(oldname, fixpath(newname))
} }
// Link creates newname as a hard link to oldname. // Link creates newname as a hard link to oldname.

View file

@ -14,7 +14,6 @@ import (
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"bytes" "bytes"
"runtime"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/fs"
@ -295,10 +294,6 @@ func (node Node) writeNodeContent(ctx context.Context, repo Repository, f *os.Fi
} }
func (node Node) createSymlinkAt(path string) error { func (node Node) createSymlinkAt(path string) error {
// Windows does not allow non-admins to create soft links.
if runtime.GOOS == "windows" {
return nil
}
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return errors.Wrap(err, "Symlink") return errors.Wrap(err, "Symlink")

View file

@ -183,9 +183,6 @@ func TestNodeRestoreAt(t *testing.T) {
rtest.OK(t, test.CreateAt(context.TODO(), nodePath, nil)) rtest.OK(t, test.CreateAt(context.TODO(), nodePath, nil))
rtest.OK(t, test.RestoreMetadata(nodePath)) rtest.OK(t, test.RestoreMetadata(nodePath))
if test.Type == "symlink" && runtime.GOOS == "windows" {
continue
}
if test.Type == "dir" { if test.Type == "dir" {
rtest.OK(t, test.RestoreTimestamps(nodePath)) rtest.OK(t, test.RestoreTimestamps(nodePath))
} }

View file

@ -17,7 +17,21 @@ func lchown(path string, uid int, gid int) (err error) {
} }
func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error { func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
return nil // tweaked version of UtimesNano from go/src/syscall/syscall_windows.go
pathp, e := syscall.UTF16PtrFromString(path)
if e != nil {
return e
}
h, e := syscall.CreateFile(pathp,
syscall.FILE_WRITE_ATTRIBUTES, syscall.FILE_SHARE_WRITE, nil, syscall.OPEN_EXISTING,
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OPEN_REPARSE_POINT, 0)
if e != nil {
return e
}
defer syscall.Close(h)
a := syscall.NsecToFiletime(syscall.TimespecToNsec(utimes[0]))
w := syscall.NsecToFiletime(syscall.TimespecToNsec(utimes[1]))
return syscall.SetFileTime(h, nil, &a, &w)
} }
// Getxattr retrieves extended attribute data associated with path. // Getxattr retrieves extended attribute data associated with path.