rclone/vfs/file_test.go
Nick Craig-Wood 6e683b4359 vfs: fix rename of open files when using the VFS cache
Before this change, renaming an open file when using the VFS cache was
delayed until the file was closed.  This meant that the file was not
readable after a rename even though it is was in the cache.

After this change we rename the local cache file and the in memory
cache, delaying only the rename of the file in object storage.

See: https://forum.rclone.org/t/xen-orchestra-ebadf-bad-file-descriptor-write/13104
2019-12-12 13:31:10 +00:00

330 lines
7.4 KiB
Go

package vfs
import (
"context"
"io/ioutil"
"os"
"testing"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fstest"
"github.com/rclone/rclone/fstest/mockfs"
"github.com/rclone/rclone/fstest/mockobject"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func fileCreate(t *testing.T, r *fstest.Run, mode CacheMode) (*VFS, *File, fstest.Item) {
opt := DefaultOpt
opt.CacheMode = mode
vfs := New(r.Fremote, &opt)
file1 := r.WriteObject(context.Background(), "dir/file1", "file1 contents", t1)
fstest.CheckItems(t, r.Fremote, file1)
node, err := vfs.Stat("dir/file1")
require.NoError(t, err)
require.True(t, node.Mode().IsRegular())
return vfs, node.(*File), file1
}
func TestFileMethods(t *testing.T) {
r := fstest.NewRun(t)
defer r.Finalise()
vfs, file, _ := fileCreate(t, r, CacheModeOff)
// String
assert.Equal(t, "dir/file1", file.String())
assert.Equal(t, "<nil *File>", (*File)(nil).String())
// IsDir
assert.Equal(t, false, file.IsDir())
// IsFile
assert.Equal(t, true, file.IsFile())
// Mode
assert.Equal(t, vfs.Opt.FilePerms, file.Mode())
// Name
assert.Equal(t, "file1", file.Name())
// Path
assert.Equal(t, "dir/file1", file.Path())
// Sys
assert.Equal(t, nil, file.Sys())
// Inode
assert.NotEqual(t, uint64(0), file.Inode())
// Node
assert.Equal(t, file, file.Node())
// ModTime
assert.WithinDuration(t, t1, file.ModTime(), r.Fremote.Precision())
// Size
assert.Equal(t, int64(14), file.Size())
// Sync
assert.NoError(t, file.Sync())
// DirEntry
assert.Equal(t, file.o, file.DirEntry())
// Dir
assert.Equal(t, file.d, file.Dir())
// VFS
assert.Equal(t, vfs, file.VFS())
}
func TestFileSetModTime(t *testing.T) {
r := fstest.NewRun(t)
if !canSetModTime(t, r) {
return
}
defer r.Finalise()
vfs, file, file1 := fileCreate(t, r, CacheModeOff)
err := file.SetModTime(t2)
require.NoError(t, err)
file1.ModTime = t2
fstest.CheckItems(t, r.Fremote, file1)
vfs.Opt.ReadOnly = true
err = file.SetModTime(t2)
assert.Equal(t, EROFS, err)
}
func fileCheckContents(t *testing.T, file *File) {
fd, err := file.Open(os.O_RDONLY)
require.NoError(t, err)
contents, err := ioutil.ReadAll(fd)
require.NoError(t, err)
assert.Equal(t, "file1 contents", string(contents))
require.NoError(t, fd.Close())
}
func TestFileOpenRead(t *testing.T) {
r := fstest.NewRun(t)
defer r.Finalise()
_, file, _ := fileCreate(t, r, CacheModeOff)
fileCheckContents(t, file)
}
func TestFileOpenReadUnknownSize(t *testing.T) {
var (
contents = []byte("file contents")
remote = "file.txt"
ctx = context.Background()
)
// create a mock object which returns size -1
o := mockobject.New(remote).WithContent(contents, mockobject.SeekModeNone)
o.SetUnknownSize(true)
assert.Equal(t, int64(-1), o.Size())
// add it to a mock fs
f := mockfs.NewFs("test", "root")
f.AddObject(o)
testObj, err := f.NewObject(ctx, remote)
require.NoError(t, err)
assert.Equal(t, int64(-1), testObj.Size())
// create a VFS from that mockfs
vfs := New(f, nil)
// find the file
node, err := vfs.Stat(remote)
require.NoError(t, err)
require.True(t, node.IsFile())
file := node.(*File)
// open it
fd, err := file.openRead()
require.NoError(t, err)
assert.Equal(t, int64(0), fd.Size())
// check the contents are not empty even though size is empty
gotContents, err := ioutil.ReadAll(fd)
require.NoError(t, err)
assert.Equal(t, contents, gotContents)
t.Logf("gotContents = %q", gotContents)
// check that file size has been updated
assert.Equal(t, int64(len(contents)), fd.Size())
require.NoError(t, fd.Close())
}
func TestFileOpenWrite(t *testing.T) {
r := fstest.NewRun(t)
defer r.Finalise()
vfs, file, _ := fileCreate(t, r, CacheModeOff)
fd, err := file.openWrite(os.O_WRONLY | os.O_TRUNC)
require.NoError(t, err)
newContents := []byte("this is some new contents")
n, err := fd.Write(newContents)
require.NoError(t, err)
assert.Equal(t, len(newContents), n)
require.NoError(t, fd.Close())
assert.Equal(t, int64(25), file.Size())
vfs.Opt.ReadOnly = true
_, err = file.openWrite(os.O_WRONLY | os.O_TRUNC)
assert.Equal(t, EROFS, err)
}
func TestFileRemove(t *testing.T) {
r := fstest.NewRun(t)
defer r.Finalise()
vfs, file, _ := fileCreate(t, r, CacheModeOff)
err := file.Remove()
require.NoError(t, err)
fstest.CheckItems(t, r.Fremote)
vfs.Opt.ReadOnly = true
err = file.Remove()
assert.Equal(t, EROFS, err)
}
func TestFileRemoveAll(t *testing.T) {
r := fstest.NewRun(t)
defer r.Finalise()
vfs, file, _ := fileCreate(t, r, CacheModeOff)
err := file.RemoveAll()
require.NoError(t, err)
fstest.CheckItems(t, r.Fremote)
vfs.Opt.ReadOnly = true
err = file.RemoveAll()
assert.Equal(t, EROFS, err)
}
func TestFileOpen(t *testing.T) {
r := fstest.NewRun(t)
defer r.Finalise()
_, file, _ := fileCreate(t, r, CacheModeOff)
fd, err := file.Open(os.O_RDONLY)
require.NoError(t, err)
_, ok := fd.(*ReadFileHandle)
assert.True(t, ok)
require.NoError(t, fd.Close())
fd, err = file.Open(os.O_WRONLY)
assert.NoError(t, err)
_, ok = fd.(*WriteFileHandle)
assert.True(t, ok)
require.NoError(t, fd.Close())
fd, err = file.Open(os.O_RDWR)
assert.NoError(t, err)
_, ok = fd.(*WriteFileHandle)
assert.True(t, ok)
fd, err = file.Open(3)
assert.Equal(t, EPERM, err)
}
func testFileRename(t *testing.T, mode CacheMode) {
r := fstest.NewRun(t)
defer r.Finalise()
vfs, file, item := fileCreate(t, r, mode)
rootDir, err := vfs.Root()
require.NoError(t, err)
// check file in cache
if mode != CacheModeOff {
// read contents to get file in cache
fileCheckContents(t, file)
assert.True(t, vfs.cache.exists(item.Path))
}
dir := file.Dir()
// start with "dir/file1"
fstest.CheckItems(t, r.Fremote, item)
// rename file to "newLeaf"
err = dir.Rename("file1", "newLeaf", rootDir)
require.NoError(t, err)
item.Path = "newLeaf"
fstest.CheckItems(t, r.Fremote, item)
// check file in cache
if mode != CacheModeOff {
assert.True(t, vfs.cache.exists(item.Path))
}
// check file exists in the vfs layer at its new name
_, err = vfs.Stat("newLeaf")
require.NoError(t, err)
// rename it back to "dir/file1"
err = rootDir.Rename("newLeaf", "file1", dir)
require.NoError(t, err)
item.Path = "dir/file1"
fstest.CheckItems(t, r.Fremote, item)
// check file in cache
if mode != CacheModeOff {
assert.True(t, vfs.cache.exists(item.Path))
}
// now try renaming it with the file open
// first open it and write to it but dont close it
fd, err := file.Open(os.O_WRONLY | os.O_TRUNC)
require.NoError(t, err)
newContents := []byte("this is some new contents")
_, err = fd.Write(newContents)
require.NoError(t, err)
// rename file to "newLeaf"
err = dir.Rename("file1", "newLeaf", rootDir)
require.NoError(t, err)
newItem := fstest.NewItem("newLeaf", string(newContents), item.ModTime)
// check file has been renamed immediately in the cache
if mode != CacheModeOff {
assert.True(t, vfs.cache.exists("newLeaf"))
}
// check file exists in the vfs layer at its new name
_, err = vfs.Stat("newLeaf")
require.NoError(t, err)
// Close the file
require.NoError(t, fd.Close())
// Check file has now been renamed on the remote
item.Path = "newLeaf"
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{newItem}, nil, fs.ModTimeNotSupported)
}
func TestFileRename(t *testing.T) {
t.Run("CacheModeOff", func(t *testing.T) {
testFileRename(t, CacheModeOff)
})
t.Run("CacheModeFull", func(t *testing.T) {
testFileRename(t, CacheModeFull)
})
}