159 lines
4.1 KiB
Go
159 lines
4.1 KiB
Go
|
// +build windows
|
||
|
|
||
|
package tools
|
||
|
|
||
|
import (
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"unsafe"
|
||
|
|
||
|
"golang.org/x/sys/windows"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
availableClusterSize = []int64{64 * 1024, 4 * 1024} // ReFS only supports 64KiB and 4KiB cluster.
|
||
|
GiB = int64(1024 * 1024 * 1024)
|
||
|
)
|
||
|
|
||
|
// fsctlDuplicateExtentsToFile = FSCTL_DUPLICATE_EXTENTS_TO_FILE IOCTL
|
||
|
// Instructs the file system to copy a range of file bytes on behalf of an application.
|
||
|
//
|
||
|
// https://docs.microsoft.com/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file
|
||
|
const fsctlDuplicateExtentsToFile = 623428
|
||
|
|
||
|
// duplicateExtentsData = DUPLICATE_EXTENTS_DATA structure
|
||
|
// Contains parameters for the FSCTL_DUPLICATE_EXTENTS control code that performs the Block Cloning operation.
|
||
|
//
|
||
|
// https://docs.microsoft.com/windows/win32/api/winioctl/ns-winioctl-duplicate_extents_data
|
||
|
type duplicateExtentsData struct {
|
||
|
FileHandle windows.Handle
|
||
|
SourceFileOffset int64
|
||
|
TargetFileOffset int64
|
||
|
ByteCount int64
|
||
|
}
|
||
|
|
||
|
// CheckCloneFileSupported runs explicit test of clone file on supplied directory.
|
||
|
// This function creates some (src and dst) file in the directory and remove after test finished.
|
||
|
//
|
||
|
// If check failed (e.g. directory is read-only), returns err.
|
||
|
func CheckCloneFileSupported(dir string) (supported bool, err error) {
|
||
|
src, err := ioutil.TempFile(dir, "src")
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
defer os.Remove(src.Name())
|
||
|
|
||
|
// Make src file not empty.
|
||
|
// Because `FSCTL_DUPLICATE_EXTENTS_TO_FILE` on empty file is always success even filesystem don't support it.
|
||
|
_, err = src.WriteString("TESTING")
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
|
||
|
dst, err := ioutil.TempFile(dir, "dst")
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
defer os.Remove(dst.Name())
|
||
|
|
||
|
return CloneFile(dst, src)
|
||
|
}
|
||
|
|
||
|
func CloneFileByPath(dst, src string) (success bool, err error) {
|
||
|
dstFile, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, 0666) // No truncate version of os.Create
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
srcFile, err := os.Open(src)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
return CloneFile(dstFile, srcFile)
|
||
|
}
|
||
|
|
||
|
func CloneFile(writer io.Writer, reader io.Reader) (success bool, err error) {
|
||
|
dst, dstIsFile := writer.(*os.File)
|
||
|
src, srcIsFile := reader.(*os.File)
|
||
|
if !(dstIsFile && srcIsFile) {
|
||
|
return false, nil
|
||
|
}
|
||
|
|
||
|
srcStat, err := src.Stat()
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
fileSize := srcStat.Size()
|
||
|
|
||
|
err = dst.Truncate(fileSize) // set file size. Thre is a requirements "The destination region must not extend past the end of file."
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
offset := int64(0)
|
||
|
|
||
|
// Requirement
|
||
|
// * The source and destination regions must begin and end at a cluster boundary. (4KiB or 64KiB)
|
||
|
// * cloneRegionSize less than 4GiB.
|
||
|
// see https://docs.microsoft.com/windows/win32/fileio/block-cloning
|
||
|
|
||
|
// Clone first xGiB region.
|
||
|
for ; offset+GiB < fileSize; offset += GiB {
|
||
|
err = callDuplicateExtentsToFile(dst, src, offset, GiB)
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Clone tail. First try with 64KiB round up, then fallback to 4KiB.
|
||
|
for _, cloneRegionSize := range availableClusterSize {
|
||
|
err = callDuplicateExtentsToFile(dst, src, offset, roundUp(fileSize-offset, cloneRegionSize))
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
|
||
|
return err == nil, err
|
||
|
}
|
||
|
|
||
|
// call FSCTL_DUPLICATE_EXTENTS_TO_FILE IOCTL
|
||
|
// 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.
|
||
|
func callDuplicateExtentsToFile(dst, src *os.File, offset int64, cloneRegionSize int64) (err error) {
|
||
|
var (
|
||
|
bytesReturned uint32
|
||
|
overlapped windows.Overlapped
|
||
|
)
|
||
|
|
||
|
request := duplicateExtentsData{
|
||
|
FileHandle: windows.Handle(src.Fd()),
|
||
|
SourceFileOffset: offset,
|
||
|
TargetFileOffset: offset,
|
||
|
ByteCount: cloneRegionSize,
|
||
|
}
|
||
|
|
||
|
return windows.DeviceIoControl(
|
||
|
windows.Handle(dst.Fd()),
|
||
|
fsctlDuplicateExtentsToFile,
|
||
|
(*byte)(unsafe.Pointer(&request)),
|
||
|
uint32(unsafe.Sizeof(request)),
|
||
|
(*byte)(unsafe.Pointer(nil)), // = nullptr
|
||
|
0,
|
||
|
&bytesReturned,
|
||
|
&overlapped)
|
||
|
}
|
||
|
|
||
|
func roundUp(value, base int64) int64 {
|
||
|
mod := value % base
|
||
|
if mod == 0 {
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
return value - mod + base
|
||
|
}
|