//go:build windows // +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 }