From dfe232cf4693ab54addf91302c940d52c50fa34d Mon Sep 17 00:00:00 2001 From: Klaus Post Date: Fri, 14 Aug 2015 15:57:47 +0200 Subject: [PATCH] Add Windows node support. The syscall.Stat_t doesn't exist on Windows, so it is replaced by an interface, which Windows can fill out, and field access is replaced by function calls. Common Unix functionality is put into "node_unix.go", so there is less boilerplate. Symlinks are skipped on Windows, since they require admin privileges. --- node.go | 78 ++++++++++++++++++++++++++++++++++++++----------- node_darwin.go | 14 +++------ node_freebsd.go | 14 +++------ node_linux.go | 14 +++------ node_openbsd.go | 14 +++------ node_test.go | 19 +++++++----- node_unix.go | 34 +++++++++++++++++++++ node_windows.go | 66 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 189 insertions(+), 64 deletions(-) create mode 100644 node_unix.go create mode 100644 node_windows.go diff --git a/node.go b/node.go index e607bc8b1..974f83834 100644 --- a/node.go +++ b/node.go @@ -15,6 +15,7 @@ import ( "github.com/restic/restic/debug" "github.com/restic/restic/pack" "github.com/restic/restic/repository" + "runtime" ) // Node is a file, directory or other item in a backup. @@ -148,7 +149,7 @@ func (node *Node) CreateAt(path string, repo *repository.Repository) error { func (node Node) restoreMetadata(path string) error { var err error - err = os.Lchown(path, int(node.UID), int(node.GID)) + err = lchown(path, int(node.UID), int(node.GID)) if err != nil { return errors.Annotate(err, "Lchown") } @@ -236,6 +237,10 @@ func (node Node) createFileAt(path string, repo *repository.Repository) error { } func (node Node) createSymlinkAt(path string) error { + // Windows does not allow non-admins to create soft links. + if runtime.GOOS == "windows" { + return nil + } err := os.Symlink(node.LinkTarget, path) if err != nil { return errors.Annotate(err, "Symlink") @@ -245,15 +250,15 @@ func (node Node) createSymlinkAt(path string) error { } func (node *Node) createDevAt(path string) error { - return syscall.Mknod(path, syscall.S_IFBLK|0600, int(node.Device)) + return mknod(path, syscall.S_IFBLK|0600, int(node.Device)) } func (node *Node) createCharDevAt(path string) error { - return syscall.Mknod(path, syscall.S_IFCHR|0600, int(node.Device)) + return mknod(path, syscall.S_IFCHR|0600, int(node.Device)) } func (node *Node) createFifoAt(path string) error { - return syscall.Mkfifo(path, 0600) + return mkfifo(path, 0600) } func (node Node) MarshalJSON() ([]byte, error) { @@ -381,9 +386,19 @@ func (node *Node) isNewer(path string, fi os.FileInfo) bool { return true } - extendedStat := fi.Sys().(*syscall.Stat_t) - inode := extendedStat.Ino - size := uint64(extendedStat.Size) + size := uint64(fi.Size()) + + extendedStat, ok := toStatT(fi.Sys()) + if !ok { + if node.ModTime != fi.ModTime() || + node.Size != size { + debug.Log("node.isNewer", "node %v is newer: timestamp, size or inode changed", path) + return true + } + return false + } + + inode := extendedStat.ino() if node.ModTime != fi.ModTime() || node.ChangeTime != changeTime(extendedStat) || @@ -397,11 +412,11 @@ func (node *Node) isNewer(path string, fi os.FileInfo) bool { return false } -func (node *Node) fillUser(stat *syscall.Stat_t) error { - node.UID = stat.Uid - node.GID = stat.Gid +func (node *Node) fillUser(stat statT) error { + node.UID = stat.uid() + node.GID = stat.gid() - username, err := lookupUsername(strconv.Itoa(int(stat.Uid))) + username, err := lookupUsername(strconv.Itoa(int(stat.uid()))) if err != nil { return errors.Annotate(err, "fillUser") } @@ -439,12 +454,12 @@ func lookupUsername(uid string) (string, error) { } func (node *Node) fillExtra(path string, fi os.FileInfo) error { - stat, ok := fi.Sys().(*syscall.Stat_t) + stat, ok := toStatT(fi.Sys()) if !ok { return nil } - node.Inode = uint64(stat.Ino) + node.Inode = uint64(stat.ino()) node.fillTimes(stat) @@ -456,15 +471,15 @@ func (node *Node) fillExtra(path string, fi os.FileInfo) error { switch node.Type { case "file": - node.Size = uint64(stat.Size) - node.Links = uint64(stat.Nlink) + node.Size = uint64(stat.size()) + node.Links = uint64(stat.nlink()) case "dir": case "symlink": node.LinkTarget, err = os.Readlink(path) case "dev": - node.Device = uint64(stat.Rdev) + node.Device = uint64(stat.rdev()) case "chardev": - node.Device = uint64(stat.Rdev) + node.Device = uint64(stat.rdev()) case "fifo": case "socket": default: @@ -473,3 +488,32 @@ func (node *Node) fillExtra(path string, fi os.FileInfo) error { return err } + +type statT interface { + dev() uint64 + ino() uint64 + nlink() uint64 + uid() uint32 + gid() uint32 + rdev() uint64 + size() int64 + atim() syscall.Timespec + mtim() syscall.Timespec + ctim() syscall.Timespec +} + +func mkfifo(path string, mode uint32) (err error) { + return mknod(path, mode|syscall.S_IFIFO, 0) +} + +func (node *Node) fillTimes(stat statT) { + ctim := stat.ctim() + atim := stat.atim() + node.ChangeTime = time.Unix(ctim.Unix()) + node.AccessTime = time.Unix(atim.Unix()) +} + +func changeTime(stat statT) time.Time { + ctim := stat.ctim() + return time.Unix(ctim.Unix()) +} diff --git a/node_darwin.go b/node_darwin.go index f63b41f13..adc980295 100644 --- a/node_darwin.go +++ b/node_darwin.go @@ -3,22 +3,16 @@ package restic import ( "os" "syscall" - "time" ) func (node *Node) OpenForReading() (*os.File, error) { return os.Open(node.path) } -func changeTime(stat *syscall.Stat_t) time.Time { - return time.Unix(stat.Ctimespec.Unix()) -} - -func (node *Node) fillTimes(stat *syscall.Stat_t) { - node.ChangeTime = time.Unix(stat.Ctimespec.Unix()) - node.AccessTime = time.Unix(stat.Atimespec.Unix()) -} - func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error { return nil } + +func (s statUnix) atim() syscall.Timespec { return s.Atimespec } +func (s statUnix) mtim() syscall.Timespec { return s.Mtimespec } +func (s statUnix) ctim() syscall.Timespec { return s.Ctimespec } diff --git a/node_freebsd.go b/node_freebsd.go index 231cf8db7..67bcdf3e9 100644 --- a/node_freebsd.go +++ b/node_freebsd.go @@ -3,22 +3,16 @@ package restic import ( "os" "syscall" - "time" ) func (node *Node) OpenForReading() (*os.File, error) { return os.OpenFile(node.path, os.O_RDONLY, 0) } -func (node *Node) fillTimes(stat *syscall.Stat_t) { - node.ChangeTime = time.Unix(stat.Ctimespec.Unix()) - node.AccessTime = time.Unix(stat.Atimespec.Unix()) -} - -func changeTime(stat *syscall.Stat_t) time.Time { - return time.Unix(stat.Ctimespec.Unix()) -} - func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error { return nil } + +func (s statUnix) atim() syscall.Timespec { return s.Atimespec } +func (s statUnix) mtim() syscall.Timespec { return s.Mtimespec } +func (s statUnix) ctim() syscall.Timespec { return s.Ctimespec } diff --git a/node_linux.go b/node_linux.go index 1043397a8..2304c13d5 100644 --- a/node_linux.go +++ b/node_linux.go @@ -4,7 +4,6 @@ import ( "os" "path/filepath" "syscall" - "time" "unsafe" "github.com/juju/errors" @@ -18,15 +17,6 @@ func (node *Node) OpenForReading() (*os.File, error) { return file, err } -func (node *Node) fillTimes(stat *syscall.Stat_t) { - node.ChangeTime = time.Unix(stat.Ctim.Unix()) - node.AccessTime = time.Unix(stat.Atim.Unix()) -} - -func changeTime(stat *syscall.Stat_t) time.Time { - return time.Unix(stat.Ctim.Unix()) -} - func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error { dir, err := os.Open(filepath.Dir(path)) defer dir.Close() @@ -65,3 +55,7 @@ func utimensat(dirfd int, path string, times *[2]syscall.Timespec, flags int) (e func utimesNanoAt(dirfd int, path string, ts [2]syscall.Timespec, flags int) (err error) { return utimensat(dirfd, path, (*[2]syscall.Timespec)(unsafe.Pointer(&ts[0])), flags) } + +func (s statUnix) atim() syscall.Timespec { return s.Atim } +func (s statUnix) mtim() syscall.Timespec { return s.Mtim } +func (s statUnix) ctim() syscall.Timespec { return s.Ctim } diff --git a/node_openbsd.go b/node_openbsd.go index 38f1d8441..feafe307e 100644 --- a/node_openbsd.go +++ b/node_openbsd.go @@ -3,7 +3,6 @@ package restic import ( "os" "syscall" - "time" ) func (node *Node) OpenForReading() (*os.File, error) { @@ -14,15 +13,10 @@ func (node *Node) OpenForReading() (*os.File, error) { return file, err } -func (node *Node) fillTimes(stat *syscall.Stat_t) { - node.ChangeTime = time.Unix(stat.Ctim.Unix()) - node.AccessTime = time.Unix(stat.Atim.Unix()) -} - -func changeTime(stat *syscall.Stat_t) time.Time { - return time.Unix(stat.Ctim.Unix()) -} - func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error { return nil } + +func (s statUnix) atim() syscall.Timespec { return s.Atim } +func (s statUnix) mtim() syscall.Timespec { return s.Mtim } +func (s statUnix) ctim() syscall.Timespec { return s.Ctim } diff --git a/node_test.go b/node_test.go index f6104b49b..5bdf284bb 100644 --- a/node_test.go +++ b/node_test.go @@ -119,6 +119,9 @@ func TestNodeRestoreAt(t *testing.T) { nodePath := filepath.Join(tempdir, test.Name) OK(t, test.CreateAt(nodePath, nil)) + if test.Type == "symlink" && runtime.GOOS == "windows" { + continue + } if test.Type == "dir" { OK(t, test.RestoreTimestamps(nodePath)) } @@ -135,14 +138,16 @@ func TestNodeRestoreAt(t *testing.T) { "%v: type doesn't match (%v != %v)", test.Type, test.Type, n2.Type) Assert(t, test.Size == n2.Size, "%v: size doesn't match (%v != %v)", test.Size, test.Size, n2.Size) - Assert(t, test.UID == n2.UID, - "%v: UID doesn't match (%v != %v)", test.Type, test.UID, n2.UID) - Assert(t, test.GID == n2.GID, - "%v: GID doesn't match (%v != %v)", test.Type, test.GID, n2.GID) - if test.Type != "symlink" { - Assert(t, test.Mode == n2.Mode, - "%v: mode doesn't match (%v != %v)", test.Type, test.Mode, n2.Mode) + if runtime.GOOS != "windows" { + Assert(t, test.UID == n2.UID, + "%v: UID doesn't match (%v != %v)", test.Type, test.UID, n2.UID) + Assert(t, test.GID == n2.GID, + "%v: GID doesn't match (%v != %v)", test.Type, test.GID, n2.GID) + if test.Type != "symlink" { + Assert(t, test.Mode == n2.Mode, + "%v: mode doesn't match (%v != %v)", test.Type, test.Mode, n2.Mode) + } } AssertFsTimeEqual(t, "AccessTime", test.Type, test.AccessTime, n2.AccessTime) diff --git a/node_unix.go b/node_unix.go new file mode 100644 index 000000000..5c0c4d63c --- /dev/null +++ b/node_unix.go @@ -0,0 +1,34 @@ +// + +// +build dragonfly linux netbsd openbsd freebsd solaris darwin + +package restic + +import ( + "os" + "syscall" +) + +var mknod = syscall.Mknod +var lchown = os.Lchown + +type statUnix syscall.Stat_t + +func toStatT(i interface{}) (statT, bool) { + if i == nil { + return nil, false + } + s, ok := i.(*syscall.Stat_t) + if ok && s != nil { + return statUnix(*s), true + } + return nil, false +} + +func (s statUnix) dev() uint64 { return uint64(s.Dev) } +func (s statUnix) ino() uint64 { return uint64(s.Ino) } +func (s statUnix) nlink() uint64 { return uint64(s.Nlink) } +func (s statUnix) uid() uint32 { return uint32(s.Uid) } +func (s statUnix) gid() uint32 { return uint32(s.Gid) } +func (s statUnix) rdev() uint64 { return uint64(s.Rdev) } +func (s statUnix) size() int64 { return int64(s.Size) } diff --git a/node_windows.go b/node_windows.go new file mode 100644 index 000000000..7d5854b80 --- /dev/null +++ b/node_windows.go @@ -0,0 +1,66 @@ +package restic + +import ( + "os" + "syscall" +) + +func (node *Node) OpenForReading() (*os.File, error) { + file, err := os.OpenFile(node.path, os.O_RDONLY, 0) + if os.IsPermission(err) { + return os.OpenFile(node.path, os.O_RDONLY, 0) + } + return file, err +} + +// mknod() creates a filesystem node (file, device +// special file, or named pipe) named pathname, with attributes +// specified by mode and dev. +var mknod = func(path string, mode uint32, dev int) (err error) { + panic("mknod not implemented") +} + +// Windows doesn't need lchown +var lchown = func(path string, uid int, gid int) (err error) { + return nil +} + +func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error { + return nil +} + +type statWin syscall.Win32FileAttributeData + +func toStatT(i interface{}) (statT, bool) { + if i == nil { + return nil, false + } + s, ok := i.(*syscall.Win32FileAttributeData) + if ok && s != nil { + return statWin(*s), true + } + return nil, false +} + +func (s statWin) dev() uint64 { return 0 } +func (s statWin) ino() uint64 { return 0 } +func (s statWin) nlink() uint64 { return 0 } +func (s statWin) uid() uint32 { return 0 } +func (s statWin) gid() uint32 { return 0 } +func (s statWin) rdev() uint64 { return 0 } + +func (s statWin) size() int64 { + return int64(s.FileSizeLow) | (int64(s.FileSizeHigh) << 32) +} + +func (s statWin) atim() syscall.Timespec { + return syscall.NsecToTimespec(s.LastAccessTime.Nanoseconds()) +} + +func (s statWin) mtim() syscall.Timespec { + return syscall.NsecToTimespec(s.LastWriteTime.Nanoseconds()) +} + +func (s statWin) ctim() syscall.Timespec { + return syscall.NsecToTimespec(s.CreationTime.Nanoseconds()) +}