mirror of
https://github.com/octoleo/restic.git
synced 2024-12-22 10:58:55 +00:00
fs: Add interface and FS implementations
This adds two implementations of the new `FS` interface: One for the local file system (`Local`) and one for a single file read from an `io.Reader` (`Reader`).
This commit is contained in:
parent
83ca08245b
commit
c4b2486b7c
16
internal/fs/const.go
Normal file
16
internal/fs/const.go
Normal file
@ -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.
|
||||
)
|
8
internal/fs/const_unix.go
Normal file
8
internal/fs/const_unix.go
Normal file
@ -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
|
6
internal/fs/const_windows.go
Normal file
6
internal/fs/const_windows.go
Normal file
@ -0,0 +1,6 @@
|
||||
// +build windows
|
||||
|
||||
package fs
|
||||
|
||||
// O_NOFOLLOW is a noop on Windows.
|
||||
const O_NOFOLLOW int = 0
|
@ -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 {
|
||||
|
96
internal/fs/fs_local.go
Normal file
96
internal/fs/fs_local.go
Normal file
@ -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)
|
||||
}
|
289
internal/fs/fs_reader.go
Normal file
289
internal/fs/fs_reader.go
Normal file
@ -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
|
||||
}
|
319
internal/fs/fs_reader_test.go
Normal file
319
internal/fs/fs_reader_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
54
internal/fs/fs_track.go
Normal file
54
internal/fs/fs_track.go
Normal file
@ -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()
|
||||
}
|
38
internal/fs/interface.go
Normal file
38
internal/fs/interface.go
Normal file
@ -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
|
||||
}
|
34
internal/fs/stat.go
Normal file
34
internal/fs/stat.go
Normal file
@ -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)
|
||||
}
|
36
internal/fs/stat_bsd.go
Normal file
36
internal/fs/stat_bsd.go
Normal file
@ -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
|
||||
}
|
31
internal/fs/stat_test.go
Normal file
31
internal/fs/stat_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
36
internal/fs/stat_unix.go
Normal file
36
internal/fs/stat_unix.go
Normal file
@ -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
|
||||
}
|
31
internal/fs/stat_windows.go
Normal file
31
internal/fs/stat_windows.go
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user