From 8e5eb1090c221cf5f3d7d9c754fd5eddd41ba118 Mon Sep 17 00:00:00 2001 From: fgma Date: Wed, 5 Aug 2020 20:16:37 +0200 Subject: [PATCH 1/4] issue2699: restore symlinks on windows when run as admin user --- changelog/unreleased/issue-2699 | 9 +++++++++ doc/050_restore.rst | 4 ++++ internal/fs/file.go | 2 +- internal/restic/node.go | 5 ----- 4 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 changelog/unreleased/issue-2699 diff --git a/changelog/unreleased/issue-2699 b/changelog/unreleased/issue-2699 new file mode 100644 index 000000000..b933c8a52 --- /dev/null +++ b/changelog/unreleased/issue-2699 @@ -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 diff --git a/doc/050_restore.rst b/doc/050_restore.rst index c7f6c0f28..cab66d54a 100644 --- a/doc/050_restore.rst +++ b/doc/050_restore.rst @@ -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 =================== diff --git a/internal/fs/file.go b/internal/fs/file.go index e8e9080d7..f35901c06 100644 --- a/internal/fs/file.go +++ b/internal/fs/file.go @@ -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. diff --git a/internal/restic/node.go b/internal/restic/node.go index bc64b7fc3..a240384be 100644 --- a/internal/restic/node.go +++ b/internal/restic/node.go @@ -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") From 0d260cfd8254afafad30a2b12b18fda3057c9003 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sat, 29 Oct 2022 21:26:34 +0200 Subject: [PATCH 2/4] enable symlink test on windows --- internal/restic/node_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/restic/node_test.go b/internal/restic/node_test.go index 8139ee57b..c942d171a 100644 --- a/internal/restic/node_test.go +++ b/internal/restic/node_test.go @@ -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)) } From 144257f8bd718227b985e5029ad9acc8f3cc0a51 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 30 Oct 2022 11:02:31 +0100 Subject: [PATCH 3/4] restore symlink timestamps on windows --- internal/restic/node_windows.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/restic/node_windows.go b/internal/restic/node_windows.go index 04a4fe62b..fc6439b40 100644 --- a/internal/restic/node_windows.go +++ b/internal/restic/node_windows.go @@ -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. From 8fe159cc5acd5890044e2fd69762674c01bc8355 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 30 Oct 2022 11:40:42 +0100 Subject: [PATCH 4/4] enable ysmlink tests for windows --- internal/archiver/testing.go | 24 --------------------- internal/archiver/testing_test.go | 35 ++++--------------------------- 2 files changed, 4 insertions(+), 55 deletions(-) diff --git a/internal/archiver/testing.go b/internal/archiver/testing.go index 234e92057..3ca1abbd4 100644 --- a/internal/archiver/testing.go +++ b/internal/archiver/testing.go @@ -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) diff --git a/internal/archiver/testing_test.go b/internal/archiver/testing_test.go index 6f6904e3f..7f89d526f 100644 --- a/internal/archiver/testing_test.go +++ b/internal/archiver/testing_test.go @@ -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()