diff --git a/backend/local/local.go b/backend/local/local.go index cc95bb5b4..6f5d9d976 100644 --- a/backend/local/local.go +++ b/backend/local/local.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "sort" + "sync" "github.com/restic/restic/backend" ) @@ -15,7 +16,9 @@ import ( var ErrWrongData = errors.New("wrong data returned by backend, checksum does not match") type Local struct { - p string + p string + mu sync.Mutex + open map[string][]*os.File // Contains open files. Guarded by 'mu'. } // Open opens the local backend at dir. @@ -37,7 +40,7 @@ func Open(dir string) (*Local, error) { } } - return &Local{p: dir}, nil + return &Local{p: dir, open: make(map[string][]*os.File)}, nil } // Create creates all the necessary files and directories for a new local @@ -143,7 +146,7 @@ func (lb *localBlob) Finalize(t backend.Type, name string) error { return err } - return os.Chmod(f, fi.Mode()&os.FileMode(^uint32(0222))) + return setNewFileMode(f, fi) } // Create creates a new Blob. The data is available only after Finalize() @@ -162,6 +165,11 @@ func (b *Local) Create() (backend.Blob, error) { basedir: b.p, } + b.mu.Lock() + open, _ := b.open["blobs"] + b.open["blobs"] = append(open, file) + b.mu.Unlock() + return &blob, nil } @@ -198,7 +206,15 @@ func dirname(base string, t backend.Type, name string) string { // Get returns a reader that yields the content stored under the given // name. The reader should be closed after draining it. func (b *Local) Get(t backend.Type, name string) (io.ReadCloser, error) { - return os.Open(filename(b.p, t, name)) + file, err := os.Open(filename(b.p, t, name)) + if err != nil { + return nil, err + } + b.mu.Lock() + open, _ := b.open[filename(b.p, t, name)] + b.open[filename(b.p, t, name)] = append(open, file) + b.mu.Unlock() + return file, nil } // GetReader returns an io.ReadCloser for the Blob with the given name of @@ -209,6 +225,11 @@ func (b *Local) GetReader(t backend.Type, name string, offset, length uint) (io. return nil, err } + b.mu.Lock() + open, _ := b.open[filename(b.p, t, name)] + b.open[filename(b.p, t, name)] = append(open, f) + b.mu.Unlock() + _, err = f.Seek(int64(offset), 0) if err != nil { return nil, err @@ -236,7 +257,17 @@ func (b *Local) Test(t backend.Type, name string) (bool, error) { // Remove removes the blob with the given name and type. func (b *Local) Remove(t backend.Type, name string) error { - return os.Remove(filename(b.p, t, name)) + // close all open files we may have. + fn := filename(b.p, t, name) + b.mu.Lock() + open, _ := b.open[fn] + for _, file := range open { + file.Close() + } + b.open[fn] = nil + b.mu.Unlock() + + return os.Remove(fn) } // List returns a channel that yields all names of blobs of type t. A @@ -283,7 +314,22 @@ func (b *Local) List(t backend.Type, done <-chan struct{}) <-chan string { } // Delete removes the repository and all files. -func (b *Local) Delete() error { return os.RemoveAll(b.p) } +func (b *Local) Delete() error { + b.Close() + return os.RemoveAll(b.p) +} -// Close does nothing -func (b *Local) Close() error { return nil } +// Close closes all open files. +// They may have been closed already, +// so we ignore all errors. +func (b *Local) Close() error { + b.mu.Lock() + for _, open := range b.open { + for _, file := range open { + file.Close() + } + } + b.open = make(map[string][]*os.File) + b.mu.Unlock() + return nil +} diff --git a/backend/local/local_unix.go b/backend/local/local_unix.go new file mode 100644 index 000000000..8b8ecec69 --- /dev/null +++ b/backend/local/local_unix.go @@ -0,0 +1,12 @@ +// +build !windows + +package local + +import ( + "os" +) + +// set file to readonly +func setNewFileMode(f string, fi os.FileInfo) error { + return os.Chmod(f, fi.Mode()&os.FileMode(^uint32(0222))) +} diff --git a/backend/local/local_windows.go b/backend/local/local_windows.go new file mode 100644 index 000000000..73633fa3e --- /dev/null +++ b/backend/local/local_windows.go @@ -0,0 +1,12 @@ +package local + +import ( + "os" +) + +// We don't modify read-only on windows, +// since it will make us unable to delete the file, +// and this isn't common practice on this platform. +func setNewFileMode(f string, fi os.FileInfo) error { + return nil +} diff --git a/backend/sftp/sftp.go b/backend/sftp/sftp.go index 71c874f0b..90cb687a9 100644 --- a/backend/sftp/sftp.go +++ b/backend/sftp/sftp.go @@ -10,7 +10,6 @@ import ( "os/exec" "path/filepath" "sort" - "syscall" "github.com/juju/errors" "github.com/pkg/sftp" @@ -37,7 +36,7 @@ func startClient(program string, args ...string) (*SFTP, error) { cmd.Stderr = os.Stderr // ignore signals sent to the parent (e.g. SIGINT) - cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} + cmd.SysProcAttr = ignoreSigIntProcAttr() // get stdin and stdout wr, err := cmd.StdinPipe() diff --git a/backend/sftp/sftp_unix.go b/backend/sftp/sftp_unix.go new file mode 100644 index 000000000..f924f0d05 --- /dev/null +++ b/backend/sftp/sftp_unix.go @@ -0,0 +1,13 @@ +// +build !windows + +package sftp + +import ( + "syscall" +) + +// ignoreSigIntProcAttr returns a syscall.SysProcAttr that +// disables SIGINT on parent. +func ignoreSigIntProcAttr() *syscall.SysProcAttr { + return &syscall.SysProcAttr{Setsid: true} +} diff --git a/backend/sftp/sftp_windows.go b/backend/sftp/sftp_windows.go new file mode 100644 index 000000000..62f748c6d --- /dev/null +++ b/backend/sftp/sftp_windows.go @@ -0,0 +1,11 @@ +package sftp + +import ( + "syscall" +) + +// ignoreSigIntProcAttr returns a default syscall.SysProcAttr +// on Windows. +func ignoreSigIntProcAttr() *syscall.SysProcAttr { + return &syscall.SysProcAttr{} +} diff --git a/build.go b/build.go index 765432f0c..c62cd8574 100644 --- a/build.go +++ b/build.go @@ -10,6 +10,7 @@ import ( "os/exec" "path" "path/filepath" + "runtime" "strings" "time" ) @@ -258,11 +259,17 @@ func main() { version := getVersion() compileTime := time.Now().Format(timeFormat) + output := "restic" + if runtime.GOOS == "windows" { + output = "restic.exe" + } + args := []string{ "-tags", strings.Join(buildTags, " "), "-ldflags", fmt.Sprintf(`-s -X main.version %q -X main.compiledAt %q`, version, compileTime), - "-o", "restic", "github.com/restic/restic/cmd/restic", + "-o", output, "github.com/restic/restic/cmd/restic", } + err = build(gopath, args...) if err != nil { fmt.Fprintf(os.Stderr, "build failed: %v\n", err) diff --git a/cache.go b/cache.go index 4cdde46f0..8dfd9ff84 100644 --- a/cache.go +++ b/cache.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + "runtime" "strings" "github.com/restic/restic/backend" @@ -212,10 +213,40 @@ func getCacheDir() (string, error) { if dir := os.Getenv("RESTIC_CACHE"); dir != "" { return dir, nil } + if runtime.GOOS == "windows" { + return getWindowsCacheDir() + } return getXDGCacheDir() } +// getWindowsCacheDir will return %APPDATA%\restic or create +// a folder in the temporary folder called "restic". +func getWindowsCacheDir() (string, error) { + cachedir := os.Getenv("APPDATA") + if cachedir == "" { + cachedir = os.TempDir() + } + cachedir = filepath.Join(cachedir, "restic") + fi, err := os.Stat(cachedir) + + if os.IsNotExist(err) { + err = os.MkdirAll(cachedir, 0700) + if err != nil { + return "", err + } + } + + if err != nil { + return "", err + } + + if !fi.IsDir() { + return "", fmt.Errorf("cache dir %v is not a directory", cachedir) + } + return cachedir, nil +} + // getXDGCacheDir returns the cache directory according to XDG basedir spec, see // http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html func getXDGCacheDir() (string, error) { diff --git a/cmd/restic/cmd_mount.go b/cmd/restic/cmd_mount.go index dc2ad673d..b8d8cb277 100644 --- a/cmd/restic/cmd_mount.go +++ b/cmd/restic/cmd_mount.go @@ -1,4 +1,5 @@ // +build !openbsd +// +build !windows package main diff --git a/cmd/restic/global.go b/cmd/restic/global.go index b71f7aa25..46ee2e011 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -132,7 +132,14 @@ func (o GlobalOptions) OpenRepository() (*repository.Repository, error) { // * s3://region/bucket -> amazon s3 bucket // * sftp://user@host/foo/bar -> remote sftp repository on host for user at path foo/bar // * sftp://host//tmp/backup -> remote sftp repository on host at path /tmp/backup +// * c:\temp -> local repository at c:\temp - the path must exist func open(u string) (backend.Backend, error) { + // check if the url is a directory that exists + fi, err := os.Stat(u) + if err == nil && fi.IsDir() { + return local.Open(u) + } + url, err := url.Parse(u) if err != nil { return nil, err @@ -140,7 +147,13 @@ func open(u string) (backend.Backend, error) { if url.Scheme == "" { return local.Open(url.Path) - } else if url.Scheme == "s3" { + } + + if len(url.Path) < 1 { + return nil, fmt.Errorf("unable to parse url %v", url) + } + + if url.Scheme == "s3" { return s3.Open(url.Host, url.Path[1:]) } @@ -156,6 +169,12 @@ func open(u string) (backend.Backend, error) { // Create the backend specified by URI. func create(u string) (backend.Backend, error) { + // check if the url is a directory that exists + fi, err := os.Stat(u) + if err == nil && fi.IsDir() { + return local.Create(u) + } + url, err := url.Parse(u) if err != nil { return nil, err @@ -163,7 +182,13 @@ func create(u string) (backend.Backend, error) { if url.Scheme == "" { return local.Create(url.Path) - } else if url.Scheme == "s3" { + } + + if len(url.Path) < 1 { + return nil, fmt.Errorf("unable to parse url %v", url) + } + + if url.Scheme == "s3" { return s3.Open(url.Host, url.Path[1:]) } diff --git a/cmd/restic/integration_fuse_test.go b/cmd/restic/integration_fuse_test.go index 897e1fc75..5a6d26ac9 100644 --- a/cmd/restic/integration_fuse_test.go +++ b/cmd/restic/integration_fuse_test.go @@ -1,4 +1,5 @@ // +build !openbsd +// +build !windows package main diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index 6fd90ac3f..a31198a53 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -6,7 +6,6 @@ import ( "os" "path/filepath" "runtime" - "syscall" "testing" . "github.com/restic/restic/test" @@ -70,38 +69,6 @@ func sameModTime(fi1, fi2 os.FileInfo) bool { return fi1.ModTime() == fi2.ModTime() } -func (e *dirEntry) equals(other *dirEntry) bool { - if e.path != other.path { - fmt.Fprintf(os.Stderr, "%v: path does not match (%v != %v)\n", e.path, e.path, other.path) - return false - } - - if e.fi.Mode() != other.fi.Mode() { - fmt.Fprintf(os.Stderr, "%v: mode does not match (%v != %v)\n", e.path, e.fi.Mode(), other.fi.Mode()) - return false - } - - if !sameModTime(e.fi, other.fi) { - fmt.Fprintf(os.Stderr, "%v: ModTime does not match (%v != %v)\n", e.path, e.fi.ModTime(), other.fi.ModTime()) - return false - } - - stat, _ := e.fi.Sys().(*syscall.Stat_t) - stat2, _ := other.fi.Sys().(*syscall.Stat_t) - - if stat.Uid != stat2.Uid { - fmt.Fprintf(os.Stderr, "%v: UID does not match (%v != %v)\n", e.path, stat.Uid, stat2.Uid) - return false - } - - if stat.Gid != stat2.Gid { - fmt.Fprintf(os.Stderr, "%v: GID does not match (%v != %v)\n", e.path, stat.Gid, stat2.Gid) - return false - } - - return true -} - // directoriesEqualContents checks if both directories contain exactly the same // contents. func directoriesEqualContents(dir1, dir2 string) bool { @@ -237,6 +204,8 @@ func withTestEnvironment(t testing.TB, f func(*testEnvironment, GlobalOptions)) } OK(t, os.MkdirAll(env.testdata, 0700)) + OK(t, os.MkdirAll(env.cache, 0700)) + OK(t, os.MkdirAll(env.repo, 0700)) f(&env, configureRestic(t, env.cache, env.repo)) diff --git a/cmd/restic/integration_helpers_unix_test.go b/cmd/restic/integration_helpers_unix_test.go new file mode 100644 index 000000000..a182898e8 --- /dev/null +++ b/cmd/restic/integration_helpers_unix_test.go @@ -0,0 +1,41 @@ +//+build !windows + +package main + +import ( + "fmt" + "os" + "syscall" +) + +func (e *dirEntry) equals(other *dirEntry) bool { + if e.path != other.path { + fmt.Fprintf(os.Stderr, "%v: path does not match (%v != %v)\n", e.path, e.path, other.path) + return false + } + + if e.fi.Mode() != other.fi.Mode() { + fmt.Fprintf(os.Stderr, "%v: mode does not match (%v != %v)\n", e.path, e.fi.Mode(), other.fi.Mode()) + return false + } + + if !sameModTime(e.fi, other.fi) { + fmt.Fprintf(os.Stderr, "%v: ModTime does not match (%v != %v)\n", e.path, e.fi.ModTime(), other.fi.ModTime()) + return false + } + + stat, _ := e.fi.Sys().(*syscall.Stat_t) + stat2, _ := other.fi.Sys().(*syscall.Stat_t) + + if stat.Uid != stat2.Uid { + fmt.Fprintf(os.Stderr, "%v: UID does not match (%v != %v)\n", e.path, stat.Uid, stat2.Uid) + return false + } + + if stat.Gid != stat2.Gid { + fmt.Fprintf(os.Stderr, "%v: GID does not match (%v != %v)\n", e.path, stat.Gid, stat2.Gid) + return false + } + + return true +} diff --git a/cmd/restic/integration_helpers_windows_test.go b/cmd/restic/integration_helpers_windows_test.go new file mode 100644 index 000000000..d67e9ca11 --- /dev/null +++ b/cmd/restic/integration_helpers_windows_test.go @@ -0,0 +1,27 @@ +//+build windows + +package main + +import ( + "fmt" + "os" +) + +func (e *dirEntry) equals(other *dirEntry) bool { + if e.path != other.path { + fmt.Fprintf(os.Stderr, "%v: path does not match (%v != %v)\n", e.path, e.path, other.path) + return false + } + + if e.fi.Mode() != other.fi.Mode() { + fmt.Fprintf(os.Stderr, "%v: mode does not match (%v != %v)\n", e.path, e.fi.Mode(), other.fi.Mode()) + return false + } + + if !sameModTime(e.fi, other.fi) { + fmt.Fprintf(os.Stderr, "%v: ModTime does not match (%v != %v)\n", e.path, e.fi.ModTime(), other.fi.ModTime()) + return false + } + + return true +} diff --git a/filter/filter.go b/filter/filter.go index f8c335e34..4092b4025 100644 --- a/filter/filter.go +++ b/filter/filter.go @@ -25,8 +25,14 @@ func Match(pattern, str string) (matched bool, err error) { return false, ErrBadString } - patterns := strings.Split(pattern, string(filepath.Separator)) - strs := strings.Split(str, string(filepath.Separator)) + // convert file path separator to '/' + if filepath.Separator != '/' { + pattern = strings.Replace(pattern, string(filepath.Separator), "/", -1) + str = strings.Replace(str, string(filepath.Separator), "/", -1) + } + + patterns := strings.Split(pattern, "/") + strs := strings.Split(str, "/") return match(patterns, strs) } diff --git a/filter/filter_test.go b/filter/filter_test.go index 78e731b68..15892e910 100644 --- a/filter/filter_test.go +++ b/filter/filter_test.go @@ -5,6 +5,8 @@ import ( "compress/bzip2" "fmt" "os" + "path/filepath" + "strings" "testing" "github.com/restic/restic/filter" @@ -71,20 +73,40 @@ var matchTests = []struct { {"foo/**/bar", "/home/user/foo/x/y/bar/main.go", true}, {"user/**/important*", "/home/user/work/x/y/hidden/x", false}, {"user/**/hidden*/**/c", "/home/user/work/x/y/hidden/z/a/b/c", true}, + {"c:/foo/*test.*", "c:/foo/bar/test.go", false}, + {"c:/foo/*/test.*", "c:/foo/bar/test.go", true}, + {"c:/foo/*/bar/test.*", "c:/foo/bar/test.go", false}, +} + +func testpattern(t *testing.T, pattern, path string, shouldMatch bool) { + match, err := filter.Match(pattern, path) + if err != nil { + t.Errorf("test pattern %q failed: expected no error for path %q, but error returned: %v", + pattern, path, err) + } + + if match != shouldMatch { + t.Errorf("test: filter.Match(%q, %q): expected %v, got %v", + pattern, path, shouldMatch, match) + } } func TestMatch(t *testing.T) { - for i, test := range matchTests { - match, err := filter.Match(test.pattern, test.path) - if err != nil { - t.Errorf("test %d failed: expected no error for pattern %q, but error returned: %v", - i, test.pattern, err) - continue - } + for _, test := range matchTests { + testpattern(t, test.pattern, test.path, test.match) - if match != test.match { - t.Errorf("test %d: filter.Match(%q, %q): expected %v, got %v", - i, test.pattern, test.path, test.match, match) + // Test with native path separator + if filepath.Separator != '/' { + // Test with pattern as native + pattern := strings.Replace(test.pattern, "/", string(filepath.Separator), -1) + testpattern(t, pattern, test.path, test.match) + + // Test with path as native + path := strings.Replace(test.path, "/", string(filepath.Separator), -1) + testpattern(t, test.pattern, path, test.match) + + // Test with both pattern and path as native + testpattern(t, pattern, path, test.match) } } } diff --git a/lock.go b/lock.go index c662f1718..0d4000fec 100644 --- a/lock.go +++ b/lock.go @@ -5,7 +5,6 @@ import ( "os" "os/signal" "os/user" - "strconv" "sync" "syscall" "time" @@ -116,19 +115,8 @@ func (l *Lock) fillUserInfo() error { } l.Username = usr.Username - uid, err := strconv.ParseInt(usr.Uid, 10, 32) - if err != nil { - return err - } - l.UID = uint32(uid) - - gid, err := strconv.ParseInt(usr.Gid, 10, 32) - if err != nil { - return err - } - l.GID = uint32(gid) - - return nil + l.UID, l.GID, err = uidGidInt(*usr) + return err } // checkForOtherLocks looks for other locks that currently exist in the repository. @@ -206,17 +194,10 @@ func (l *Lock) Stale() bool { return true } - proc, err := os.FindProcess(l.PID) - defer proc.Release() - if err != nil { - debug.Log("Lock.Stale", "error searching for process %d: %v\n", l.PID, err) - return true - } - - debug.Log("Lock.Stale", "sending SIGHUP to process %d\n", l.PID) - err = proc.Signal(syscall.SIGHUP) - if err != nil { - debug.Log("Lock.Stale", "signal error: %v, lock is probably stale\n", err) + // check if we can reach the process retaining the lock + exists := l.processExists() + if !exists { + debug.Log("Lock.Stale", "could not reach process, %d, lock is probably stale\n", l.PID) return true } diff --git a/lock_test.go b/lock_test.go index a2b5cb2d3..c07abb239 100644 --- a/lock_test.go +++ b/lock_test.go @@ -124,7 +124,7 @@ var staleLockTests = []struct { { timestamp: time.Now(), stale: true, - pid: os.Getpid() + 500, + pid: os.Getpid() + 500000, }, } @@ -158,7 +158,7 @@ func TestLockWithStaleLock(t *testing.T) { id2, err := createFakeLock(repo, time.Now().Add(-time.Minute), os.Getpid()) OK(t, err) - id3, err := createFakeLock(repo, time.Now().Add(-time.Minute), os.Getpid()+500) + id3, err := createFakeLock(repo, time.Now().Add(-time.Minute), os.Getpid()+500000) OK(t, err) OK(t, restic.RemoveStaleLocks(repo)) diff --git a/lock_unix.go b/lock_unix.go new file mode 100644 index 000000000..aaf0cfdd5 --- /dev/null +++ b/lock_unix.go @@ -0,0 +1,48 @@ +// +build !windows + +package restic + +import ( + "os" + "os/user" + "strconv" + "syscall" + + "github.com/restic/restic/debug" +) + +// uidGidInt returns uid, gid of the user as a number. +func uidGidInt(u user.User) (uid, gid uint32, err error) { + var ui, gi int64 + ui, err = strconv.ParseInt(u.Uid, 10, 32) + if err != nil { + return + } + gi, err = strconv.ParseInt(u.Gid, 10, 32) + if err != nil { + return + } + uid = uint32(ui) + gid = uint32(gi) + return +} + +// checkProcess will check if the process retaining the lock +// exists and responds to SIGHUP signal. +// Returns true if the process exists and responds. +func (l Lock) processExists() bool { + proc, err := os.FindProcess(l.PID) + if err != nil { + debug.Log("Lock.Stale", "error searching for process %d: %v\n", l.PID, err) + return false + } + defer proc.Release() + + debug.Log("Lock.Stale", "sending SIGHUP to process %d\n", l.PID) + err = proc.Signal(syscall.SIGHUP) + if err != nil { + debug.Log("Lock.Stale", "signal error: %v, lock is probably stale\n", err) + return false + } + return true +} diff --git a/lock_windows.go b/lock_windows.go new file mode 100644 index 000000000..fc700a9b5 --- /dev/null +++ b/lock_windows.go @@ -0,0 +1,25 @@ +package restic + +import ( + "os" + "os/user" + + "github.com/restic/restic/debug" +) + +// uidGidInt always returns 0 on Windows, since uid isn't numbers +func uidGidInt(u user.User) (uid, gid uint32, err error) { + return 0, 0, nil +} + +// checkProcess will check if the process retaining the lock exists. +// Returns true if the process exists. +func (l Lock) processExists() bool { + proc, err := os.FindProcess(l.PID) + if err != nil { + debug.Log("Lock.Stale", "error searching for process %d: %v\n", l.PID, err) + return false + } + proc.Release() + return true +} diff --git a/node.go b/node.go index e607bc8b1..e79306d5d 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 or size 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..eec07fc5a --- /dev/null +++ b/node_unix.go @@ -0,0 +1,32 @@ +// +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..5a5a9dba3 --- /dev/null +++ b/node_windows.go @@ -0,0 +1,63 @@ +package restic + +import ( + "errors" + "os" + "syscall" +) + +func (node *Node) OpenForReading() (*os.File, error) { + return os.OpenFile(node.path, os.O_RDONLY, 0) +} + +// 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) { + return errors.New("device nodes cannot be created on windows") +} + +// 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()) +} diff --git a/restorer.go b/restorer.go index 524a9c734..d9c6ba0e3 100644 --- a/restorer.go +++ b/restorer.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "syscall" "github.com/restic/restic/backend" "github.com/restic/restic/debug" @@ -96,16 +95,13 @@ func (res *Restorer) restoreNodeTo(node *Node, dir string, dst string) error { } // Did it fail because of ENOENT? - if pe, ok := errors.Cause(err).(*os.PathError); ok { - errn, ok := pe.Err.(syscall.Errno) - if ok && errn == syscall.ENOENT { - debug.Log("Restorer.restoreNodeTo", "create intermediate paths") + if err != nil && os.IsNotExist(errors.Cause(err)) { + debug.Log("Restorer.restoreNodeTo", "create intermediate paths") - // Create parent directories and retry - err = os.MkdirAll(filepath.Dir(dstPath), 0700) - if err == nil || err == os.ErrExist { - err = node.CreateAt(dstPath, res.repo) - } + // Create parent directories and retry + err = os.MkdirAll(filepath.Dir(dstPath), 0700) + if err == nil || err == os.ErrExist { + err = node.CreateAt(dstPath, res.repo) } } diff --git a/snapshot.go b/snapshot.go index b1477c316..91c40a62d 100644 --- a/snapshot.go +++ b/snapshot.go @@ -5,7 +5,6 @@ import ( "os" "os/user" "path/filepath" - "strconv" "time" "github.com/restic/restic/backend" @@ -76,19 +75,9 @@ func (sn *Snapshot) fillUserInfo() error { } sn.Username = usr.Username - uid, err := strconv.ParseInt(usr.Uid, 10, 32) - if err != nil { - return err - } - sn.UID = uint32(uid) - - gid, err := strconv.ParseInt(usr.Gid, 10, 32) - if err != nil { - return err - } - sn.GID = uint32(gid) - - return nil + // set userid and groupid + sn.UID, sn.GID, err = uidGidInt(*usr) + return err } // FindSnapshot takes a string and tries to find a snapshot whose ID matches