mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-22 19:08:58 +00:00
parent
ff84f075d5
commit
e9bb17307d
@ -14,6 +14,9 @@ type copyRangeImplementationBasicFile func(src, dst basicFile, srcOffset, dstOff
|
|||||||
|
|
||||||
func copyRangeImplementationForBasicFile(impl copyRangeImplementationBasicFile) copyRangeImplementation {
|
func copyRangeImplementationForBasicFile(impl copyRangeImplementationBasicFile) copyRangeImplementation {
|
||||||
return func(src, dst File, srcOffset, dstOffset, size int64) error {
|
return func(src, dst File, srcOffset, dstOffset, size int64) error {
|
||||||
|
src = unwrap(src)
|
||||||
|
dst = unwrap(dst)
|
||||||
|
// Then see if it's basic files
|
||||||
srcFile, srcOk := src.(basicFile)
|
srcFile, srcOk := src.(basicFile)
|
||||||
dstFile, dstOk := dst.(basicFile)
|
dstFile, dstOk := dst.(basicFile)
|
||||||
if !srcOk || !dstOk {
|
if !srcOk || !dstOk {
|
||||||
@ -22,3 +25,38 @@ func copyRangeImplementationForBasicFile(impl copyRangeImplementationBasicFile)
|
|||||||
return impl(srcFile, dstFile, srcOffset, dstOffset, size)
|
return impl(srcFile, dstFile, srcOffset, dstOffset, size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withFileDescriptors(first, second basicFile, fn func(first, second uintptr) (int, error)) (int, error) {
|
||||||
|
fc, err := first.SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
sc, err := second.SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var n int
|
||||||
|
var ferr, serr, fnerr error
|
||||||
|
ferr = fc.Control(func(first uintptr) {
|
||||||
|
serr = sc.Control(func(second uintptr) {
|
||||||
|
n, fnerr = fn(first, second)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if ferr != nil {
|
||||||
|
return n, ferr
|
||||||
|
}
|
||||||
|
if serr != nil {
|
||||||
|
return n, serr
|
||||||
|
}
|
||||||
|
return n, fnerr
|
||||||
|
}
|
||||||
|
|
||||||
|
func unwrap(f File) File {
|
||||||
|
for {
|
||||||
|
if wrapped, ok := f.(interface{ unwrap() File }); ok {
|
||||||
|
f = wrapped.unwrap()
|
||||||
|
} else {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -29,7 +29,9 @@ func copyRangeCopyFileRange(src, dst basicFile, srcOffset, dstOffset, size int64
|
|||||||
// appropriately.
|
// appropriately.
|
||||||
//
|
//
|
||||||
// Also, even if explicitly not stated, the same is true for dstOffset
|
// Also, even if explicitly not stated, the same is true for dstOffset
|
||||||
n, err := unix.CopyFileRange(int(src.Fd()), &srcOffset, int(dst.Fd()), &dstOffset, int(size), 0)
|
n, err := withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) {
|
||||||
|
return unix.CopyFileRange(int(srcFd), &srcOffset, int(dstFd), &dstOffset, int(size), 0)
|
||||||
|
})
|
||||||
if n == 0 && err == nil {
|
if n == 0 && err == nil {
|
||||||
return io.ErrUnexpectedEOF
|
return io.ErrUnexpectedEOF
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
package fs
|
package fs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
@ -46,13 +47,24 @@ type duplicateExtentsData struct {
|
|||||||
func copyRangeDuplicateExtents(src, dst basicFile, srcOffset, dstOffset, size int64) error {
|
func copyRangeDuplicateExtents(src, dst basicFile, srcOffset, dstOffset, size int64) error {
|
||||||
var err error
|
var err error
|
||||||
// Check that the destination file has sufficient space
|
// Check that the destination file has sufficient space
|
||||||
if fi, err := dst.Stat(); err != nil {
|
dstFi, err := dst.Stat()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if fi.Size() < dstOffset+size {
|
}
|
||||||
|
dstSize := dstFi.Size()
|
||||||
|
if dstSize < dstOffset+size {
|
||||||
// set file size. There is a requirements "The destination region must not extend past the end of file."
|
// set file size. There is a requirements "The destination region must not extend past the end of file."
|
||||||
if err = dst.Truncate(dstOffset + size); err != nil {
|
if err = dst.Truncate(dstOffset + size); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
dstSize = dstOffset + size
|
||||||
|
}
|
||||||
|
|
||||||
|
// The source file has to be big enough
|
||||||
|
if fi, err := src.Stat(); err != nil {
|
||||||
|
return err
|
||||||
|
} else if fi.Size() < srcOffset+size {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
}
|
}
|
||||||
|
|
||||||
// Requirement
|
// Requirement
|
||||||
@ -60,9 +72,25 @@ func copyRangeDuplicateExtents(src, dst basicFile, srcOffset, dstOffset, size in
|
|||||||
// * cloneRegionSize less than 4GiB.
|
// * cloneRegionSize less than 4GiB.
|
||||||
// see https://docs.microsoft.com/windows/win32/fileio/block-cloning
|
// see https://docs.microsoft.com/windows/win32/fileio/block-cloning
|
||||||
|
|
||||||
|
smallestClusterSize := availableClusterSize[len(availableClusterSize)-1]
|
||||||
|
|
||||||
|
if srcOffset%smallestClusterSize != 0 || dstOffset%smallestClusterSize != 0 {
|
||||||
|
return syscall.EINVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each file gets allocated multiple of "clusterSize" blocks, yet file size determines how much of the last block
|
||||||
|
// is readable/visible.
|
||||||
|
// Copies only happen in block sized chunks, hence you can copy non block sized regions of data to a file, as long
|
||||||
|
// as the regions are copied at the end of the file where the block visibility is adjusted by the file size.
|
||||||
|
if size%smallestClusterSize != 0 && dstOffset+size != dstSize {
|
||||||
|
return syscall.EINVAL
|
||||||
|
}
|
||||||
|
|
||||||
// Clone first xGiB region.
|
// Clone first xGiB region.
|
||||||
for size > GiB {
|
for size > GiB {
|
||||||
err = callDuplicateExtentsToFile(src.Fd(), dst.Fd(), srcOffset, dstOffset, GiB)
|
_, err = withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) {
|
||||||
|
return 0, callDuplicateExtentsToFile(srcFd, dstFd, srcOffset, dstOffset, GiB)
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapError(err)
|
return wrapError(err)
|
||||||
}
|
}
|
||||||
@ -73,7 +101,9 @@ func copyRangeDuplicateExtents(src, dst basicFile, srcOffset, dstOffset, size in
|
|||||||
|
|
||||||
// Clone tail. First try with 64KiB round up, then fallback to 4KiB.
|
// Clone tail. First try with 64KiB round up, then fallback to 4KiB.
|
||||||
for _, cloneRegionSize := range availableClusterSize {
|
for _, cloneRegionSize := range availableClusterSize {
|
||||||
err = callDuplicateExtentsToFile(src.Fd(), dst.Fd(), srcOffset, dstOffset, roundUp(size, cloneRegionSize))
|
_, err = withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) {
|
||||||
|
return 0, callDuplicateExtentsToFile(srcFd, dstFd, srcOffset, dstOffset, roundUp(size, cloneRegionSize))
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -94,7 +124,7 @@ func wrapError(err error) error {
|
|||||||
// see https://docs.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file
|
// see https://docs.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file
|
||||||
//
|
//
|
||||||
// memo: Overflow (cloneRegionSize is greater than file ends) is safe and just ignored by windows.
|
// memo: Overflow (cloneRegionSize is greater than file ends) is safe and just ignored by windows.
|
||||||
func callDuplicateExtentsToFile(src, dst uintptr, srcOffset, dstOffset int64, cloneRegionSize int64) (err error) {
|
func callDuplicateExtentsToFile(src, dst uintptr, srcOffset, dstOffset int64, cloneRegionSize int64) error {
|
||||||
var (
|
var (
|
||||||
bytesReturned uint32
|
bytesReturned uint32
|
||||||
overlapped windows.Overlapped
|
overlapped windows.Overlapped
|
||||||
|
@ -4,11 +4,12 @@
|
|||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
// +build !windows,!solaris,!darwin
|
// +build linux
|
||||||
|
|
||||||
package fs
|
package fs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
@ -43,6 +44,10 @@ func copyRangeIoctl(src, dst basicFile, srcOffset, dstOffset, size int64) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if srcOffset+size > fi.Size() {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
// https://www.man7.org/linux/man-pages/man2/ioctl_ficlonerange.2.html
|
// https://www.man7.org/linux/man-pages/man2/ioctl_ficlonerange.2.html
|
||||||
// If src_length is zero, the ioctl reflinks to the end of the source file.
|
// If src_length is zero, the ioctl reflinks to the end of the source file.
|
||||||
if srcOffset+size == fi.Size() {
|
if srcOffset+size == fi.Size() {
|
||||||
@ -51,20 +56,36 @@ func copyRangeIoctl(src, dst basicFile, srcOffset, dstOffset, size int64) error
|
|||||||
|
|
||||||
if srcOffset == 0 && dstOffset == 0 && size == 0 {
|
if srcOffset == 0 && dstOffset == 0 && size == 0 {
|
||||||
// Optimization for whole file copies.
|
// Optimization for whole file copies.
|
||||||
_, _, errNo := syscall.Syscall(syscall.SYS_IOCTL, dst.Fd(), FICLONE, src.Fd())
|
var errNo syscall.Errno
|
||||||
|
_, err := withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) {
|
||||||
|
_, _, errNo = syscall.Syscall(syscall.SYS_IOCTL, dstFd, FICLONE, srcFd)
|
||||||
|
return 0, nil
|
||||||
|
})
|
||||||
|
// Failure in withFileDescriptors
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if errNo != 0 {
|
if errNo != 0 {
|
||||||
return errNo
|
return errNo
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
params := fileCloneRange{
|
var errNo syscall.Errno
|
||||||
srcFd: int64(src.Fd()),
|
_, err = withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) {
|
||||||
srcOffset: uint64(srcOffset),
|
params := fileCloneRange{
|
||||||
srcLength: uint64(size),
|
srcFd: int64(srcFd),
|
||||||
dstOffset: uint64(dstOffset),
|
srcOffset: uint64(srcOffset),
|
||||||
|
srcLength: uint64(size),
|
||||||
|
dstOffset: uint64(dstOffset),
|
||||||
|
}
|
||||||
|
_, _, errNo = syscall.Syscall(syscall.SYS_IOCTL, dstFd, FICLONERANGE, uintptr(unsafe.Pointer(¶ms)))
|
||||||
|
return 0, nil
|
||||||
|
})
|
||||||
|
// Failure in withFileDescriptors
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
_, _, errNo := syscall.Syscall(syscall.SYS_IOCTL, dst.Fd(), FICLONERANGE, uintptr(unsafe.Pointer(¶ms)))
|
|
||||||
if errNo != 0 {
|
if errNo != 0 {
|
||||||
return errNo
|
return errNo
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
// +build !windows,!darwin
|
// +build linux solaris
|
||||||
|
|
||||||
package fs
|
package fs
|
||||||
|
|
||||||
@ -51,7 +51,9 @@ func copyRangeSendFile(src, dst basicFile, srcOffset, dstOffset, size int64) err
|
|||||||
// following the last byte that was read. If offset is not NULL, then sendfile() does not modify the current
|
// following the last byte that was read. If offset is not NULL, then sendfile() does not modify the current
|
||||||
// file offset of in_fd; otherwise the current file offset is adjusted to reflect the number of bytes read from
|
// file offset of in_fd; otherwise the current file offset is adjusted to reflect the number of bytes read from
|
||||||
// in_fd.
|
// in_fd.
|
||||||
n, err := syscall.Sendfile(int(dst.Fd()), int(src.Fd()), &srcOffset, int(size))
|
n, err := withFileDescriptors(dst, src, func(dstFd, srcFd uintptr) (int, error) {
|
||||||
|
return syscall.Sendfile(int(dstFd), int(srcFd), &srcOffset, int(size))
|
||||||
|
})
|
||||||
if n == 0 && err == nil {
|
if n == 0 && err == nil {
|
||||||
err = io.ErrUnexpectedEOF
|
err = io.ErrUnexpectedEOF
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -51,7 +52,7 @@ var (
|
|||||||
expectedErrors: nil,
|
expectedErrors: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "append to end, offsets at start",
|
name: "append to end offsets at start",
|
||||||
srcSize: generationSize,
|
srcSize: generationSize,
|
||||||
dstSize: generationSize,
|
dstSize: generationSize,
|
||||||
srcOffset: 0,
|
srcOffset: 0,
|
||||||
@ -124,27 +125,87 @@ var (
|
|||||||
copySize: defaultCopySize * 10,
|
copySize: defaultCopySize * 10,
|
||||||
// ioctl returns syscall.EINVAL, rest are wrapped
|
// ioctl returns syscall.EINVAL, rest are wrapped
|
||||||
expectedErrors: map[CopyRangeMethod]error{
|
expectedErrors: map[CopyRangeMethod]error{
|
||||||
CopyRangeMethodIoctl: syscall.EINVAL,
|
CopyRangeMethodIoctl: io.ErrUnexpectedEOF,
|
||||||
CopyRangeMethodStandard: io.ErrUnexpectedEOF,
|
CopyRangeMethodStandard: io.ErrUnexpectedEOF,
|
||||||
CopyRangeMethodCopyFileRange: io.ErrUnexpectedEOF,
|
CopyRangeMethodCopyFileRange: io.ErrUnexpectedEOF,
|
||||||
CopyRangeMethodSendFile: io.ErrUnexpectedEOF,
|
CopyRangeMethodSendFile: io.ErrUnexpectedEOF,
|
||||||
CopyRangeMethodAllWithFallback: io.ErrUnexpectedEOF,
|
CopyRangeMethodAllWithFallback: io.ErrUnexpectedEOF,
|
||||||
|
CopyRangeMethodDuplicateExtents: io.ErrUnexpectedEOF,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Non block sized file
|
|
||||||
{
|
{
|
||||||
name: "not block aligned write",
|
name: "unaligned source offset unaligned size",
|
||||||
srcSize: generationSize + 2,
|
srcSize: generationSize,
|
||||||
dstSize: 0,
|
dstSize: 0,
|
||||||
srcOffset: 1,
|
srcOffset: 1,
|
||||||
dstOffset: 0,
|
dstOffset: 0,
|
||||||
srcStartingPos: 0,
|
srcStartingPos: 0,
|
||||||
dstStartingPos: 0,
|
dstStartingPos: 0,
|
||||||
expectedDstSizeAfterCopy: generationSize + 1,
|
expectedDstSizeAfterCopy: defaultCopySize + 1,
|
||||||
copySize: generationSize + 1,
|
copySize: defaultCopySize + 1,
|
||||||
// Only fails for ioctl
|
|
||||||
expectedErrors: map[CopyRangeMethod]error{
|
expectedErrors: map[CopyRangeMethod]error{
|
||||||
CopyRangeMethodIoctl: syscall.EINVAL,
|
CopyRangeMethodIoctl: syscall.EINVAL,
|
||||||
|
CopyRangeMethodDuplicateExtents: syscall.EINVAL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unaligned source offset aligned size",
|
||||||
|
srcSize: generationSize,
|
||||||
|
dstSize: 0,
|
||||||
|
srcOffset: 1,
|
||||||
|
dstOffset: 0,
|
||||||
|
srcStartingPos: 0,
|
||||||
|
dstStartingPos: 0,
|
||||||
|
expectedDstSizeAfterCopy: defaultCopySize,
|
||||||
|
copySize: defaultCopySize,
|
||||||
|
expectedErrors: map[CopyRangeMethod]error{
|
||||||
|
CopyRangeMethodIoctl: syscall.EINVAL,
|
||||||
|
CopyRangeMethodDuplicateExtents: syscall.EINVAL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unaligned destination offset unaligned size",
|
||||||
|
srcSize: generationSize,
|
||||||
|
dstSize: generationSize,
|
||||||
|
srcOffset: 0,
|
||||||
|
dstOffset: 1,
|
||||||
|
srcStartingPos: 0,
|
||||||
|
dstStartingPos: 0,
|
||||||
|
expectedDstSizeAfterCopy: generationSize,
|
||||||
|
copySize: defaultCopySize + 1,
|
||||||
|
expectedErrors: map[CopyRangeMethod]error{
|
||||||
|
CopyRangeMethodIoctl: syscall.EINVAL,
|
||||||
|
CopyRangeMethodDuplicateExtents: syscall.EINVAL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unaligned destination offset aligned size",
|
||||||
|
srcSize: generationSize,
|
||||||
|
dstSize: generationSize,
|
||||||
|
srcOffset: 0,
|
||||||
|
dstOffset: 1,
|
||||||
|
srcStartingPos: 0,
|
||||||
|
dstStartingPos: 0,
|
||||||
|
expectedDstSizeAfterCopy: generationSize,
|
||||||
|
copySize: defaultCopySize,
|
||||||
|
expectedErrors: map[CopyRangeMethod]error{
|
||||||
|
CopyRangeMethodIoctl: syscall.EINVAL,
|
||||||
|
CopyRangeMethodDuplicateExtents: syscall.EINVAL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "aligned offsets unaligned size",
|
||||||
|
srcSize: generationSize,
|
||||||
|
dstSize: generationSize,
|
||||||
|
srcOffset: 0,
|
||||||
|
dstOffset: 0,
|
||||||
|
srcStartingPos: 0,
|
||||||
|
dstStartingPos: 0,
|
||||||
|
expectedDstSizeAfterCopy: generationSize,
|
||||||
|
copySize: defaultCopySize + 1,
|
||||||
|
expectedErrors: map[CopyRangeMethod]error{
|
||||||
|
CopyRangeMethodIoctl: syscall.EINVAL,
|
||||||
|
CopyRangeMethodDuplicateExtents: syscall.EINVAL,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Last block that starts on a nice boundary
|
// Last block that starts on a nice boundary
|
||||||
@ -189,131 +250,149 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCopyRange(ttt *testing.T) {
|
func TestCopyRange(tttt *testing.T) {
|
||||||
randSrc := rand.New(rand.NewSource(rand.Int63()))
|
randSrc := rand.New(rand.NewSource(rand.Int63()))
|
||||||
for copyMethod, impl := range copyRangeMethods {
|
paths := filepath.SplitList(os.Getenv("STFSTESTPATH"))
|
||||||
ttt.Run(copyMethod.String(), func(tt *testing.T) {
|
if len(paths) == 0 {
|
||||||
for _, testCase := range testCases {
|
paths = []string{""}
|
||||||
tt.Run(testCase.name, func(t *testing.T) {
|
}
|
||||||
srcBuf := make([]byte, testCase.srcSize)
|
for _, path := range paths {
|
||||||
dstBuf := make([]byte, testCase.dstSize)
|
testPath, err := ioutil.TempDir(path, "")
|
||||||
td, err := ioutil.TempDir(os.Getenv("STFSTESTPATH"), "")
|
if err != nil {
|
||||||
if err != nil {
|
tttt.Fatal(err)
|
||||||
t.Fatal(err)
|
}
|
||||||
}
|
defer os.RemoveAll(testPath)
|
||||||
defer func() { _ = os.RemoveAll(td) }()
|
name := path
|
||||||
fs := NewFilesystem(FilesystemTypeBasic, td)
|
if name == "" {
|
||||||
|
name = "tmp"
|
||||||
if _, err := io.ReadFull(randSrc, srcBuf); err != nil {
|
}
|
||||||
t.Fatal(err)
|
tttt.Run(name, func(ttt *testing.T) {
|
||||||
}
|
for copyMethod, impl := range copyRangeMethods {
|
||||||
|
ttt.Run(copyMethod.String(), func(tt *testing.T) {
|
||||||
if _, err := io.ReadFull(randSrc, dstBuf); err != nil {
|
for _, testCase := range testCases {
|
||||||
t.Fatal(err)
|
tt.Run(testCase.name, func(t *testing.T) {
|
||||||
}
|
srcBuf := make([]byte, testCase.srcSize)
|
||||||
|
dstBuf := make([]byte, testCase.dstSize)
|
||||||
src, err := fs.Create("src")
|
td, err := ioutil.TempDir(testPath, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
|
||||||
defer func() { _ = src.Close() }()
|
|
||||||
|
|
||||||
dst, err := fs.Create("dst")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer func() { _ = dst.Close() }()
|
|
||||||
|
|
||||||
// Write some data
|
|
||||||
|
|
||||||
if _, err := src.Write(srcBuf); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := dst.Write(dstBuf); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the offsets
|
|
||||||
|
|
||||||
if n, err := src.Seek(testCase.srcStartingPos, io.SeekStart); err != nil || n != testCase.srcStartingPos {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if n, err := dst.Seek(testCase.dstStartingPos, io.SeekStart); err != nil || n != testCase.dstStartingPos {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := impl(src.(basicFile), dst.(basicFile), testCase.srcOffset, testCase.dstOffset, testCase.copySize); err != nil {
|
|
||||||
if err == syscall.ENOTSUP {
|
|
||||||
// Test runner can adjust directory in which to run the tests, that allow broader tests.
|
|
||||||
t.Skip("Not supported on the current filesystem, set STFSTESTPATH env var.")
|
|
||||||
}
|
|
||||||
if testCase.expectedErrors[copyMethod] == err {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if testCase.expectedErrors[copyMethod] != nil {
|
|
||||||
t.Fatal("did not get expected error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check offsets where we expect them
|
|
||||||
|
|
||||||
if srcCurPos, err := src.Seek(0, io.SeekCurrent); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if srcCurPos != testCase.srcStartingPos {
|
|
||||||
t.Errorf("src pos expected %d got %d", testCase.srcStartingPos, srcCurPos)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dstCurPos, err := dst.Seek(0, io.SeekCurrent); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if dstCurPos != testCase.dstStartingPos {
|
|
||||||
t.Errorf("dst pos expected %d got %d", testCase.dstStartingPos, dstCurPos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check dst size
|
|
||||||
|
|
||||||
if fi, err := dst.Stat(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if fi.Size() != testCase.expectedDstSizeAfterCopy {
|
|
||||||
t.Errorf("expected %d size, got %d", testCase.expectedDstSizeAfterCopy, fi.Size())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the data is as expected
|
|
||||||
|
|
||||||
if _, err := dst.Seek(0, io.SeekStart); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resultBuf := make([]byte, testCase.expectedDstSizeAfterCopy)
|
|
||||||
if _, err := io.ReadFull(dst, resultBuf); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(srcBuf[testCase.srcOffset:testCase.srcOffset+testCase.copySize], resultBuf[testCase.dstOffset:testCase.dstOffset+testCase.copySize]) {
|
|
||||||
t.Errorf("Not equal")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check not copied content does not get corrupted
|
|
||||||
|
|
||||||
if testCase.dstOffset > testCase.dstSize {
|
|
||||||
if !bytes.Equal(dstBuf[:testCase.dstSize], resultBuf[:testCase.dstSize]) {
|
|
||||||
t.Error("region before copy region not equals")
|
|
||||||
}
|
|
||||||
if !bytes.Equal(resultBuf[testCase.dstSize:testCase.dstOffset], make([]byte, testCase.dstOffset-testCase.dstSize)) {
|
|
||||||
t.Error("found non zeroes in expected zero region")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !bytes.Equal(dstBuf[:testCase.dstOffset], resultBuf[:testCase.dstOffset]) {
|
|
||||||
t.Error("region before copy region not equals")
|
|
||||||
}
|
|
||||||
afterCopyStart := testCase.dstOffset + testCase.copySize
|
|
||||||
|
|
||||||
if afterCopyStart < testCase.dstSize {
|
|
||||||
if !bytes.Equal(dstBuf[afterCopyStart:], resultBuf[afterCopyStart:len(dstBuf)]) {
|
|
||||||
t.Error("region after copy region not equals")
|
|
||||||
}
|
}
|
||||||
}
|
defer os.RemoveAll(td)
|
||||||
|
|
||||||
|
fs := NewFilesystem(FilesystemTypeBasic, td)
|
||||||
|
|
||||||
|
if _, err := io.ReadFull(randSrc, srcBuf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.ReadFull(randSrc, dstBuf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := fs.Create("src")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = src.Close() }()
|
||||||
|
|
||||||
|
dst, err := fs.Create("dst")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = dst.Close() }()
|
||||||
|
|
||||||
|
// Write some data
|
||||||
|
|
||||||
|
if _, err := src.Write(srcBuf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := dst.Write(dstBuf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the offsets
|
||||||
|
|
||||||
|
if n, err := src.Seek(testCase.srcStartingPos, io.SeekStart); err != nil || n != testCase.srcStartingPos {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, err := dst.Seek(testCase.dstStartingPos, io.SeekStart); err != nil || n != testCase.dstStartingPos {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := impl(src.(basicFile), dst.(basicFile), testCase.srcOffset, testCase.dstOffset, testCase.copySize); err != nil {
|
||||||
|
if err == syscall.ENOTSUP {
|
||||||
|
// Test runner can adjust directory in which to run the tests, that allow broader tests.
|
||||||
|
t.Skip("Not supported on the current filesystem, set STFSTESTPATH env var.")
|
||||||
|
}
|
||||||
|
if testCase.expectedErrors[copyMethod] == err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if testCase.expectedErrors[copyMethod] != nil {
|
||||||
|
t.Fatal("did not get expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check offsets where we expect them
|
||||||
|
|
||||||
|
if srcCurPos, err := src.Seek(0, io.SeekCurrent); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if srcCurPos != testCase.srcStartingPos {
|
||||||
|
t.Errorf("src pos expected %d got %d", testCase.srcStartingPos, srcCurPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dstCurPos, err := dst.Seek(0, io.SeekCurrent); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if dstCurPos != testCase.dstStartingPos {
|
||||||
|
t.Errorf("dst pos expected %d got %d", testCase.dstStartingPos, dstCurPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check dst size
|
||||||
|
|
||||||
|
if fi, err := dst.Stat(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if fi.Size() != testCase.expectedDstSizeAfterCopy {
|
||||||
|
t.Errorf("expected %d size, got %d", testCase.expectedDstSizeAfterCopy, fi.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the data is as expected
|
||||||
|
|
||||||
|
if _, err := dst.Seek(0, io.SeekStart); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBuf := make([]byte, testCase.expectedDstSizeAfterCopy)
|
||||||
|
if _, err := io.ReadFull(dst, resultBuf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(srcBuf[testCase.srcOffset:testCase.srcOffset+testCase.copySize], resultBuf[testCase.dstOffset:testCase.dstOffset+testCase.copySize]) {
|
||||||
|
t.Errorf("Not equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check not copied content does not get corrupted
|
||||||
|
|
||||||
|
if testCase.dstOffset > testCase.dstSize {
|
||||||
|
if !bytes.Equal(dstBuf[:testCase.dstSize], resultBuf[:testCase.dstSize]) {
|
||||||
|
t.Error("region before copy region not equals")
|
||||||
|
}
|
||||||
|
if !bytes.Equal(resultBuf[testCase.dstSize:testCase.dstOffset], make([]byte, testCase.dstOffset-testCase.dstSize)) {
|
||||||
|
t.Error("found non zeroes in expected zero region")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !bytes.Equal(dstBuf[:testCase.dstOffset], resultBuf[:testCase.dstOffset]) {
|
||||||
|
t.Error("region before copy region not equals")
|
||||||
|
}
|
||||||
|
afterCopyStart := testCase.dstOffset + testCase.copySize
|
||||||
|
|
||||||
|
if afterCopyStart < testCase.dstSize {
|
||||||
|
if !bytes.Equal(dstBuf[afterCopyStart:], resultBuf[afterCopyStart:len(dstBuf)]) {
|
||||||
|
t.Error("region after copy region not equals")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -130,7 +130,7 @@ func (f *MtimeFS) Create(name string) (File, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &mtimeFile{fd, f}, nil
|
return mtimeFile{fd, f}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *MtimeFS) Open(name string) (File, error) {
|
func (f *MtimeFS) Open(name string) (File, error) {
|
||||||
@ -138,7 +138,7 @@ func (f *MtimeFS) Open(name string) (File, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &mtimeFile{fd, f}, nil
|
return mtimeFile{fd, f}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *MtimeFS) OpenFile(name string, flags int, mode FileMode) (File, error) {
|
func (f *MtimeFS) OpenFile(name string, flags int, mode FileMode) (File, error) {
|
||||||
@ -146,7 +146,7 @@ func (f *MtimeFS) OpenFile(name string, flags int, mode FileMode) (File, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &mtimeFile{fd, f}, nil
|
return mtimeFile{fd, f}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// "real" is the on disk timestamp
|
// "real" is the on disk timestamp
|
||||||
@ -208,7 +208,7 @@ type mtimeFile struct {
|
|||||||
fs *MtimeFS
|
fs *MtimeFS
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *mtimeFile) Stat() (FileInfo, error) {
|
func (f mtimeFile) Stat() (FileInfo, error) {
|
||||||
info, err := f.File.Stat()
|
info, err := f.File.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -228,6 +228,10 @@ func (f *mtimeFile) Stat() (FileInfo, error) {
|
|||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f mtimeFile) unwrap() File {
|
||||||
|
return f.File
|
||||||
|
}
|
||||||
|
|
||||||
// The dbMtime is our database representation
|
// The dbMtime is our database representation
|
||||||
|
|
||||||
type dbMtime struct {
|
type dbMtime struct {
|
||||||
|
@ -205,7 +205,11 @@ func TestHandleFileWithTemp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCopierFinder(t *testing.T) {
|
func TestCopierFinder(t *testing.T) {
|
||||||
for _, method := range []fs.CopyRangeMethod{fs.CopyRangeMethodStandard, fs.CopyRangeMethodAllWithFallback} {
|
methods := []fs.CopyRangeMethod{fs.CopyRangeMethodStandard, fs.CopyRangeMethodAllWithFallback}
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
methods = append(methods, fs.CopyRangeMethodSendFile)
|
||||||
|
}
|
||||||
|
for _, method := range methods {
|
||||||
t.Run(method.String(), func(t *testing.T) {
|
t.Run(method.String(), func(t *testing.T) {
|
||||||
// After diff between required and existing we should:
|
// After diff between required and existing we should:
|
||||||
// Copy: 1, 2, 3, 4, 6, 7, 8
|
// Copy: 1, 2, 3, 4, 6, 7, 8
|
||||||
|
Loading…
Reference in New Issue
Block a user