2
2
mirror of https://github.com/octoleo/restic.git synced 2024-11-22 21:05:10 +00:00

Merge pull request #259 from klauspost/windows-support-rebased

Add Windows support
This commit is contained in:
Alexander Neumann 2015-08-18 20:25:58 +02:00
commit a820719c07
30 changed files with 555 additions and 173 deletions

View File

@ -8,6 +8,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"sync"
"github.com/restic/restic/backend" "github.com/restic/restic/backend"
) )
@ -15,7 +16,9 @@ import (
var ErrWrongData = errors.New("wrong data returned by backend, checksum does not match") var ErrWrongData = errors.New("wrong data returned by backend, checksum does not match")
type Local struct { 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. // 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 // 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 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() // 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, basedir: b.p,
} }
b.mu.Lock()
open, _ := b.open["blobs"]
b.open["blobs"] = append(open, file)
b.mu.Unlock()
return &blob, nil 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 // Get returns a reader that yields the content stored under the given
// name. The reader should be closed after draining it. // name. The reader should be closed after draining it.
func (b *Local) Get(t backend.Type, name string) (io.ReadCloser, error) { 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 // 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 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) _, err = f.Seek(int64(offset), 0)
if err != nil { if err != nil {
return nil, err 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. // Remove removes the blob with the given name and type.
func (b *Local) Remove(t backend.Type, name string) error { 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 // 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. // 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 // Close closes all open files.
func (b *Local) Close() error { return nil } // 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
}

View File

@ -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)))
}

View File

@ -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
}

View File

@ -10,7 +10,6 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort" "sort"
"syscall"
"github.com/juju/errors" "github.com/juju/errors"
"github.com/pkg/sftp" "github.com/pkg/sftp"
@ -37,7 +36,7 @@ func startClient(program string, args ...string) (*SFTP, error) {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
// ignore signals sent to the parent (e.g. SIGINT) // ignore signals sent to the parent (e.g. SIGINT)
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} cmd.SysProcAttr = ignoreSigIntProcAttr()
// get stdin and stdout // get stdin and stdout
wr, err := cmd.StdinPipe() wr, err := cmd.StdinPipe()

13
backend/sftp/sftp_unix.go Normal file
View File

@ -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}
}

View File

@ -0,0 +1,11 @@
package sftp
import (
"syscall"
)
// ignoreSigIntProcAttr returns a default syscall.SysProcAttr
// on Windows.
func ignoreSigIntProcAttr() *syscall.SysProcAttr {
return &syscall.SysProcAttr{}
}

View File

