diff --git a/internal/fs/const.go b/internal/fs/const.go new file mode 100644 index 000000000..dfa6ad5f0 --- /dev/null +++ b/internal/fs/const.go @@ -0,0 +1,16 @@ +package fs + +import "syscall" + +// Flags to OpenFile wrapping those of the underlying system. Not all flags may +// be implemented on a given system. +const ( + O_RDONLY int = syscall.O_RDONLY // open the file read-only. + O_WRONLY int = syscall.O_WRONLY // open the file write-only. + O_RDWR int = syscall.O_RDWR // open the file read-write. + O_APPEND int = syscall.O_APPEND // append data to the file when writing. + O_CREATE int = syscall.O_CREAT // create a new file if none exists. + O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist + O_SYNC int = syscall.O_SYNC // open for synchronous I/O. + O_TRUNC int = syscall.O_TRUNC // if possible, truncate file when opened. +) diff --git a/internal/fs/const_unix.go b/internal/fs/const_unix.go new file mode 100644 index 000000000..a90d171b1 --- /dev/null +++ b/internal/fs/const_unix.go @@ -0,0 +1,8 @@ +// +build !windows + +package fs + +import "syscall" + +// O_NOFOLLOW instructs the kernel to not follow symlinks when opening a file. +const O_NOFOLLOW int = syscall.O_NOFOLLOW diff --git a/internal/fs/const_windows.go b/internal/fs/const_windows.go new file mode 100644 index 000000000..18c89c27e --- /dev/null +++ b/internal/fs/const_windows.go @@ -0,0 +1,6 @@ +// +build windows + +package fs + +// O_NOFOLLOW is a noop on Windows. +const O_NOFOLLOW int = 0 diff --git a/internal/fs/file.go b/internal/fs/file.go index d055107b4..86c519aff 100644 --- a/internal/fs/file.go +++ b/internal/fs/file.go @@ -1,25 +1,11 @@ package fs import ( - "io" "os" "path/filepath" "time" ) -// File is an open file on a file system. -type File interface { - io.Reader - io.Writer - io.Closer - - Fd() uintptr - Readdirnames(n int) ([]string, error) - Readdir(int) ([]os.FileInfo, error) - Seek(int64, int) (int64, error) - Stat() (os.FileInfo, error) -} - // Mkdir creates a new directory with the specified name and permission bits. // If there is an error, it will be of type *PathError. func Mkdir(name string, perm os.FileMode) error { diff --git a/internal/fs/fs_local.go b/internal/fs/fs_local.go new file mode 100644 index 000000000..dd1faafa0 --- /dev/null +++ b/internal/fs/fs_local.go @@ -0,0 +1,96 @@ +package fs + +import ( + "os" + "path/filepath" +) + +// Local is the local file system. Most methods are just passed on to the stdlib. +type Local struct{} + +// statically ensure that Local implements FS. +var _ FS = &Local{} + +// VolumeName returns leading volume name. Given "C:\foo\bar" it returns "C:" +// on Windows. Given "\\host\share\foo" it returns "\\host\share". On other +// platforms it returns "". +func (fs Local) VolumeName(path string) string { + return filepath.VolumeName(path) +} + +// Open opens a file for reading. +func (fs Local) Open(name string) (File, error) { + f, err := os.Open(fixpath(name)) + if err != nil { + return nil, err + } + return f, nil +} + +// OpenFile is the generalized open call; most users will use Open +// or Create instead. It opens the named file with specified flag +// (O_RDONLY etc.) and perm, (0666 etc.) if applicable. If successful, +// methods on the returned File can be used for I/O. +// If there is an error, it will be of type *PathError. +func (fs Local) OpenFile(name string, flag int, perm os.FileMode) (File, error) { + f, err := os.OpenFile(fixpath(name), flag, perm) + if err != nil { + return nil, err + } + return f, nil +} + +// Stat returns a FileInfo describing the named file. If there is an error, it +// will be of type *PathError. +func (fs Local) Stat(name string) (os.FileInfo, error) { + return os.Stat(fixpath(name)) +} + +// Lstat returns the FileInfo structure describing the named file. +// If the file is a symbolic link, the returned FileInfo +// describes the symbolic link. Lstat makes no attempt to follow the link. +// If there is an error, it will be of type *PathError. +func (fs Local) Lstat(name string) (os.FileInfo, error) { + return os.Lstat(fixpath(name)) +} + +// Join joins any number of path elements into a single path, adding a +// Separator if necessary. Join calls Clean on the result; in particular, all +// empty strings are ignored. On Windows, the result is a UNC path if and only +// if the first path element is a UNC path. +func (fs Local) Join(elem ...string) string { + return filepath.Join(elem...) +} + +// Separator returns the OS and FS dependent separator for dirs/subdirs/files. +func (fs Local) Separator() string { + return string(filepath.Separator) +} + +// IsAbs reports whether the path is absolute. +func (fs Local) IsAbs(path string) bool { + return filepath.IsAbs(path) +} + +// Abs returns an absolute representation of path. If the path is not absolute +// it will be joined with the current working directory to turn it into an +// absolute path. The absolute path name for a given file is not guaranteed to +// be unique. Abs calls Clean on the result. +func (fs Local) Abs(path string) (string, error) { + return filepath.Abs(path) +} + +// Clean returns the cleaned path. For details, see filepath.Clean. +func (fs Local) Clean(p string) string { + return filepath.Clean(p) +} + +// Base returns the last element of path. +func (fs Local) Base(path string) string { + return filepath.Base(path) +} + +// Dir returns path without the last element. +func (fs Local) Dir(path string) string { + return filepath.Dir(path) +} diff --git a/internal/fs/fs_reader.go b/internal/fs/fs_reader.go new file mode 100644 index 000000000..385c8f92b --- /dev/null +++ b/internal/fs/fs_reader.go @@ -0,0 +1,289 @@ +package fs + +import ( + "io" + "os" + "path" + "sync" + "syscall" + "time" + + "github.com/restic/restic/internal/errors" +) + +// Reader is a file system which provides a directory with a single file. When +// this file is opened for reading, the reader is passed through. The file can +// be opened once, all subsequent open calls return syscall.EIO. For Lstat(), +// the provided FileInfo is returned. +type Reader struct { + Name string + io.ReadCloser + + Mode os.FileMode + ModTime time.Time + Size int64 + + open sync.Once +} + +// statically ensure that Local implements FS. +var _ FS = &Reader{} + +// VolumeName returns leading volume name, for the Reader file system it's +// always the empty string. +func (fs *Reader) VolumeName(path string) string { + return "" +} + +// Open opens a file for reading. +func (fs *Reader) Open(name string) (f File, err error) { + switch name { + case fs.Name: + fs.open.Do(func() { + f = newReaderFile(fs.ReadCloser, fs.fi()) + }) + + if f == nil { + return nil, syscall.EIO + } + + return f, nil + case "/", ".": + f = fakeDir{ + entries: []os.FileInfo{fs.fi()}, + } + return f, nil + } + + return nil, syscall.ENOENT +} + +func (fs *Reader) fi() os.FileInfo { + return fakeFileInfo{ + name: fs.Name, + size: fs.Size, + mode: fs.Mode, + modtime: fs.ModTime, + } +} + +// OpenFile is the generalized open call; most users will use Open +// or Create instead. It opens the named file with specified flag +// (O_RDONLY etc.) and perm, (0666 etc.) if applicable. If successful, +// methods on the returned File can be used for I/O. +// If there is an error, it will be of type *PathError. +func (fs *Reader) OpenFile(name string, flag int, perm os.FileMode) (f File, err error) { + if flag & ^(O_RDONLY|O_NOFOLLOW) != 0 { + return nil, errors.Errorf("invalid combination of flags 0x%x", flag) + } + + fs.open.Do(func() { + f = newReaderFile(fs.ReadCloser, fs.fi()) + }) + + if f == nil { + return nil, syscall.EIO + } + + return f, nil +} + +// Stat returns a FileInfo describing the named file. If there is an error, it +// will be of type *PathError. +func (fs *Reader) Stat(name string) (os.FileInfo, error) { + return fs.Lstat(name) +} + +// Lstat returns the FileInfo structure describing the named file. +// If the file is a symbolic link, the returned FileInfo +// describes the symbolic link. Lstat makes no attempt to follow the link. +// If there is an error, it will be of type *PathError. +func (fs *Reader) Lstat(name string) (os.FileInfo, error) { + switch name { + case fs.Name: + return fs.fi(), nil + case "/", ".": + fi := fakeFileInfo{ + name: name, + size: 0, + mode: 0755, + modtime: time.Now(), + } + return fi, nil + } + + return nil, os.ErrNotExist +} + +// Join joins any number of path elements into a single path, adding a +// Separator if necessary. Join calls Clean on the result; in particular, all +// empty strings are ignored. On Windows, the result is a UNC path if and only +// if the first path element is a UNC path. +func (fs *Reader) Join(elem ...string) string { + return path.Join(elem...) +} + +// Separator returns the OS and FS dependent separator for dirs/subdirs/files. +func (fs *Reader) Separator() string { + return "/" +} + +// IsAbs reports whether the path is absolute. For the Reader, this is always the case. +func (fs *Reader) IsAbs(p string) bool { + return true +} + +// Abs returns an absolute representation of path. If the path is not absolute +// it will be joined with the current working directory to turn it into an +// absolute path. The absolute path name for a given file is not guaranteed to +// be unique. Abs calls Clean on the result. +// +// For the Reader, all paths are absolute. +func (fs *Reader) Abs(p string) (string, error) { + return path.Clean(p), nil +} + +// Clean returns the cleaned path. For details, see filepath.Clean. +func (fs *Reader) Clean(p string) string { + return path.Clean(p) +} + +// Base returns the last element of p. +func (fs *Reader) Base(p string) string { + return path.Base(p) +} + +// Dir returns p without the last element. +func (fs *Reader) Dir(p string) string { + return path.Dir(p) +} + +func newReaderFile(rd io.ReadCloser, fi os.FileInfo) readerFile { + return readerFile{ + ReadCloser: rd, + fakeFile: fakeFile{ + FileInfo: fi, + name: fi.Name(), + }, + } +} + +type readerFile struct { + io.ReadCloser + fakeFile +} + +func (r readerFile) Read(p []byte) (int, error) { + return r.ReadCloser.Read(p) +} + +func (r readerFile) Close() error { + return r.ReadCloser.Close() +} + +// ensure that readerFile implements File +var _ File = readerFile{} + +// fakeFile implements all File methods, but only returns errors for anything +// except Stat() and Name(). +type fakeFile struct { + name string + os.FileInfo +} + +// ensure that fakeFile implements File +var _ File = fakeFile{} + +func (f fakeFile) Fd() uintptr { + return 0 +} + +func (f fakeFile) Readdirnames(n int) ([]string, error) { + return nil, os.ErrInvalid +} + +func (f fakeFile) Readdir(n int) ([]os.FileInfo, error) { + return nil, os.ErrInvalid +} + +func (f fakeFile) Seek(int64, int) (int64, error) { + return 0, os.ErrInvalid +} + +func (f fakeFile) Write(p []byte) (int, error) { + return 0, os.ErrInvalid +} + +func (f fakeFile) Read(p []byte) (int, error) { + return 0, os.ErrInvalid +} + +func (f fakeFile) Close() error { + return nil +} + +func (f fakeFile) Stat() (os.FileInfo, error) { + return f.FileInfo, nil +} + +func (f fakeFile) Name() string { + return f.name +} + +// fakeDir implements Readdirnames and Readdir, everything else is delegated to fakeFile. +type fakeDir struct { + entries []os.FileInfo + fakeFile +} + +func (d fakeDir) Readdirnames(n int) ([]string, error) { + if n >= 0 { + return nil, errors.New("not implemented") + } + names := make([]string, 0, len(d.entries)) + for _, entry := range d.entries { + names = append(names, entry.Name()) + } + + return names, nil +} + +func (d fakeDir) Readdir(n int) ([]os.FileInfo, error) { + if n >= 0 { + return nil, errors.New("not implemented") + } + return d.entries, nil +} + +// fakeFileInfo implements the bare minimum of os.FileInfo. +type fakeFileInfo struct { + name string + size int64 + mode os.FileMode + modtime time.Time + sys interface{} +} + +func (fi fakeFileInfo) Name() string { + return fi.name +} + +func (fi fakeFileInfo) Size() int64 { + return fi.size +} + +func (fi fakeFileInfo) Mode() os.FileMode { + return fi.mode +} + +func (fi fakeFileInfo) ModTime() time.Time { + return fi.modtime +} + +func (fi fakeFileInfo) IsDir() bool { + return fi.mode&os.ModeDir > 0 +} + +func (fi fakeFileInfo) Sys() interface{} { + return fi.sys +} diff --git a/internal/fs/fs_reader_test.go b/internal/fs/fs_reader_test.go new file mode 100644 index 000000000..f4cb2bb34 --- /dev/null +++ b/internal/fs/fs_reader_test.go @@ -0,0 +1,319 @@ +package fs + +import ( + "bytes" + "io/ioutil" + "os" + "sort" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/restic/restic/internal/test" +) + +func verifyFileContentOpen(t testing.TB, fs FS, filename string, want []byte) { + f, err := fs.Open(filename) + if err != nil { + t.Fatal(err) + } + + buf, err := ioutil.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + err = f.Close() + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(want, buf) { + t.Error(cmp.Diff(want, buf)) + } +} + +func verifyFileContentOpenFile(t testing.TB, fs FS, filename string, want []byte) { + f, err := fs.OpenFile(filename, O_RDONLY, 0) + if err != nil { + t.Fatal(err) + } + + buf, err := ioutil.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + err = f.Close() + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(want, buf) { + t.Error(cmp.Diff(want, buf)) + } +} + +func verifyDirectoryContents(t testing.TB, fs FS, dir string, want []string) { + f, err := fs.Open(dir) + if err != nil { + t.Fatal(err) + } + + entries, err := f.Readdirnames(-1) + if err != nil { + t.Fatal(err) + } + + err = f.Close() + if err != nil { + t.Fatal(err) + } + + sort.Sort(sort.StringSlice(want)) + sort.Sort(sort.StringSlice(entries)) + + if !cmp.Equal(want, entries) { + t.Error(cmp.Diff(want, entries)) + } +} + +type fiSlice []os.FileInfo + +func (s fiSlice) Len() int { + return len(s) +} + +func (s fiSlice) Less(i, j int) bool { + return s[i].Name() < s[j].Name() +} + +func (s fiSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func verifyDirectoryContentsFI(t testing.TB, fs FS, dir string, want []os.FileInfo) { + f, err := fs.Open(dir) + if err != nil { + t.Fatal(err) + } + + entries, err := f.Readdir(-1) + if err != nil { + t.Fatal(err) + } + + err = f.Close() + if err != nil { + t.Fatal(err) + } + + sort.Sort(fiSlice(want)) + sort.Sort(fiSlice(entries)) + + if len(want) != len(entries) { + t.Errorf("wrong number of entries returned, want %d, got %d", len(want), len(entries)) + } + max := len(want) + if len(entries) < max { + max = len(entries) + } + + for i := 0; i < max; i++ { + fi1 := want[i] + fi2 := entries[i] + + if fi1.Name() != fi2.Name() { + t.Errorf("entry %d: wrong value for Name: want %q, got %q", i, fi1.Name(), fi2.Name()) + } + + if fi1.IsDir() != fi2.IsDir() { + t.Errorf("entry %d: wrong value for IsDir: want %v, got %v", i, fi1.IsDir(), fi2.IsDir()) + } + + if fi1.Mode() != fi2.Mode() { + t.Errorf("entry %d: wrong value for Mode: want %v, got %v", i, fi1.Mode(), fi2.Mode()) + } + + if fi1.ModTime() != fi2.ModTime() { + t.Errorf("entry %d: wrong value for ModTime: want %v, got %v", i, fi1.ModTime(), fi2.ModTime()) + } + + if fi1.Size() != fi2.Size() { + t.Errorf("entry %d: wrong value for Size: want %v, got %v", i, fi1.Size(), fi2.Size()) + } + + if fi1.Sys() != fi2.Sys() { + t.Errorf("entry %d: wrong value for Sys: want %v, got %v", i, fi1.Sys(), fi2.Sys()) + } + } +} + +func checkFileInfo(t testing.TB, fi os.FileInfo, filename string, modtime time.Time, mode os.FileMode, isdir bool) { + if fi.IsDir() { + t.Errorf("IsDir returned true, want false") + } + + if fi.Mode() != mode { + t.Errorf("Mode() returned wrong value, want 0%o, got 0%o", mode, fi.Mode()) + } + + if !modtime.Equal(time.Time{}) && !fi.ModTime().Equal(modtime) { + t.Errorf("ModTime() returned wrong value, want %v, got %v", modtime, fi.ModTime()) + } + + if fi.Name() != filename { + t.Errorf("Name() returned wrong value, want %q, got %q", filename, fi.Name()) + } +} + +func TestFSReader(t *testing.T) { + data := test.Random(55, 1<<18+588) + now := time.Now() + filename := "foobar" + + var tests = []struct { + name string + f func(t *testing.T, fs FS) + }{ + { + name: "Readdirnames-slash", + f: func(t *testing.T, fs FS) { + verifyDirectoryContents(t, fs, "/", []string{filename}) + }, + }, + { + name: "Readdirnames-current", + f: func(t *testing.T, fs FS) { + verifyDirectoryContents(t, fs, ".", []string{filename}) + }, + }, + { + name: "Readdir-slash", + f: func(t *testing.T, fs FS) { + fi := fakeFileInfo{ + mode: 0644, + modtime: now, + name: filename, + size: int64(len(data)), + } + verifyDirectoryContentsFI(t, fs, "/", []os.FileInfo{fi}) + }, + }, + { + name: "Readdir-current", + f: func(t *testing.T, fs FS) { + fi := fakeFileInfo{ + mode: 0644, + modtime: now, + name: filename, + size: int64(len(data)), + } + verifyDirectoryContentsFI(t, fs, ".", []os.FileInfo{fi}) + }, + }, + { + name: "file/Open", + f: func(t *testing.T, fs FS) { + verifyFileContentOpen(t, fs, filename, data) + }, + }, + { + name: "file/OpenFile", + f: func(t *testing.T, fs FS) { + verifyFileContentOpenFile(t, fs, filename, data) + }, + }, + { + name: "file/Lstat", + f: func(t *testing.T, fs FS) { + fi, err := fs.Lstat(filename) + if err != nil { + t.Fatal(err) + } + + checkFileInfo(t, fi, filename, now, 0644, false) + }, + }, + { + name: "file/Stat", + f: func(t *testing.T, fs FS) { + f, err := fs.Open(filename) + if err != nil { + t.Fatal(err) + } + + fi, err := f.Stat() + if err != nil { + t.Fatal(err) + } + + err = f.Close() + if err != nil { + t.Fatal(err) + } + + checkFileInfo(t, fi, filename, now, 0644, false) + }, + }, + { + name: "dir/Lstat-slash", + f: func(t *testing.T, fs FS) { + fi, err := fs.Lstat("/") + if err != nil { + t.Fatal(err) + } + + checkFileInfo(t, fi, "/", time.Time{}, 0755, false) + }, + }, + { + name: "dir/Lstat-current", + f: func(t *testing.T, fs FS) { + fi, err := fs.Lstat(".") + if err != nil { + t.Fatal(err) + } + + checkFileInfo(t, fi, ".", time.Time{}, 0755, false) + }, + }, + { + name: "dir/Open-slash", + f: func(t *testing.T, fs FS) { + fi, err := fs.Lstat("/") + if err != nil { + t.Fatal(err) + } + + checkFileInfo(t, fi, "/", time.Time{}, 0755, false) + }, + }, + { + name: "dir/Open-current", + f: func(t *testing.T, fs FS) { + fi, err := fs.Lstat(".") + if err != nil { + t.Fatal(err) + } + + checkFileInfo(t, fi, ".", time.Time{}, 0755, false) + }, + }, + } + + for _, test := range tests { + fs := &Reader{ + Name: filename, + ReadCloser: ioutil.NopCloser(bytes.NewReader(data)), + + Mode: 0644, + Size: int64(len(data)), + ModTime: now, + } + + t.Run(test.name, func(t *testing.T) { + test.f(t, fs) + }) + } +} diff --git a/internal/fs/fs_track.go b/internal/fs/fs_track.go new file mode 100644 index 000000000..319fbfaff --- /dev/null +++ b/internal/fs/fs_track.go @@ -0,0 +1,54 @@ +package fs + +import ( + "fmt" + "os" + "runtime" + "runtime/debug" +) + +// Track is a wrapper around another file system which installs finalizers +// for open files which call panic() when they are not closed when the garbage +// collector releases them. This can be used to find resource leaks via open +// files. +type Track struct { + FS +} + +// Open wraps the Open method of the underlying file system. +func (fs Track) Open(name string) (File, error) { + f, err := fs.FS.Open(fixpath(name)) + if err != nil { + return nil, err + } + + return newTrackFile(debug.Stack(), name, f), nil +} + +// OpenFile wraps the OpenFile method of the underlying file system. +func (fs Track) OpenFile(name string, flag int, perm os.FileMode) (File, error) { + f, err := fs.FS.OpenFile(fixpath(name), flag, perm) + if err != nil { + return nil, err + } + + return newTrackFile(debug.Stack(), name, f), nil +} + +type trackFile struct { + File +} + +func newTrackFile(stack []byte, filename string, file File) *trackFile { + f := &trackFile{file} + runtime.SetFinalizer(f, func(f *trackFile) { + fmt.Fprintf(os.Stderr, "file %s not closed\n\nStacktrack:\n%s\n", filename, stack) + panic("file " + filename + " not closed") + }) + return f +} + +func (f *trackFile) Close() error { + runtime.SetFinalizer(f, nil) + return f.File.Close() +} diff --git a/internal/fs/interface.go b/internal/fs/interface.go new file mode 100644 index 000000000..1c2260215 --- /dev/null +++ b/internal/fs/interface.go @@ -0,0 +1,38 @@ +package fs + +import ( + "io" + "os" +) + +// FS bundles all methods needed for a file system. +type FS interface { + Open(name string) (File, error) + OpenFile(name string, flag int, perm os.FileMode) (File, error) + Stat(name string) (os.FileInfo, error) + Lstat(name string) (os.FileInfo, error) + + Join(elem ...string) string + Separator() string + Abs(path string) (string, error) + Clean(path string) string + VolumeName(path string) string + IsAbs(path string) bool + + Dir(path string) string + Base(path string) string +} + +// File is an open file on a file system. +type File interface { + io.Reader + io.Writer + io.Closer + + Fd() uintptr + Readdirnames(n int) ([]string, error) + Readdir(int) ([]os.FileInfo, error) + Seek(int64, int) (int64, error) + Stat() (os.FileInfo, error) + Name() string +} diff --git a/internal/fs/stat.go b/internal/fs/stat.go new file mode 100644 index 000000000..d37d12942 --- /dev/null +++ b/internal/fs/stat.go @@ -0,0 +1,34 @@ +package fs + +import ( + "os" + "time" +) + +// ExtendedFileInfo is an extended stat_t, filled with attributes that are +// supported by most operating systems. The original FileInfo is embedded. +type ExtendedFileInfo struct { + os.FileInfo + + DeviceID uint64 // ID of device containing the file + Inode uint64 // Inode number + Links uint64 // Number of hard links + UID uint32 // owner user ID + GID uint32 // owner group ID + Device uint64 // Device ID (if this is a device file) + BlockSize int64 // block size for filesystem IO + Blocks int64 // number of allocated filesystem blocks + Size int64 // file size in byte + + AccessTime time.Time // last access time stamp + ModTime time.Time // last (content) modification time stamp +} + +// ExtendedStat returns an ExtendedFileInfo constructed from the os.FileInfo. +func ExtendedStat(fi os.FileInfo) ExtendedFileInfo { + if fi == nil { + panic("os.FileInfo is nil") + } + + return extendedStat(fi) +} diff --git a/internal/fs/stat_bsd.go b/internal/fs/stat_bsd.go new file mode 100644 index 000000000..97c03bedc --- /dev/null +++ b/internal/fs/stat_bsd.go @@ -0,0 +1,36 @@ +// +build freebsd darwin + +package fs + +import ( + "fmt" + "os" + "syscall" + "time" +) + +// extendedStat extracts info into an ExtendedFileInfo for unix based operating systems. +func extendedStat(fi os.FileInfo) ExtendedFileInfo { + s, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + panic(fmt.Sprintf("conversion to syscall.Stat_t failed, type is %T", fi.Sys())) + } + + extFI := ExtendedFileInfo{ + FileInfo: fi, + DeviceID: uint64(s.Dev), + Inode: uint64(s.Ino), + Links: uint64(s.Nlink), + UID: s.Uid, + GID: s.Gid, + Device: uint64(s.Rdev), + BlockSize: int64(s.Blksize), + Blocks: s.Blocks, + Size: s.Size, + + AccessTime: time.Unix(s.Atimespec.Unix()), + ModTime: time.Unix(s.Mtimespec.Unix()), + } + + return extFI +} diff --git a/internal/fs/stat_test.go b/internal/fs/stat_test.go new file mode 100644 index 000000000..43e514047 --- /dev/null +++ b/internal/fs/stat_test.go @@ -0,0 +1,31 @@ +package fs + +import ( + "io/ioutil" + "path/filepath" + "testing" + + restictest "github.com/restic/restic/internal/test" +) + +func TestExtendedStat(t *testing.T) { + tempdir, cleanup := restictest.TempDir(t) + defer cleanup() + + filename := filepath.Join(tempdir, "file") + err := ioutil.WriteFile(filename, []byte("foobar"), 0640) + if err != nil { + t.Fatal(err) + } + + fi, err := Lstat(filename) + if err != nil { + t.Fatal(err) + } + + extFI := ExtendedStat(fi) + + if !extFI.ModTime.Equal(fi.ModTime()) { + t.Errorf("extFI.ModTime does not match, want %v, got %v", fi.ModTime(), extFI.ModTime) + } +} diff --git a/internal/fs/stat_unix.go b/internal/fs/stat_unix.go new file mode 100644 index 000000000..612898566 --- /dev/null +++ b/internal/fs/stat_unix.go @@ -0,0 +1,36 @@ +// +build !windows,!darwin,!freebsd + +package fs + +import ( + "fmt" + "os" + "syscall" + "time" +) + +// extendedStat extracts info into an ExtendedFileInfo for unix based operating systems. +func extendedStat(fi os.FileInfo) ExtendedFileInfo { + s, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + panic(fmt.Sprintf("conversion to syscall.Stat_t failed, type is %T", fi.Sys())) + } + + extFI := ExtendedFileInfo{ + FileInfo: fi, + DeviceID: uint64(s.Dev), + Inode: s.Ino, + Links: uint64(s.Nlink), + UID: s.Uid, + GID: s.Gid, + Device: uint64(s.Rdev), + BlockSize: int64(s.Blksize), + Blocks: s.Blocks, + Size: s.Size, + + AccessTime: time.Unix(s.Atim.Unix()), + ModTime: time.Unix(s.Mtim.Unix()), + } + + return extFI +} diff --git a/internal/fs/stat_windows.go b/internal/fs/stat_windows.go new file mode 100644 index 000000000..16f9fe0eb --- /dev/null +++ b/internal/fs/stat_windows.go @@ -0,0 +1,31 @@ +// +build windows + +package fs + +import ( + "fmt" + "os" + "syscall" + "time" +) + +// extendedStat extracts info into an ExtendedFileInfo for Windows. +func extendedStat(fi os.FileInfo) ExtendedFileInfo { + s, ok := fi.Sys().(*syscall.Win32FileAttributeData) + if !ok { + panic(fmt.Sprintf("conversion to syscall.Win32FileAttributeData failed, type is %T", fi.Sys())) + } + + extFI := ExtendedFileInfo{ + FileInfo: fi, + Size: int64(s.FileSizeLow) + int64(s.FileSizeHigh)<<32, + } + + atime := syscall.NsecToTimespec(s.LastAccessTime.Nanoseconds()) + extFI.AccessTime = time.Unix(atime.Unix()) + + mtime := syscall.NsecToTimespec(s.LastWriteTime.Nanoseconds()) + extFI.ModTime = time.Unix(mtime.Unix()) + + return extFI +}