forked from TrueCloudLab/restic
Merge pull request #2875 from fgma/issue2699
issue2699: restore symlinks on windows when run as admin user
This commit is contained in:
commit
4b5234924b
8 changed files with 33 additions and 65 deletions
9
changelog/unreleased/issue-2699
Normal file
9
changelog/unreleased/issue-2699
Normal 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
|
|
@ -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
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue