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
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
===================

View file

@ -78,10 +78,6 @@ func TestCreateFiles(t testing.TB, target string, dir TestDir) {
t.Fatal(err)
}
case TestSymlink:
if runtime.GOOS == "windows" {
continue
}
err := fs.Symlink(filepath.FromSlash(it.Target), targetPath)
if err != nil {
t.Fatal(err)
@ -139,16 +135,6 @@ func TestEnsureFiles(t testing.TB, target string, dir TestDir) {
// first, test that all items are there
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)
if err != nil {
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)
case TestSymlink:
// skip symlinks on windows
if runtime.GOOS == "windows" {
continue
}
if node.Type != "symlink" {
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 {
// skip checking symlinks on Windows
entry := dir[name]
if _, ok := entry.(TestSymlink); ok && runtime.GOOS == "windows" {
continue
}
_, ok := checked[name]
if !ok {
t.Errorf("tree %v: expected node %q not found, has: %v", prefix, name, nodeNames)

View file

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

View file

@ -51,7 +51,7 @@ func Rename(oldpath, newpath string) error {
// Symlink creates newname as a symbolic link to oldname.
// If there is an error, it will be of type *LinkError.
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.

View file

@ -14,7 +14,6 @@ import (
"github.com/restic/restic/internal/errors"
"bytes"
"runtime"
"github.com/restic/restic/internal/debug"
"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 {
// 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) {
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.RestoreMetadata(nodePath))
if test.Type == "symlink" && runtime.GOOS == "windows" {
continue
}
if test.Type == "dir" {
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 {
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.