basicfs_copy_range_duplicateextents.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. // Copyright (C) 2020 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. // +build windows
  7. package fs
  8. import (
  9. "io"
  10. "syscall"
  11. "unsafe"
  12. "golang.org/x/sys/windows"
  13. )
  14. func init() {
  15. registerCopyRangeImplementation(CopyRangeMethodDuplicateExtents, copyRangeImplementationForBasicFile(copyRangeDuplicateExtents))
  16. }
  17. // Inspired by https://github.com/git-lfs/git-lfs/blob/master/tools/util_windows.go
  18. var (
  19. availableClusterSize = []int64{64 * 1024, 4 * 1024} // ReFS only supports 64KiB and 4KiB cluster.
  20. GiB = int64(1024 * 1024 * 1024)
  21. )
  22. // fsctlDuplicateExtentsToFile = FSCTL_DUPLICATE_EXTENTS_TO_FILE IOCTL
  23. // Instructs the file system to copy a range of file bytes on behalf of an application.
  24. //
  25. // https://docs.microsoft.com/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file
  26. const fsctlDuplicateExtentsToFile = 623428
  27. // duplicateExtentsData = DUPLICATE_EXTENTS_DATA structure
  28. // Contains parameters for the FSCTL_DUPLICATE_EXTENTS control code that performs the Block Cloning operation.
  29. //
  30. // https://docs.microsoft.com/windows/win32/api/winioctl/ns-winioctl-duplicate_extents_data
  31. type duplicateExtentsData struct {
  32. FileHandle windows.Handle
  33. SourceFileOffset int64
  34. TargetFileOffset int64
  35. ByteCount int64
  36. }
  37. func copyRangeDuplicateExtents(src, dst basicFile, srcOffset, dstOffset, size int64) error {
  38. var err error
  39. // Check that the destination file has sufficient space
  40. dstFi, err := dst.Stat()
  41. if err != nil {
  42. return err
  43. }
  44. dstSize := dstFi.Size()
  45. if dstSize < dstOffset+size {
  46. // set file size. There is a requirements "The destination region must not extend past the end of file."
  47. if err = dst.Truncate(dstOffset + size); err != nil {
  48. return err
  49. }
  50. dstSize = dstOffset + size
  51. }
  52. // The source file has to be big enough
  53. if fi, err := src.Stat(); err != nil {
  54. return err
  55. } else if fi.Size() < srcOffset+size {
  56. return io.ErrUnexpectedEOF
  57. }
  58. // Requirement
  59. // * The source and destination regions must begin and end at a cluster boundary. (4KiB or 64KiB)
  60. // * cloneRegionSize less than 4GiB.
  61. // see https://docs.microsoft.com/windows/win32/fileio/block-cloning
  62. smallestClusterSize := availableClusterSize[len(availableClusterSize)-1]
  63. if srcOffset%smallestClusterSize != 0 || dstOffset%smallestClusterSize != 0 {
  64. return syscall.EINVAL
  65. }
  66. // Each file gets allocated multiple of "clusterSize" blocks, yet file size determines how much of the last block
  67. // is readable/visible.
  68. // Copies only happen in block sized chunks, hence you can copy non block sized regions of data to a file, as long
  69. // as the regions are copied at the end of the file where the block visibility is adjusted by the file size.
  70. if size%smallestClusterSize != 0 && dstOffset+size != dstSize {
  71. return syscall.EINVAL
  72. }
  73. // Clone first xGiB region.
  74. for size > GiB {
  75. _, err = withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) {
  76. return 0, callDuplicateExtentsToFile(srcFd, dstFd, srcOffset, dstOffset, GiB)
  77. })
  78. if err != nil {
  79. return wrapError(err)
  80. }
  81. size -= GiB
  82. srcOffset += GiB
  83. dstOffset += GiB
  84. }
  85. // Clone tail. First try with 64KiB round up, then fallback to 4KiB.
  86. for _, cloneRegionSize := range availableClusterSize {
  87. _, err = withFileDescriptors(src, dst, func(srcFd, dstFd uintptr) (int, error) {
  88. return 0, callDuplicateExtentsToFile(srcFd, dstFd, srcOffset, dstOffset, roundUp(size, cloneRegionSize))
  89. })
  90. if err != nil {
  91. continue
  92. }
  93. break
  94. }
  95. return wrapError(err)
  96. }
  97. func wrapError(err error) error {
  98. if err == windows.SEVERITY_ERROR {
  99. return syscall.ENOTSUP
  100. }
  101. return err
  102. }
  103. // call FSCTL_DUPLICATE_EXTENTS_TO_FILE IOCTL
  104. // see https://docs.microsoft.com/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file
  105. //
  106. // memo: Overflow (cloneRegionSize is greater than file ends) is safe and just ignored by windows.
  107. func callDuplicateExtentsToFile(src, dst uintptr, srcOffset, dstOffset int64, cloneRegionSize int64) error {
  108. var (
  109. bytesReturned uint32
  110. overlapped windows.Overlapped
  111. )
  112. request := duplicateExtentsData{
  113. FileHandle: windows.Handle(src),
  114. SourceFileOffset: srcOffset,
  115. TargetFileOffset: dstOffset,
  116. ByteCount: cloneRegionSize,
  117. }
  118. return windows.DeviceIoControl(
  119. windows.Handle(dst),
  120. fsctlDuplicateExtentsToFile,
  121. (*byte)(unsafe.Pointer(&request)),
  122. uint32(unsafe.Sizeof(request)),
  123. (*byte)(unsafe.Pointer(nil)), // = nullptr
  124. 0,
  125. &bytesReturned,
  126. &overlapped)
  127. }
  128. func roundUp(value, base int64) int64 {
  129. mod := value % base
  130. if mod == 0 {
  131. return value
  132. }
  133. return value - mod + base
  134. }