mirror of
https://github.com/restic/restic.git
synced 2025-03-30 00:00:14 +01:00

Regular files that are empty according to stat are now not opened for reading their contents. Such files are quite common (in my homedir, at least) and we can save multiple system calls this way. On a network filesystem, that can mean round trips. Also, we can back up empty files that we cannot open for reading. Finally, fixes #4257. Existing tests cover this case. fs.Reader now no longer has a meaningful Size. Nothing depended on that.
438 lines
8.2 KiB
Go
438 lines
8.2 KiB
Go
package fs
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
"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 := io.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 := io.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.Strings(want)
|
|
sort.Strings(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.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() != isdir {
|
|
t.Errorf("IsDir returned %t, want %t", fi.IsDir(), isdir)
|
|
}
|
|
|
|
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 path.Base(fi.Name()) != fi.Name() {
|
|
t.Errorf("Name() returned is not base, want %q, got %q", path.Base(fi.Name()), fi.Name())
|
|
}
|
|
|
|
if fi.Name() != path.Base(filename) {
|
|
t.Errorf("Name() returned wrong value, want %q, got %q", path.Base(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,
|
|
}
|
|
verifyDirectoryContentsFI(t, fs, "/", []os.FileInfo{fi})
|
|
},
|
|
},
|
|
{
|
|
name: "Readdir-current",
|
|
f: func(t *testing.T, fs FS) {
|
|
fi := fakeFileInfo{
|
|
mode: 0644,
|
|
modtime: now,
|
|
name: filename,
|
|
}
|
|
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{}, os.ModeDir|0755, true)
|
|
},
|
|
},
|
|
{
|
|
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{}, os.ModeDir|0755, true)
|
|
},
|
|
},
|
|
{
|
|
name: "dir/Lstat-error-not-exist",
|
|
f: func(t *testing.T, fs FS) {
|
|
_, err := fs.Lstat("other")
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
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{}, os.ModeDir|0755, true)
|
|
},
|
|
},
|
|
{
|
|
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{}, os.ModeDir|0755, true)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
fs := &Reader{
|
|
Name: filename,
|
|
ReadCloser: io.NopCloser(bytes.NewReader(data)),
|
|
|
|
Mode: 0644,
|
|
ModTime: now,
|
|
}
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
test.f(t, fs)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFSReaderDir(t *testing.T) {
|
|
data := test.Random(55, 1<<18+588)
|
|
now := time.Now()
|
|
|
|
var tests = []struct {
|
|
name string
|
|
filename string
|
|
}{
|
|
{
|
|
name: "Lstat-absolute",
|
|
filename: "/path/to/foobar",
|
|
},
|
|
{
|
|
name: "Lstat-relative",
|
|
filename: "path/to/foobar",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
fs := &Reader{
|
|
Name: test.filename,
|
|
ReadCloser: io.NopCloser(bytes.NewReader(data)),
|
|
|
|
Mode: 0644,
|
|
ModTime: now,
|
|
}
|
|
|
|
dir := path.Dir(fs.Name)
|
|
for {
|
|
if dir == "/" || dir == "." {
|
|
break
|
|
}
|
|
|
|
fi, err := fs.Lstat(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
checkFileInfo(t, fi, dir, time.Time{}, os.ModeDir|0755, true)
|
|
|
|
dir = path.Dir(dir)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFSReaderMinFileSize(t *testing.T) {
|
|
var tests = []struct {
|
|
name string
|
|
data string
|
|
allowEmpty bool
|
|
readMustErr bool
|
|
}{
|
|
{
|
|
name: "regular",
|
|
data: "foobar",
|
|
},
|
|
{
|
|
name: "empty",
|
|
data: "",
|
|
allowEmpty: false,
|
|
readMustErr: true,
|
|
},
|
|
{
|
|
name: "empty2",
|
|
data: "",
|
|
allowEmpty: true,
|
|
readMustErr: false,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
fs := &Reader{
|
|
Name: "testfile",
|
|
ReadCloser: io.NopCloser(strings.NewReader(test.data)),
|
|
Mode: 0644,
|
|
ModTime: time.Now(),
|
|
AllowEmptyFile: test.allowEmpty,
|
|
}
|
|
|
|
f, err := fs.Open("testfile")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
buf, err := io.ReadAll(f)
|
|
if test.readMustErr {
|
|
if err == nil {
|
|
t.Fatal("expected error not found, got nil")
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if string(buf) != test.data {
|
|
t.Fatalf("wrong data returned, want %q, got %q", test.data, string(buf))
|
|
}
|
|
|
|
err = f.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
}
|