diff options
| author | Dan Engelbrecht <[email protected]> | 2025-10-11 20:30:01 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2025-10-11 20:30:01 +0200 |
| commit | ac6d1838d7b9d1a49f064bc67c027e0dda5348bb (patch) | |
| tree | f31f2da84564c735dbccc507628f034e6a29468f /src | |
| parent | add ability to limit concurrency (#565) (diff) | |
| download | zen-ac6d1838d7b9d1a49f064bc67c027e0dda5348bb.tar.xz zen-ac6d1838d7b9d1a49f064bc67c027e0dda5348bb.zip | |
block reference couting copy support windows (#564)
- Improvement: On Windows file systems that allow block reference counting we use it where possible to speed up copy of data during `zen builds download` operations
- Enabled by default, disable with `--allow-file-clone=false`
Diffstat (limited to 'src')
| -rw-r--r-- | src/zen/cmds/builds_cmd.cpp | 242 | ||||
| -rw-r--r-- | src/zen/cmds/builds_cmd.h | 1 | ||||
| -rw-r--r-- | src/zencore/filesystem.cpp | 276 | ||||
| -rw-r--r-- | src/zencore/include/zencore/filesystem.h | 23 |
4 files changed, 475 insertions, 67 deletions
diff --git a/src/zen/cmds/builds_cmd.cpp b/src/zen/cmds/builds_cmd.cpp index 331b200c6..e40d4366b 100644 --- a/src/zen/cmds/builds_cmd.cpp +++ b/src/zen/cmds/builds_cmd.cpp @@ -371,9 +371,10 @@ namespace { const std::vector<std::string_view> DefaultExcludeFolders({UnsyncFolderName, ZenFolderName, UGSFolderName, LegacyZenTempFolderName}); const std::vector<std::string_view> DefaultExcludeExtensions({}); - static bool IsVerbose = false; - static bool IsQuiet = false; - static ProgressBar::Mode ProgressMode = ProgressBar::Mode::Pretty; + static bool IsVerbose = false; + static bool IsQuiet = false; + static ProgressBar::Mode ProgressMode = ProgressBar::Mode::Pretty; + static bool AllowFileClone = true; uint32_t GetUpdateDelayMS(ProgressBar::Mode InMode) { @@ -566,22 +567,34 @@ namespace { const std::filesystem::path& TargetFilePath, uint64_t RawSize, std::atomic<uint64_t>& WriteCount, - std::atomic<uint64_t>& WriteByteCount) + std::atomic<uint64_t>& WriteByteCount, + std::atomic<uint64_t>& CloneCount, + std::atomic<uint64_t>& CloneByteCount) { - BasicFile TargetFile(TargetFilePath, BasicFile::Mode::kTruncate); - if (UseSparseFiles) + if (AllowFileClone && TryCloneFile(SourceFilePath, TargetFilePath)) { - PrepareFileForScatteredWrite(TargetFile.Handle(), RawSize); + WriteCount += 1; + WriteByteCount += RawSize; + CloneCount += 1; + CloneByteCount += RawSize; } - uint64_t Offset = 0; - if (!ScanFile(SourceFilePath, 512u * 1024u, [&](const void* Data, size_t Size) { - TargetFile.Write(Data, Size, Offset); - Offset += Size; - WriteCount++; - WriteByteCount += Size; - })) + else { - throw std::runtime_error(fmt::format("Failed to copy scavenged file '{}' to '{}'", SourceFilePath, TargetFilePath)); + BasicFile TargetFile(TargetFilePath, BasicFile::Mode::kTruncate); + if (UseSparseFiles) + { + PrepareFileForScatteredWrite(TargetFile.Handle(), RawSize); + } + uint64_t Offset = 0; + if (!ScanFile(SourceFilePath, 512u * 1024u, [&](const void* Data, size_t Size) { + TargetFile.Write(Data, Size, Offset); + Offset += Size; + WriteCount++; + WriteByteCount += Size; + })) + { + throw std::runtime_error(fmt::format("Failed to copy scavenged file '{}' to '{}'", SourceFilePath, TargetFilePath)); + } } } @@ -962,6 +975,8 @@ namespace { std::atomic<uint64_t> ReadByteCount = 0; std::atomic<uint64_t> WriteCount = 0; std::atomic<uint64_t> WriteByteCount = 0; + std::atomic<uint64_t> CloneCount = 0; + std::atomic<uint64_t> CloneByteCount = 0; std::atomic<uint64_t> CurrentOpenFileCount = 0; }; @@ -2005,6 +2020,9 @@ namespace { return Result; } + public: + void* Handle() { return m_Source.Handle(); } + private: BasicFile m_Source; const uint64_t m_SourceSize; @@ -4652,15 +4670,17 @@ namespace { "\n OpenWriteCount: {}" "\n ReadCount: {}" "\n ReadByteCount: {}" - "\n WriteCount: {}" - "\n WriteByteCount: {}" + "\n WriteCount: {} ({} cloned)" + "\n WriteByteCount: {} ({} cloned)" "\n CurrentOpenFileCount: {}", DiskStats.OpenReadCount.load(), DiskStats.OpenWriteCount.load(), DiskStats.ReadCount.load(), NiceBytes(DiskStats.ReadByteCount.load()), DiskStats.WriteCount.load(), + DiskStats.CloneCount.load(), NiceBytes(DiskStats.WriteByteCount.load()), + NiceBytes(DiskStats.CloneByteCount.load()), DiskStats.CurrentOpenFileCount.load()); ZEN_CONSOLE_VERBOSE( @@ -7106,8 +7126,14 @@ namespace { const std::filesystem::path TempFilePath = GetTempChunkedSequenceFileName(CacheFolderPath, RemoteSequenceRawHash); - const uint64_t RawSize = ScavengedContent.RawSizes[ScavengeOp.ScavengedContentIndex]; - CopyFile(ScavengedFilePath, TempFilePath, RawSize, DiskStats.WriteCount, DiskStats.WriteByteCount); + const uint64_t RawSize = ScavengedContent.RawSizes[ScavengeOp.ScavengedPathIndex]; + CopyFile(ScavengedFilePath, + TempFilePath, + RawSize, + DiskStats.WriteCount, + DiskStats.WriteByteCount, + DiskStats.CloneCount, + DiskStats.CloneByteCount); const std::filesystem::path CacheFilePath = GetFinalChunkedSequenceFileName(CacheFolderPath, RemoteSequenceRawHash); @@ -7467,6 +7493,12 @@ namespace { }); } + std::unique_ptr<CloneQueryInterface> CloneQuery; + if (AllowFileClone) + { + CloneQuery = GetCloneQueryInterface(CacheFolderPath); + } + for (size_t CopyDataIndex = 0; CopyDataIndex < CacheCopyDatas.size(); CopyDataIndex++) { ZEN_ASSERT(!Options.PrimeCacheOnly); @@ -7482,6 +7514,7 @@ namespace { &RemoteContent, &RemoteLookup, &CacheFolderPath, + &CloneQuery, &LocalLookup, &SequenceIndexChunksLeftToWriteCounters, &WriteCache, @@ -7577,7 +7610,10 @@ namespace { tsl::robin_set<uint32_t> ChunkIndexesWritten; - BufferedOpenFile SourceFile(SourceFilePath, DiskStats); + BufferedOpenFile SourceFile(SourceFilePath, DiskStats); + + bool CanCloneSource = CloneQuery && CloneQuery->CanClone(SourceFile.Handle()); + BufferedWriteFileCache::Local LocalWriter(WriteCache); for (size_t WriteOpIndex = 0; WriteOpIndex < WriteOps.size();) @@ -7593,6 +7629,7 @@ namespace { RemoteContent.ChunkedContent.ChunkCounts[RemoteSequenceIndex]); ZEN_ASSERT(SequenceIndexChunksLeftToWriteCounters[RemoteSequenceIndex].load() > 0); const uint32_t RemotePathIndex = RemoteLookup.SequenceIndexFirstPathIndex[RemoteSequenceIndex]; + const uint64_t TargetSize = RemoteContent.RawSizes[RemotePathIndex]; const uint64_t ChunkSize = RemoteContent.ChunkedContent.ChunkRawSizes[Op.ChunkIndex]; uint64_t ReadLength = ChunkSize; @@ -7625,18 +7662,102 @@ namespace { WriteCount++; } - CompositeBuffer ChunkSource = SourceFile.GetRange(Op.CacheFileOffset, ReadLength); + { + bool DidClone = false; + + if (CanCloneSource) + { + uint64_t PreBytes = 0; + uint64_t PostBytes = 0; + uint64_t ClonableBytes = CloneQuery->GetClonableRange(Op.CacheFileOffset, + Op.Target->Offset, + ReadLength, + PreBytes, + PostBytes); + if (ClonableBytes > 0) + { + // We need to open the file... + BufferedWriteFileCache::Local::Writer* Writer = LocalWriter.GetWriter(RemoteSequenceIndex); + if (!Writer) + { + Writer = + LocalWriter.PutWriter(RemoteSequenceIndex, + std::make_unique<BufferedWriteFileCache::Local::Writer>()); + + Writer->File = std::make_unique<BasicFile>(); + + const std::filesystem::path FileName = GetTempChunkedSequenceFileName( + CacheFolderPath, + RemoteContent.ChunkedContent.SequenceRawHashes[RemoteSequenceIndex]); + Writer->File->Open(FileName, BasicFile::Mode::kWrite); + if (UseSparseFiles) + { + PrepareFileForScatteredWrite(Writer->File->Handle(), TargetSize); + } + } + DidClone = CloneQuery->TryClone(SourceFile.Handle(), + Writer->File->Handle(), + Op.CacheFileOffset + PreBytes, + Op.Target->Offset + PreBytes, + ClonableBytes, + TargetSize); + if (DidClone) + { + DiskStats.WriteCount++; + DiskStats.WriteByteCount += ClonableBytes; + + DiskStats.CloneCount++; + DiskStats.CloneByteCount += ClonableBytes; - const uint64_t FileOffset = Op.Target->Offset; + if (PreBytes > 0) + { + CompositeBuffer ChunkSource = SourceFile.GetRange(Op.CacheFileOffset, PreBytes); + const uint64_t FileOffset = Op.Target->Offset; + + WriteSequenceChunk(CacheFolderPath, + RemoteContent, + LocalWriter, + ChunkSource, + RemoteSequenceIndex, + FileOffset, + RemotePathIndex, + DiskStats); + } + if (PostBytes > 0) + { + CompositeBuffer ChunkSource = + SourceFile.GetRange(Op.CacheFileOffset + ReadLength - PostBytes, PostBytes); + const uint64_t FileOffset = Op.Target->Offset + ReadLength - PostBytes; + + WriteSequenceChunk(CacheFolderPath, + RemoteContent, + LocalWriter, + ChunkSource, + RemoteSequenceIndex, + FileOffset, + RemotePathIndex, + DiskStats); + } + } + } + } - WriteSequenceChunk(CacheFolderPath, - RemoteContent, - LocalWriter, - ChunkSource, - RemoteSequenceIndex, - FileOffset, - RemotePathIndex, - DiskStats); + if (!DidClone) + { + CompositeBuffer ChunkSource = SourceFile.GetRange(Op.CacheFileOffset, ReadLength); + + const uint64_t FileOffset = Op.Target->Offset; + + WriteSequenceChunk(CacheFolderPath, + RemoteContent, + LocalWriter, + ChunkSource, + RemoteSequenceIndex, + FileOffset, + RemotePathIndex, + DiskStats); + } + } CacheLocalFileBytesRead += ReadLength; // TODO: This should be the sum of unique chunk sizes? @@ -8169,11 +8290,17 @@ namespace { (DownloadStats.RequestsCompleteCount == TotalRequestCount) ? "" : fmt::format(" {}bits/s", NiceNum(FilteredDownloadedBytesPerSecond.GetCurrent() * 8)); + std::string CloneDetails; + if (DiskStats.CloneCount.load() > 0) + { + CloneDetails = fmt::format(" ({} cloned)", NiceBytes(DiskStats.CloneByteCount.load())); + } std::string WriteDetails = Options.PrimeCacheOnly ? "" - : fmt::format(" {}/{} ({}B/s) written.", + : fmt::format(" {}/{} ({}B/s) written{}.", NiceBytes(DiskStats.WriteByteCount.load()), NiceBytes(BytesToWrite), - NiceNum(FilteredWrittenBytesPerSecond.GetCurrent())); + NiceNum(FilteredWrittenBytesPerSecond.GetCurrent()), + CloneDetails); std::string Details = fmt::format("{}/{} ({}{}) downloaded.{}", DownloadStats.RequestsCompleteCount.load(), TotalRequestCount, @@ -8191,6 +8318,8 @@ namespace { }); } + CloneQuery.reset(); + FilteredWrittenBytesPerSecond.Stop(); FilteredDownloadedBytesPerSecond.Stop(); @@ -8230,12 +8359,18 @@ namespace { +DownloadStats.DownloadedPartialBlockByteCount.load(); if (!IsQuiet) { - ZEN_CONSOLE("Downloaded {} ({}bits/s) in {}. Wrote {} ({}B/s) in {}. Completed in {}", + std::string CloneDetails; + if (DiskStats.CloneCount.load() > 0) + { + CloneDetails = fmt::format(" ({} cloned)", NiceBytes(DiskStats.CloneByteCount.load())); + } + ZEN_CONSOLE("Downloaded {} ({}bits/s) in {}. Wrote {} ({}B/s){} in {}. Completed in {}", NiceBytes(DownloadedBytes), NiceNum(GetBytesPerSecond(FilteredDownloadedBytesPerSecond.GetElapsedTimeUS(), DownloadedBytes * 8)), NiceTimeSpanMs(FilteredDownloadedBytesPerSecond.GetElapsedTimeUS() / 1000), NiceBytes(DiskStats.WriteByteCount.load()), NiceNum(GetBytesPerSecond(FilteredWrittenBytesPerSecond.GetElapsedTimeUS(), DiskStats.WriteByteCount.load())), + CloneDetails, NiceTimeSpanMs(FilteredWrittenBytesPerSecond.GetElapsedTimeUS() / 1000), NiceTimeSpanMs(WriteTimer.GetElapsedTimeMs())); } @@ -8603,7 +8738,15 @@ namespace { const uint64_t RawSize = LocalContent.RawSizes[LocalPathIndex]; std::atomic<uint64_t> WriteCount; std::atomic<uint64_t> WriteByteCount; - CopyFile(SourceFilePath, FirstTargetFilePath, RawSize, WriteCount, WriteByteCount); + std::atomic<uint64_t> CloneCount; + std::atomic<uint64_t> CloneByteCount; + CopyFile(SourceFilePath, + FirstTargetFilePath, + RawSize, + WriteCount, + WriteByteCount, + CloneCount, + CloneByteCount); RebuildFolderStateStats.FinalizeTreeFilesCopiedCount++; } else @@ -8663,7 +8806,15 @@ namespace { const uint64_t RawSize = RemoteContent.RawSizes[RemotePathIndex]; std::atomic<uint64_t> WriteCount; std::atomic<uint64_t> WriteByteCount; - CopyFile(FirstTargetFilePath, TargetFilePath, RawSize, WriteCount, WriteByteCount); + std::atomic<uint64_t> CloneCount; + std::atomic<uint64_t> CloneByteCount; + CopyFile(FirstTargetFilePath, + TargetFilePath, + RawSize, + WriteCount, + WriteByteCount, + CloneCount, + CloneByteCount); RebuildFolderStateStats.FinalizeTreeFilesCopiedCount++; } @@ -10546,6 +10697,13 @@ BuildsCommand::BuildsCommand() m_DownloadOptions.parse_positional({"local-path", "build-id", "build-part-name"}); m_DownloadOptions.positional_help("local-path build-id build-part-name"); + m_DownloadOptions.add_option("", + "", + "allow-file-clone", + "Enable use of block reference counting when copying files", + cxxopts::value(m_AllowFileClone), + "<allowclone>"); + // ls AddSystemOptions(m_LsOptions); AddCloudOptions(m_LsOptions); @@ -10608,6 +10766,12 @@ BuildsCommand::BuildsCommand() "Enable scavenging of data from previouse download locations", cxxopts::value(m_EnableScavenging), "<scavenge>"); + m_TestOptions.add_option("", + "", + "allow-file-clone", + "Enable use of block reference counting when copying files", + cxxopts::value(m_AllowFileClone), + "<allowclone>"); m_TestOptions.parse_positional({"local-path"}); m_TestOptions.positional_help("local-path"); @@ -10679,6 +10843,12 @@ BuildsCommand::BuildsCommand() "Enable scavenging of data from previouse download locations", cxxopts::value(m_EnableScavenging), "<scavenge>"); + m_MultiTestDownloadOptions.add_option("", + "", + "allow-file-clone", + "Enable use of block reference counting when copying files", + cxxopts::value(m_AllowFileClone), + "<allowclone>"); m_MultiTestDownloadOptions.parse_positional({"local-path"}); m_MultiTestDownloadOptions.positional_help("local-path"); } @@ -10814,6 +10984,8 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) }; ParseOutputOptions(); + AllowFileClone = m_AllowFileClone; + std::unique_ptr<AuthMgr> Auth; HttpClientSettings ClientSettings{.LogCategory = "httpbuildsclient", .AssumeHttp2 = m_AssumeHttp2, diff --git a/src/zen/cmds/builds_cmd.h b/src/zen/cmds/builds_cmd.h index ddec1f791..e909a6155 100644 --- a/src/zen/cmds/builds_cmd.h +++ b/src/zen/cmds/builds_cmd.h @@ -35,6 +35,7 @@ private: bool m_BoostWorkerThreads = false; bool m_UseSparseFiles = true; bool m_Quiet = false; + bool m_AllowFileClone = true; std::filesystem::path m_ZenFolderPath; diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp index d18f21dbe..5ac460fd9 100644 --- a/src/zencore/filesystem.cpp +++ b/src/zencore/filesystem.cpp @@ -621,24 +621,217 @@ SupportsBlockRefCounting(std::filesystem::path Path) #endif // ZEN_PLATFORM_WINDOWS } -static bool -CloneFile(std::filesystem::path FromPath, std::filesystem::path ToPath) +#if ZEN_PLATFORM_WINDOWS +class WindowsCloneQueryInterface : public CloneQueryInterface +{ +public: + WindowsCloneQueryInterface(uint64_t AlignmentSize, DWORD TargetVolumeSerialNumber) + : m_AlignmentSize(AlignmentSize) + , m_TargetVolumeSerialNumber(TargetVolumeSerialNumber) + { + } + virtual bool CanClone(void* SourceNativeHandle) override + { + HANDLE SourceFile = (HANDLE)SourceNativeHandle; + DWORD SourceVolumeSerialNumber = 0; + if (GetVolumeInformationByHandleW(SourceFile, nullptr, MAX_PATH, &SourceVolumeSerialNumber, nullptr, nullptr, nullptr, 0) == FALSE) + { + return false; + } + if (SourceVolumeSerialNumber != m_TargetVolumeSerialNumber) + { + return false; + } + return true; + } + + virtual uint64_t GetClonableRange(uint64_t SourceOffset, + uint64_t TargetOffset, + uint64_t Size, + uint64_t& OutPreBytes, + uint64_t& OutPostBytes) override + { + if (Size < m_AlignmentSize) + { + return 0; + } + + uint64_t PreBytes = (m_AlignmentSize - (SourceOffset % m_AlignmentSize)) % m_AlignmentSize; + uint64_t PostBytes = (SourceOffset + Size) % m_AlignmentSize; + ZEN_ASSERT(Size >= PreBytes + PostBytes); + if (Size - (PreBytes + PostBytes) < m_AlignmentSize) + { + return 0; + } + ZEN_ASSERT((PreBytes < Size && PostBytes < Size && Size >= PreBytes + PostBytes + m_AlignmentSize)); + + const uint64_t DestCloneOffset = TargetOffset + PreBytes; + if (DestCloneOffset % m_AlignmentSize != 0) + { + return 0; + } + + OutPreBytes = PreBytes; + OutPostBytes = PostBytes; + uint64_t CloneSize = Size - (PreBytes + PostBytes); + ZEN_ASSERT(CloneSize % m_AlignmentSize == 0); + return CloneSize; + } + + virtual bool TryClone(void* SourceNativeHandle, + void* TargetNativeHandle, + uint64_t AlignedSourceOffset, + uint64_t AlignedTargetOffset, + uint64_t AlignedSize, + uint64_t TargetFinalSize) + { + ZEN_ASSERT_SLOW(CanClone(SourceNativeHandle)); + ZEN_ASSERT((AlignedSourceOffset % m_AlignmentSize) == 0); + ZEN_ASSERT((AlignedTargetOffset % m_AlignmentSize) == 0); + ZEN_ASSERT(AlignedSize > 0); + ZEN_ASSERT((AlignedSize % m_AlignmentSize) == 0); + + HANDLE SourceFile = (HANDLE)SourceNativeHandle; + ZEN_ASSERT(SourceFile != INVALID_HANDLE_VALUE); + HANDLE TargetFile = (HANDLE)TargetNativeHandle; + ZEN_ASSERT(TargetFile != INVALID_HANDLE_VALUE); + + FILE_BASIC_INFO SourceFileInfo{}; + if (GetFileInformationByHandleEx(SourceFile, FileBasicInfo, &SourceFileInfo, sizeof(SourceFileInfo)) == FALSE) + { + std::error_code DummyEc; + ZEN_DEBUG("Failed to get file information for file {}", PathFromHandle(SourceFile, DummyEc)); + return false; + } + FILE_BASIC_INFO DestFileInfo{}; + if (GetFileInformationByHandleEx(TargetFile, FileBasicInfo, &DestFileInfo, sizeof(DestFileInfo)) == FALSE) + { + std::error_code DummyEc; + ZEN_DEBUG("Failed to get file information for file {}", PathFromHandle(TargetFile, DummyEc)); + return false; + } + + if ((SourceFileInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE) != (DestFileInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE)) + { + const bool SourceIsSparse = (SourceFileInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE) != 0; + FILE_SET_SPARSE_BUFFER Sparse{}; + Sparse.SetSparse = SourceIsSparse ? TRUE : FALSE; + DWORD BytesReturned{}; + if (!DeviceIoControl(TargetFile, FSCTL_SET_SPARSE, &Sparse, sizeof(Sparse), nullptr, 0, &BytesReturned, nullptr)) + { + std::error_code DummyEc; + ZEN_DEBUG("Failed setting sparse status for file {} to {}", PathFromHandle(TargetFile, DummyEc), SourceIsSparse); + return false; + } + } + + { + const uint64_t TargetFileSize = FileSizeFromHandle(TargetFile); + if (TargetFileSize != TargetFinalSize) + { + FILE_END_OF_FILE_INFO EOFInfo{}; + EOFInfo.EndOfFile.QuadPart = TargetFinalSize; + if (!SetFileInformationByHandle(TargetFile, FileEndOfFileInfo, &EOFInfo, sizeof(EOFInfo))) + { + std::error_code DummyEc; + ZEN_DEBUG("Failed setting final size {} for file {}", TargetFinalSize, PathFromHandle(TargetFile, DummyEc)); + return false; + } + } + } + { + DUPLICATE_EXTENTS_DATA Extent{}; + Extent.FileHandle = SourceFile; + Extent.SourceFileOffset.QuadPart = AlignedSourceOffset; + Extent.TargetFileOffset.QuadPart = AlignedTargetOffset; + Extent.ByteCount.QuadPart = AlignedSize; + DWORD BytesReturned{}; + if (DeviceIoControl(TargetFile, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &Extent, sizeof(Extent), NULL, 0, &BytesReturned, NULL) != + TRUE) + { + std::error_code DummyEc; + ZEN_DEBUG("Failed cloning {} bytes from file {} at {} to file {} at {}", + Extent.ByteCount.QuadPart, + PathFromHandle(SourceFile, DummyEc), + Extent.SourceFileOffset.QuadPart, + PathFromHandle(TargetFile, DummyEc), + Extent.TargetFileOffset.QuadPart); + return false; + } + } + + return true; + } + +private: + uint64_t m_AlignmentSize; + DWORD m_TargetVolumeSerialNumber; +}; + +#endif // ZEN_PLATFORM_WINDOWS + +std::unique_ptr<CloneQueryInterface> +GetCloneQueryInterface(const std::filesystem::path& TargetDirectory) { #if ZEN_PLATFORM_WINDOWS - windows::Handle FromFile(CreateFileW(FromPath.c_str(), - GENERIC_READ, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, - OPEN_EXISTING, - 0, - nullptr)); - if (FromFile == INVALID_HANDLE_VALUE) + std::filesystem::path TargetFilePath = TargetDirectory / ".cloneinfo"; + windows::Handle TargetFile(CreateFileW(TargetFilePath.c_str(), + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, + OPEN_ALWAYS, + 0, + nullptr)); + if (TargetFile == INVALID_HANDLE_VALUE) { - FromFile.Detach(); - return false; + TargetFile.Detach(); + ThrowLastError(fmt::format("Failed opening file '{}' for write", TargetFilePath)); + } + + DWORD DestVolumeSerialNumber = 0; + DWORD DestVolumeFlags = 0; + if (GetVolumeInformationByHandleW(TargetFile, nullptr, 0, &DestVolumeSerialNumber, nullptr, &DestVolumeFlags, nullptr, 0) == FALSE) + { + ZEN_DEBUG("Failed to get volume information for path {}", TargetFilePath); + return {}; } + TargetFile.Close(); + std::error_code Dummy; + RemoveFile(TargetFilePath, Dummy); + if ((DestVolumeFlags & FILE_SUPPORTS_BLOCK_REFCOUNTING) == 0) + { + return {}; + } + + std::filesystem::path::string_type NativePath = TargetFilePath; + if (NativePath.find(TEXT("\\\\?\\")) == 0) + { + NativePath = NativePath.substr(4); + } + if (std::filesystem::path::string_type::size_type DelimiterPos = NativePath.find(TEXT(":\\")); + DelimiterPos != std::filesystem::path::string_type::npos) + { + NativePath = NativePath.substr(0, DelimiterPos + 2); + DWORD SectorsPerCluster = 0; + DWORD BytesPerSector = 0; + if (GetDiskFreeSpace(NativePath.c_str(), &SectorsPerCluster, &BytesPerSector, nullptr, nullptr) == TRUE) + { + return std::make_unique<WindowsCloneQueryInterface>(SectorsPerCluster * BytesPerSector, DestVolumeSerialNumber); + } + } +#else // ZEN_PLATFORM_WINDOWS + ZEN_UNUSED(TargetDirectory); +#endif // ZEN_PLATFORM_WINDOWS + return {}; +} - ULONG FileSystemFlags; +#if ZEN_PLATFORM_WINDOWS +static bool +TryCloneFile(void* SourceNativeHandle, void* TargetNativeHandle) +{ + HANDLE FromFile = (HANDLE)SourceNativeHandle; + HANDLE TargetFile = (HANDLE)TargetNativeHandle; + ULONG FileSystemFlags; if (!GetVolumeInformationByHandleW(FromFile, nullptr, 0, nullptr, nullptr, /* lpFileSystemFlags */ &FileSystemFlags, nullptr, 0)) { return false; @@ -675,29 +868,11 @@ CloneFile(std::filesystem::path FromPath, std::filesystem::path ToPath) return false; } - SetFileAttributesW(ToPath.c_str(), FILE_ATTRIBUTE_NORMAL); - - windows::Handle TargetFile(CreateFileW(ToPath.c_str(), - GENERIC_READ | GENERIC_WRITE | DELETE, - /* no sharing */ FILE_SHARE_READ, - nullptr, - OPEN_ALWAYS, - 0, - /* hTemplateFile */ FromFile)); - - if (TargetFile == INVALID_HANDLE_VALUE) - { - TargetFile.Detach(); - return false; - } - // Delete target file when handle is closed (we only reset this if the copy succeeds) FILE_DISPOSITION_INFO FileDisposition = {TRUE}; if (!SetFileInformationByHandle(TargetFile, FileDispositionInfo, &FileDisposition, sizeof FileDisposition)) { const DWORD ErrorCode = ::GetLastError(); - TargetFile.Close(); - DeleteFileW(ToPath.c_str()); SetLastError(ErrorCode); return false; } @@ -787,6 +962,43 @@ CloneFile(std::filesystem::path FromPath, std::filesystem::path ToPath) const bool AllOk = (TRUE == SetFileInformationByHandle(TargetFile, FileDispositionInfo, &FileDisposition, sizeof FileDisposition)); return AllOk; +} +#endif // ZEN_PLATFORM_WINDOWS + +bool +TryCloneFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToPath) +{ +#if ZEN_PLATFORM_WINDOWS + windows::Handle FromFile(CreateFileW(FromPath.c_str(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, + OPEN_EXISTING, + 0, + nullptr)); + if (FromFile == INVALID_HANDLE_VALUE) + { + FromFile.Detach(); + return false; + } + + SetFileAttributesW(ToPath.c_str(), FILE_ATTRIBUTE_NORMAL); + + windows::Handle TargetFile(CreateFileW(ToPath.c_str(), + GENERIC_READ | GENERIC_WRITE | DELETE, + /* no sharing */ FILE_SHARE_READ, + nullptr, + OPEN_ALWAYS, + 0, + /* hTemplateFile */ FromFile)); + + if (TargetFile == INVALID_HANDLE_VALUE) + { + TargetFile.Detach(); + return false; + } + return TryCloneFile((void*)FromFile.m_Handle, (void*)TargetFile.m_Handle); + #elif ZEN_PLATFORM_LINUX # if 0 struct ScopedFd @@ -850,7 +1062,7 @@ CopyFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToP if (Options.EnableClone) { - Success = CloneFile(FromPath.native(), ToPath.native()); + Success = TryCloneFile(FromPath.native(), ToPath.native()); if (Success) { diff --git a/src/zencore/include/zencore/filesystem.h b/src/zencore/include/zencore/filesystem.h index 36d4d1b68..92b249cec 100644 --- a/src/zencore/include/zencore/filesystem.h +++ b/src/zencore/include/zencore/filesystem.h @@ -179,6 +179,29 @@ ZENCORE_API void WriteFile(void* NativeHandle, std::error_code& Ec); ZENCORE_API void ReadFile(void* NativeHandle, void* Data, uint64_t Size, uint64_t FileOffset, uint64_t ChunkSize, std::error_code& Ec); +class CloneQueryInterface +{ +public: + virtual ~CloneQueryInterface() {} + + virtual bool CanClone(void* SourceNativeHandle) = 0; + virtual uint64_t GetClonableRange(uint64_t SourceOffset, + uint64_t TargetOffset, + uint64_t Size, + uint64_t& OutPreBytes, + uint64_t& OutPostBytes) = 0; + virtual bool TryClone(void* SourceNativeHandle, + void* TargetNativeHandle, + uint64_t AlignedSourceOffset, + uint64_t AlignedTargetOffset, + uint64_t AlignedSize, + uint64_t TargetFinalSize) = 0; +}; + +ZENCORE_API std::unique_ptr<CloneQueryInterface> GetCloneQueryInterface(const std::filesystem::path& TargetDirectory); + +ZENCORE_API bool TryCloneFile(const std::filesystem::path& FromPath, const std::filesystem::path& ToPath); + struct CopyFileOptions { bool EnableClone = true; |