@ -10,6 +10,7 @@ import (
"os/exec" "os/exec"
"path" "path"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"time" "time"
) )
@ -258,11 +259,17 @@ func main() {
version := getVersion() version := getVersion()
compileTime := time.Now().Format(timeFormat) compileTime := time.Now().Format(timeFormat)
output := "restic"
if runtime.GOOS == "windows" {
output = "restic.exe"
}
args := []string{ args := []string{
"-tags", strings.Join(buildTags, " "), "-tags", strings.Join(buildTags, " "),
"-ldflags", fmt.Sprintf(`-s -X main.version %q -X main.compiledAt %q`, version, compileTime), "-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...) err = build(gopath, args...)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "build failed: %v\n", err) fmt.Fprintf(os.Stderr, "build failed: %v\n", err)

View File

@ -6,6 +6,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"github.com/restic/restic/backend" "github.com/restic/restic/backend"
@ -212,10 +213,40 @@ func getCacheDir() (string, error) {
if dir := os.Getenv("RESTIC_CACHE"); dir != "" { if dir := os.Getenv("RESTIC_CACHE"); dir != "" {
return dir, nil return dir, nil
} }
if runtime.GOOS == "windows" {
return getWindowsCacheDir()
}
return getXDGCacheDir() 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 // getXDGCacheDir returns the cache directory according to XDG basedir spec, see
// http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html // http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
func getXDGCacheDir() (string, error) { func getXDGCacheDir() (string, error) {

View File

@ -1,4 +1,5 @@
// +build !openbsd // +build !openbsd
// +build !windows
package main package main

View File

@ -132,7 +132,14 @@ func (o GlobalOptions) OpenRepository() (*repository.Repository, error) {
// * s3://region/bucket -> amazon s3 bucket // * s3://region/bucket -> amazon s3 bucket
// * sftp://user@host/foo/bar -> remote sftp repository on host for user at path foo/bar // * 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 // * 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) { 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) url, err := url.Parse(u)
if err != nil { if err != nil {
return nil, err return nil, err
@ -140,7 +147,13 @@ func open(u string) (backend.Backend, error) {
if url.Scheme == "" { if url.Scheme == "" {
return local.Open(url.Path) 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:]) 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. // Create the backend specified by URI.
func create(u string) (backend.Backend, error) { 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) url, err := url.Parse(u)
if err != nil { if err != nil {
return nil, err return nil, err
@ -163,7 +182,13 @@ func create(u string) (backend.Backend, error) {
if url.Scheme == "" { if url.Scheme == "" {
return local.Create(url.Path) 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:]) return s3.Open(url.Host, url.Path[1:])
} }

View File

@ -1,4 +1,5 @@
// +build !openbsd // +build !openbsd
// +build !windows
package main package main

View File

@ -6,7 +6,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"syscall"
"testing" "testing"
. "github.com/restic/restic/test" . "github.com/restic/restic/test"
@ -70,38 +69,6 @@ func sameModTime(fi1, fi2 os.FileInfo) bool {
return fi1.ModTime() == fi2.ModTime() 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 // directoriesEqualContents checks if both directories contain exactly the same
// contents. // contents.
func directoriesEqualContents(dir1, dir2 string) bool { 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.testdata, 0700))
OK(t, os.MkdirAll(env.cache, 0700))
OK(t, os.MkdirAll(env.repo, 0700))
f(&env, configureRestic(t, env.cache, env.repo)) f(&env, configureRestic(t, env.cache, env.repo))

View File

@ -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
}

View File

@ -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
}

View File

@ -25,8 +25,14 @@ func Match(pattern, str string) (matched bool, err error) {
return false, ErrBadString return false, ErrBadString
} }
patterns := strings.Split(pattern, string(filepath.Separator)) // convert file path separator to '/'
strs := strings.Split(str, string(filepath.Separator)) 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) return match(patterns, strs)
} }

View File

@ -5,6 +5,8 @@ import (
"compress/bzip2" "compress/bzip2"
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings"
"testing" "testing"
"github.com/restic/restic/filter" "github.com/restic/restic/filter"
@ -71,20 +73,40 @@ var matchTests = []struct {
{"foo/**/bar", "/home/user/foo/x/y/bar/main.go", true}, {"foo/**/bar", "/home/user/foo/x/y/bar/main.go", true},
{"user/**/important*", "/home/user/work/x/y/hidden/x", false}, {"user/**/important*", "/home/user/work/x/y/hidden/x", false},
{"user/**/hidden*/**/c", "/home/user/work/x/y/hidden/z/a/b/c", true}, {"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) { func TestMatch(t *testing.T) {
for i, test := range matchTests { for _, test := range matchTests {
match, err := filter.Match(test.pattern, test.path) testpattern(t, test.pattern, test.path, test.match)
if err != nil {
t.Errorf("test %d failed: expected no error for pattern %q, but error returned: %v",
i, test.pattern, err)
continue
}
if match != test.match { // Test with native path separator
t.Errorf("test %d: filter.Match(%q, %q): expected %v, got %v", if filepath.Separator != '/' {
i, test.pattern, test.path, test.match, match) // 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)
} }
} }
} }

31
lock.go
View File

@ -5,7 +5,6 @@ import (
"os" "os"
"os/signal" "os/signal"
"os/user" "os/user"
"strconv"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@ -116,19 +115,8 @@ func (l *Lock) fillUserInfo() error {
} }
l.Username = usr.Username l.Username = usr.Username
uid, err := strconv.ParseInt(usr.Uid, 10, 32) l.UID, l.GID, err = uidGidInt(*usr)
if err != nil { return err
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
} }
// checkForOtherLocks looks for other locks that currently exist in the repository. // checkForOtherLocks looks for other locks that currently exist in the repository.
@ -206,17 +194,10 @@ func (l *Lock) Stale() bool {
return true return true
} }
proc, err := os.FindProcess(l.PID) // check if we can reach the process retaining the lock
defer proc.Release() exists := l.processExists()
if err != nil { if !exists {
debug.Log("Lock.Stale", "error searching for process %d: %v\n", l.PID, err) debug.Log("Lock.Stale", "could not reach process, %d, lock is probably stale\n", l.PID)
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)
return true return true
} }

View File

@ -124,7 +124,7 @@ var staleLockTests = []struct {
{ {
timestamp: time.Now(), timestamp: time.Now(),
stale: true, 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()) id2, err := createFakeLock(repo, time.Now().Add(-time.Minute), os.Getpid())
OK(t, err) 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, err)
OK(t, restic.RemoveStaleLocks(repo)) OK(t, restic.RemoveStaleLocks(repo))

48
lock_unix.go Normal file
View File

@ -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
}

25
lock_windows.go Normal file
View File

@ -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
}

78
node.go
View File

@ -15,6 +15,7 @@ import (
"github.com/restic/restic/debug" "github.com/restic/restic/debug"
"github.com/restic/restic/pack" "github.com/restic/restic/pack"
"github.com/restic/restic/repository" "github.com/restic/restic/repository"
"runtime"
) )
// Node is a file, directory or other item in a backup. // 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 { func (node Node) restoreMetadata(path string) error {
var err 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 { if err != nil {
return errors.Annotate(err, "Lchown") 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 { 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) err := os.Symlink(node.LinkTarget, path)
if err != nil { if err != nil {
return errors.Annotate(err, "Symlink") return errors.Annotate(err, "Symlink")
@ -245,15 +250,15 @@ func (node Node) createSymlinkAt(path string) error {
} }
func (node *Node) createDevAt(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 { 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 { func (node *Node) createFifoAt(path string) error {
return syscall.Mkfifo(path, 0600) return mkfifo(path, 0600)
} }
func (node Node) MarshalJSON() ([]byte, error) { func (node Node) MarshalJSON() ([]byte, error) {
@ -381,9 +386,19 @@ func (node *Node) isNewer(path string, fi os.FileInfo) bool {
return true return true
} }
extendedStat := fi.Sys().(*syscall.Stat_t) size := uint64(fi.Size())
inode := extendedStat.Ino
size := uint64(extendedStat.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() || if node.ModTime != fi.ModTime() ||
node.ChangeTime != changeTime(extendedStat) || node.ChangeTime != changeTime(extendedStat) ||
@ -397,11 +412,11 @@ func (node *Node) isNewer(path string, fi os.FileInfo) bool {
return false return false
} }
func (node *Node) fillUser(stat *syscall.Stat_t) error { func (node *Node) fillUser(stat statT) error {
node.UID = stat.Uid node.UID = stat.uid()
node.GID = stat.Gid node.GID = stat.gid()
username, err := lookupUsername(strconv.Itoa(int(stat.Uid))) username, err := lookupUsername(strconv.Itoa(int(stat.uid())))
if err != nil { if err != nil {
return errors.Annotate(err, "fillUser") 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 { func (node *Node) fillExtra(path string, fi os.FileInfo) error {
stat, ok := fi.Sys().(*syscall.Stat_t) stat, ok := toStatT(fi.Sys())
if !ok { if !ok {
return nil return nil
} }
node.Inode = uint64(stat.Ino) node.Inode = uint64(stat.ino())
node.fillTimes(stat) node.fillTimes(stat)
@ -456,15 +471,15 @@ func (node *Node) fillExtra(path string, fi os.FileInfo) error {
switch node.Type { switch node.Type {
case "file": case "file":
node.Size = uint64(stat.Size) node.Size = uint64(stat.size())
node.Links = uint64(stat.Nlink) node.Links = uint64(stat.nlink())
case "dir": case "dir":
case "symlink": case "symlink":
node.LinkTarget, err = os.Readlink(path) node.LinkTarget, err = os.Readlink(path)
case "dev": case "dev":
node.Device = uint64(stat.Rdev) node.Device = uint64(stat.rdev())
case "chardev": case "chardev":
node.Device = uint64(stat.Rdev) node.Device = uint64(stat.rdev())
case "fifo": case "fifo":
case "socket": case "socket":
default: default:
@ -473,3 +488,32 @@ func (node *Node) fillExtra(path string, fi os.FileInfo) error {
return err 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())
}

View File

@ -3,22 +3,16 @@ package restic
import ( import (
"os" "os"
"syscall" "syscall"
"time"
) )
func (node *Node) OpenForReading() (*os.File, error) { func (node *Node) OpenForReading() (*os.File, error) {
return os.Open(node.path) 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 { func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
return nil 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 }

View File

@ -3,22 +3,16 @@ package restic
import ( import (
"os" "os"
"syscall" "syscall"
"time"
) )
func (node *Node) OpenForReading() (*os.File, error) { func (node *Node) OpenForReading() (*os.File, error) {
return os.OpenFile(node.path, os.O_RDONLY, 0) 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 { func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
return nil 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 }

View File

@ -4,7 +4,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"syscall" "syscall"
"time"
"unsafe" "unsafe"
"github.com/juju/errors" "github.com/juju/errors"
@ -18,15 +17,6 @@ func (node *Node) OpenForReading() (*os.File, error) {
return file, err 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 { func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
dir, err := os.Open(filepath.Dir(path)) dir, err := os.Open(filepath.Dir(path))
defer dir.Close() 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) { 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) 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 }

View File

@ -3,7 +3,6 @@ package restic
import ( import (
"os" "os"
"syscall" "syscall"
"time"
) )
func (node *Node) OpenForReading() (*os.File, error) { func (node *Node) OpenForReading() (*os.File, error) {
@ -14,15 +13,10 @@ func (node *Node) OpenForReading() (*os.File, error) {
return file, err 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 { func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespec) error {
return nil 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 }

View File

@ -119,6 +119,9 @@ func TestNodeRestoreAt(t *testing.T) {
nodePath := filepath.Join(tempdir, test.Name) nodePath := filepath.Join(tempdir, test.Name)
OK(t, test.CreateAt(nodePath, nil)) OK(t, test.CreateAt(nodePath, nil))
if test.Type == "symlink" && runtime.GOOS == "windows" {
continue
}
if test.Type == "dir" { if test.Type == "dir" {
OK(t, test.RestoreTimestamps(nodePath)) 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) "%v: type doesn't match (%v != %v)", test.Type, test.Type, n2.Type)
Assert(t, test.Size == n2.Size, Assert(t, test.Size == n2.Size,
"%v: size doesn't match (%v != %v)", test.Size, 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" { if runtime.GOOS != "windows" {
Assert(t, test.Mode == n2.Mode, Assert(t, test.UID == n2.UID,
"%v: mode doesn't match (%v != %v)", test.Type, test.Mode, n2.Mode) "%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) AssertFsTimeEqual(t, "AccessTime", test.Type, test.AccessTime, n2.AccessTime)

32
node_unix.go Normal file
View File

@ -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) }

63
node_windows.go Normal file
View File

@ -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())
}

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"syscall"
"github.com/restic/restic/backend" "github.com/restic/restic/backend"
"github.com/restic/restic/debug" "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? // Did it fail because of ENOENT?
if pe, ok := errors.Cause(err).(*os.PathError); ok { if err != nil && os.IsNotExist(errors.Cause(err)) {
errn, ok := pe.Err.(syscall.Errno) debug.Log("Restorer.restoreNodeTo", "create intermediate paths")
if ok && errn == syscall.ENOENT {
debug.Log("Restorer.restoreNodeTo", "create intermediate paths")
// Create parent directories and retry // Create parent directories and retry
err = os.MkdirAll(filepath.Dir(dstPath), 0700) err = os.MkdirAll(filepath.Dir(dstPath), 0700)
if err == nil || err == os.ErrExist { if err == nil || err == os.ErrExist {
err = node.CreateAt(dstPath, res.repo) err = node.CreateAt(dstPath, res.repo)
}
} }
} }

View File

@ -5,7 +5,6 @@ import (
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
"strconv"
"time" "time"
"github.com/restic/restic/backend" "github.com/restic/restic/backend"
@ -76,19 +75,9 @@ func (sn *Snapshot) fillUserInfo() error {
} }
sn.Username = usr.Username sn.Username = usr.Username
uid, err := strconv.ParseInt(usr.Uid, 10, 32) // set userid and groupid
if err != nil { sn.UID, sn.GID, err = uidGidInt(*usr)
return err 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
} }
// FindSnapshot takes a string and tries to find a snapshot whose ID matches // FindSnapshot takes a string and tries to find a snapshot whose ID matches