diff options
| author | Dan Engelbrecht <[email protected]> | 2025-11-24 10:06:52 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2025-11-24 10:06:52 +0100 |
| commit | 6dcdddbf733b0aa323ffb7ecbe56c04b15c6c16a (patch) | |
| tree | 78685156e98214e4e1125501a8c09cac37bc45f4 /src | |
| parent | changelog (#661) (diff) | |
| download | zen-6dcdddbf733b0aa323ffb7ecbe56c04b15c6c16a.tar.xz zen-6dcdddbf733b0aa323ffb7ecbe56c04b15c6c16a.zip | |
update state when wildcard (#657)
* add --append option and improve state handling when using downloads for `zen builds download`
Diffstat (limited to 'src')
| -rw-r--r-- | src/zen/cmds/builds_cmd.cpp | 886 | ||||
| -rw-r--r-- | src/zen/cmds/builds_cmd.h | 1 | ||||
| -rw-r--r-- | src/zenremotestore/builds/buildcontent.cpp | 253 | ||||
| -rw-r--r-- | src/zenremotestore/builds/buildsavedstate.cpp | 845 | ||||
| -rw-r--r-- | src/zenremotestore/builds/buildstorageoperations.cpp | 261 | ||||
| -rw-r--r-- | src/zenremotestore/chunking/chunkedcontent.cpp | 569 | ||||
| -rw-r--r-- | src/zenremotestore/include/zenremotestore/builds/buildcontent.h | 43 | ||||
| -rw-r--r-- | src/zenremotestore/include/zenremotestore/builds/buildsavedstate.h | 101 | ||||
| -rw-r--r-- | src/zenremotestore/include/zenremotestore/builds/buildstorageoperations.h | 26 | ||||
| -rw-r--r-- | src/zenremotestore/include/zenremotestore/chunking/chunkedcontent.h | 39 | ||||
| -rw-r--r-- | src/zenremotestore/zenremotestore.cpp | 3 | ||||
| -rw-r--r-- | src/zenutil/include/zenutil/wildcard.h | 5 | ||||
| -rw-r--r-- | src/zenutil/wildcard.cpp | 33 |
13 files changed, 2037 insertions, 1028 deletions
diff --git a/src/zen/cmds/builds_cmd.cpp b/src/zen/cmds/builds_cmd.cpp index fcda6e809..665676fcd 100644 --- a/src/zen/cmds/builds_cmd.cpp +++ b/src/zen/cmds/builds_cmd.cpp @@ -23,6 +23,7 @@ #include <zenhttp/httpclient.h> #include <zenhttp/httpclientauth.h> #include <zenhttp/httpcommon.h> +#include <zenremotestore/builds/buildcontent.h> #include <zenremotestore/builds/buildsavedstate.h> #include <zenremotestore/builds/buildstoragecache.h> #include <zenremotestore/builds/buildstorageoperations.h> @@ -62,7 +63,7 @@ ZEN_THIRD_PARTY_INCLUDES_END # include <unistd.h> #endif -#define EXTRA_VERIFY 0 +static const bool DoExtraContentVerify = false; #define ZEN_CLOUD_STORAGE "Cloud Storage" @@ -267,6 +268,29 @@ namespace { static bool IsQuiet = false; static ProgressBar::Mode ProgressMode = ProgressBar::Mode::Pretty; + enum EAppendNewContentMode + { + Replace, + Auto, + Append, + Invalid + }; + + EAppendNewContentMode AppendNewContentModeFromString(const std::string_view ModeString) + { + switch (HashStringAsLowerDjb2(ModeString)) + { + case HashStringDjb2("false"): + return EAppendNewContentMode::Replace; + case HashStringDjb2("auto"): + return EAppendNewContentMode::Auto; + case HashStringDjb2("true"): + return EAppendNewContentMode::Append; + default: + return EAppendNewContentMode::Invalid; + } + } + #define ZEN_CONSOLE_VERBOSE(fmtstr, ...) \ if (IsVerbose) \ { \ @@ -292,32 +316,7 @@ namespace { std::span<const std::string> ExcludeWildcards, const std::filesystem::path& Path) { - const std::string PathString = Path.generic_string(); - bool IncludePath = true; - if (!IncludeWildcards.empty()) - { - IncludePath = false; - for (const std::string& IncludeWildcard : IncludeWildcards) - { - if (MatchWildcard(IncludeWildcard, PathString, /*CaseSensitive*/ false)) - { - IncludePath = true; - break; - } - } - if (!IncludePath) - { - return false; - } - } - for (const std::string& ExcludeWildcard : ExcludeWildcards) - { - if (MatchWildcard(ExcludeWildcard, PathString, /*CaseSensitive*/ false)) - { - return false; - } - } - return true; + return zen::IncludePath(IncludeWildcards, ExcludeWildcards, ToLower(Path.generic_string()), /*CaseSensitive*/ true); } class FilteredRate @@ -490,6 +489,7 @@ namespace { std::move(MetaData), BuildsOperationUploadFolder::Options{.IsQuiet = IsQuiet, .IsVerbose = IsVerbose, + .DoExtraContentValidation = DoExtraContentVerify, .FindBlockMaxCount = FindBlockMaxCount, .BlockReuseMinPercentLimit = BlockReuseMinPercentLimit, .AllowMultiparts = AllowMultiparts, @@ -1079,7 +1079,23 @@ namespace { return SB.ToString(); } - std::vector<std::pair<Oid, std::string>> ResolveBuildPartNames(BuildStorageBase& Storage, + CbObject GetBuild(BuildStorageBase& Storage, const Oid& BuildId) + { + Stopwatch GetBuildTimer; + CbObject BuildObject = Storage.GetBuild(BuildId); + if (!IsQuiet) + { + ZEN_CONSOLE("GetBuild took {}. Name: '{}', Payload size: {}", + NiceTimeSpanMs(GetBuildTimer.GetElapsedTimeMs()), + BuildObject["name"sv].AsString(), + NiceBytes(BuildObject.GetSize())); + + ZEN_CONSOLE("{}", GetCbObjectAsNiceString(BuildObject, " "sv, "\n"sv)); + } + return BuildObject; + } + + std::vector<std::pair<Oid, std::string>> ResolveBuildPartNames(CbObjectView BuildObject, const Oid& BuildId, const std::vector<Oid>& BuildPartIds, std::span<const std::string> BuildPartNames, @@ -1087,18 +1103,6 @@ namespace { { std::vector<std::pair<Oid, std::string>> Result; { - Stopwatch GetBuildTimer; - CbObject BuildObject = Storage.GetBuild(BuildId); - if (!IsQuiet) - { - ZEN_CONSOLE("GetBuild took {}. Name: '{}', Payload size: {}", - NiceTimeSpanMs(GetBuildTimer.GetElapsedTimeMs()), - BuildObject["name"sv].AsString(), - NiceBytes(BuildObject.GetSize())); - - ZEN_CONSOLE("{}", GetCbObjectAsNiceString(BuildObject, " "sv, "\n"sv)); - } - CbObjectView PartsObject = BuildObject["parts"sv].AsObjectView(); if (!PartsObject) { @@ -1251,7 +1255,8 @@ namespace { OutBlockDescriptions, OutRemoteContent.ChunkedContent.ChunkHashes, OutRemoteContent.ChunkedContent.ChunkRawSizes, - OutRemoteContent.ChunkedContent.ChunkOrders); + OutRemoteContent.ChunkedContent.ChunkOrders, + DoExtraContentVerify); if (!IncludeWildcards.empty() || !ExcludeWildcards.empty()) { @@ -1272,7 +1277,7 @@ namespace { } #if ZEN_BUILD_DEBUG - ValidateChunkedFolderContent(OutRemoteContent, OutBlockDescriptions, OutLooseChunkHashes); + ValidateChunkedFolderContent(OutRemoteContent, OutBlockDescriptions, OutLooseChunkHashes, IncludeWildcards, ExcludeWildcards); #endif // ZEN_BUILD_DEBUG }; @@ -1364,276 +1369,60 @@ namespace { return RemoteContent; } - ChunkedFolderContent GetLocalContent(ThreadWorkers& Workers, - GetFolderContentStatistics& LocalFolderScanStats, - ChunkingStatistics& ChunkingStats, - const std::filesystem::path& Path, - const std::filesystem::path& StateFilePath, - ChunkingController& ChunkController, - std::span<const std::filesystem::path> ReferencePaths, - std::span<const std::string> IncludeWildcards, - std::span<const std::string> ExcludeWildcards, - FolderContent& OutLocalFolderContent) + std::vector<std::filesystem::path> GetNewPaths(const std::span<const std::filesystem::path> KnownPaths, + const std::span<const std::filesystem::path> Paths) { - FolderContent LocalFolderState; - ChunkedFolderContent LocalContent; - - Stopwatch ReadStateTimer; - bool HasLocalState = false; - bool FileExists = IsFile(StateFilePath); - if (FileExists) + tsl::robin_set<std::string> KnownPathsSet; + KnownPathsSet.reserve(KnownPaths.size()); + for (const std::filesystem::path& LocalPath : KnownPaths) { - try - { - ReadStateFile(StateFilePath, LocalFolderState, LocalContent); - if (!IsQuiet) - { - ZEN_CONSOLE("Read local state file {} in {}", StateFilePath, NiceTimeSpanMs(ReadStateTimer.GetElapsedTimeMs())); - } - HasLocalState = true; - } - catch (const std::exception& Ex) - { - ZEN_CONSOLE_WARN("Failed reading state file {}, falling back to scannning. Reason: {}", StateFilePath, Ex.what()); - } + KnownPathsSet.insert(LocalPath.generic_string()); } + std::vector<std::filesystem::path> NewPaths; + for (const std::filesystem::path& UntrackedPath : Paths) { - const uint32_t LocalPathCount = gsl::narrow<uint32_t>(LocalFolderState.Paths.size()); - const uint32_t RemotePathCount = gsl::narrow<uint32_t>(ReferencePaths.size()); - - std::vector<std::filesystem::path> PathsToCheck; - PathsToCheck.reserve(LocalPathCount + RemotePathCount); - - tsl::robin_set<std::string> FileSet; - FileSet.reserve(LocalPathCount + RemotePathCount); - - for (const std::filesystem::path& LocalPath : LocalFolderState.Paths) - { - if (IncludePath(IncludeWildcards, ExcludeWildcards, LocalPath)) - { - FileSet.insert(LocalPath.generic_string()); - PathsToCheck.push_back(LocalPath); - } - } - - for (const std::filesystem::path& RemotePath : ReferencePaths) - { - if (IncludePath(IncludeWildcards, ExcludeWildcards, RemotePath)) - { - if (FileSet.insert(RemotePath.generic_string()).second) - { - PathsToCheck.push_back(RemotePath); - } - } - } - + if (!KnownPathsSet.contains(UntrackedPath.generic_string())) { - ProgressBar ProgressBar(ProgressMode, "Check Files"); - OutLocalFolderContent = - GetValidFolderContent(Workers, - LocalFolderScanStats, - Path, - PathsToCheck, - [&ProgressBar, &LocalFolderScanStats](uint64_t PathCount, uint64_t CompletedPathCount) { - std::string Details = fmt::format("{}/{} checked, {} found", - CompletedPathCount, - PathCount, - LocalFolderScanStats.FoundFileCount.load()); - ProgressBar.UpdateState({.Task = "Checking files ", - .Details = Details, - .TotalCount = PathCount, - .RemainingCount = PathCount - CompletedPathCount, - .Status = ProgressBar::State::CalculateStatus(AbortFlag, PauseFlag)}, - false); - }); - ProgressBar.Finish(); - } - if (AbortFlag) - { - return {}; + NewPaths.push_back(UntrackedPath); } } + return NewPaths; + } - bool ScanContent = true; - std::vector<uint32_t> PathIndexesOufOfDate; - if (HasLocalState) + BuildSaveState GetLocalStateFromPaths(ThreadWorkers& Workers, + GetFolderContentStatistics& LocalFolderScanStats, + ChunkingStatistics& ChunkingStats, + const std::filesystem::path& Path, + ChunkingController& ChunkController, + std::span<const std::filesystem::path> PathsToCheck) + { + FolderContent FolderState; + ChunkedFolderContent ChunkedContent; { - if (!LocalFolderState.AreKnownFilesEqual(OutLocalFolderContent)) - { - const size_t LocalStatePathCount = LocalFolderState.Paths.size(); - std::vector<std::filesystem::path> DeletedPaths; - FolderContent UpdatedContent = GetUpdatedContent(LocalFolderState, OutLocalFolderContent, DeletedPaths); - if (!DeletedPaths.empty()) - { - LocalContent = DeletePathsFromChunkedContent(LocalContent, DeletedPaths); - } - - if (!IsQuiet) - { - ZEN_CONSOLE("Updating state, {} local files deleted and {} local files updated out of {}", - DeletedPaths.size(), - UpdatedContent.Paths.size(), - LocalStatePathCount); - } - if (UpdatedContent.Paths.size() > 0) - { - uint64_t ByteCountToScan = 0; - for (const uint64_t RawSize : UpdatedContent.RawSizes) - { - ByteCountToScan += RawSize; - } - ProgressBar ProgressBar(ProgressMode, "Scan Files"); - FilteredRate FilteredBytesHashed; - FilteredBytesHashed.Start(); - ChunkingStatistics LocalChunkingStats; - ChunkedFolderContent UpdatedLocalContent = ChunkFolderContent( - LocalChunkingStats, - Workers.GetIOWorkerPool(), - Path, - UpdatedContent, - ChunkController, - GetUpdateDelayMS(ProgressMode), - [&](bool IsAborted, bool IsPaused, std::ptrdiff_t) { - FilteredBytesHashed.Update(LocalChunkingStats.BytesHashed.load()); - std::string Details = fmt::format("{}/{} ({}/{}, {}B/s) scanned, {} ({}) chunks found", - LocalChunkingStats.FilesProcessed.load(), - UpdatedContent.Paths.size(), - NiceBytes(LocalChunkingStats.BytesHashed.load()), - NiceBytes(ByteCountToScan), - NiceNum(FilteredBytesHashed.GetCurrent()), - LocalChunkingStats.UniqueChunksFound.load(), - NiceBytes(LocalChunkingStats.UniqueBytesFound.load())); - ProgressBar.UpdateState({.Task = "Scanning files ", - .Details = Details, - .TotalCount = ByteCountToScan, - .RemainingCount = ByteCountToScan - LocalChunkingStats.BytesHashed.load(), - .Status = ProgressBar::State::CalculateStatus(IsAborted, IsPaused)}, - false); - }, - AbortFlag, - PauseFlag); - - ChunkingStats += LocalChunkingStats; - - FilteredBytesHashed.Stop(); - ProgressBar.Finish(); - if (AbortFlag) - { - return {}; - } - LocalContent = MergeChunkedFolderContents(LocalContent, {{UpdatedLocalContent}}); - } - } - else - { - // Remove files from LocalContent no longer in LocalFolderState - tsl::robin_set<std::string> LocalFolderPaths; - LocalFolderPaths.reserve(LocalFolderState.Paths.size()); - for (const std::filesystem::path& LocalFolderPath : LocalFolderState.Paths) - { - LocalFolderPaths.insert(LocalFolderPath.generic_string()); - } - std::vector<std::filesystem::path> DeletedPaths; - for (const std::filesystem::path& LocalContentPath : LocalContent.Paths) - { - if (!LocalFolderPaths.contains(LocalContentPath.generic_string())) - { - DeletedPaths.push_back(LocalContentPath); - } - } - if (!DeletedPaths.empty()) - { - LocalContent = DeletePathsFromChunkedContent(LocalContent, DeletedPaths); - } - } - - // Check files that are present in current folder state that matches the ReferencePaths but not known to the local state - { - FolderContent UpdatedContent; - - tsl::robin_set<std::string> LocalPathIndexLookup; - for (const std::filesystem::path& LocalPath : LocalContent.Paths) - { - LocalPathIndexLookup.insert(LocalPath.generic_string()); - } - - tsl::robin_set<std::string> RemotePathIndexLookup; - for (const std::filesystem::path& RemotePath : ReferencePaths) - { - RemotePathIndexLookup.insert(RemotePath.generic_string()); - } - - for (uint32_t LocalFolderPathIndex = 0; LocalFolderPathIndex < OutLocalFolderContent.Paths.size(); LocalFolderPathIndex++) - { - const std::filesystem::path& LocalFolderPath = OutLocalFolderContent.Paths[LocalFolderPathIndex]; - const std::string GenericLocalFolderPath = LocalFolderPath.generic_string(); - if (RemotePathIndexLookup.contains(GenericLocalFolderPath)) - { - if (!LocalPathIndexLookup.contains(GenericLocalFolderPath)) - { - UpdatedContent.Paths.push_back(LocalFolderPath); - UpdatedContent.RawSizes.push_back(OutLocalFolderContent.RawSizes[LocalFolderPathIndex]); - UpdatedContent.Attributes.push_back(OutLocalFolderContent.Attributes[LocalFolderPathIndex]); - UpdatedContent.ModificationTicks.push_back(OutLocalFolderContent.ModificationTicks[LocalFolderPathIndex]); - } - } - } - - if (UpdatedContent.Paths.size() > 0) - { - uint64_t ByteCountToScan = 0; - for (const uint64_t RawSize : UpdatedContent.RawSizes) - { - ByteCountToScan += RawSize; - } - ProgressBar ProgressBar(ProgressMode, "Scan Files"); - FilteredRate FilteredBytesHashed; - FilteredBytesHashed.Start(); - ChunkingStatistics LocalChunkingStats; - ChunkedFolderContent UpdatedLocalContent = ChunkFolderContent( - LocalChunkingStats, - Workers.GetIOWorkerPool(), - Path, - UpdatedContent, - ChunkController, - GetUpdateDelayMS(ProgressMode), - [&](bool IsAborted, bool IsPaused, std::ptrdiff_t) { - FilteredBytesHashed.Update(LocalChunkingStats.BytesHashed.load()); - std::string Details = fmt::format("{}/{} ({}/{}, {}B/s) scanned, {} ({}) chunks found", - LocalChunkingStats.FilesProcessed.load(), - UpdatedContent.Paths.size(), - NiceBytes(LocalChunkingStats.BytesHashed.load()), - NiceBytes(ByteCountToScan), - NiceNum(FilteredBytesHashed.GetCurrent()), - LocalChunkingStats.UniqueChunksFound.load(), - NiceBytes(LocalChunkingStats.UniqueBytesFound.load())); - ProgressBar.UpdateState({.Task = "Scanning files ", - .Details = Details, - .TotalCount = ByteCountToScan, - .RemainingCount = ByteCountToScan - LocalChunkingStats.BytesHashed.load(), - .Status = ProgressBar::State::CalculateStatus(IsAborted, IsPaused)}, - false); - }, - AbortFlag, - PauseFlag); - ChunkingStats += LocalChunkingStats; - FilteredBytesHashed.Stop(); - ProgressBar.Finish(); - if (!AbortFlag) - { - LocalContent = MergeChunkedFolderContents(LocalContent, {{UpdatedLocalContent}}); - } - } - } - - ScanContent = false; + ProgressBar ProgressBar(ProgressMode, "Check Files"); + FolderState = GetValidFolderContent( + Workers, + LocalFolderScanStats, + Path, + PathsToCheck, + [&ProgressBar, &LocalFolderScanStats](uint64_t PathCount, uint64_t CompletedPathCount) { + std::string Details = + fmt::format("{}/{} checked, {} found", CompletedPathCount, PathCount, LocalFolderScanStats.FoundFileCount.load()); + ProgressBar.UpdateState({.Task = "Checking files ", + .Details = Details, + .TotalCount = PathCount, + .RemainingCount = PathCount - CompletedPathCount, + .Status = ProgressBar::State::CalculateStatus(AbortFlag, PauseFlag)}, + false); + }); + ProgressBar.Finish(); } - if (ScanContent) + if (FolderState.Paths.size() > 0) { uint64_t ByteCountToScan = 0; - for (const uint64_t RawSize : OutLocalFolderContent.RawSizes) + for (const uint64_t RawSize : FolderState.RawSizes) { ByteCountToScan += RawSize; } @@ -1641,18 +1430,18 @@ namespace { FilteredRate FilteredBytesHashed; FilteredBytesHashed.Start(); ChunkingStatistics LocalChunkingStats; - LocalContent = ChunkFolderContent( + ChunkedContent = ChunkFolderContent( LocalChunkingStats, Workers.GetIOWorkerPool(), Path, - OutLocalFolderContent, + FolderState, ChunkController, GetUpdateDelayMS(ProgressMode), [&](bool IsAborted, bool IsPaused, std::ptrdiff_t) { FilteredBytesHashed.Update(LocalChunkingStats.BytesHashed.load()); std::string Details = fmt::format("{}/{} ({}/{}, {}B/s) scanned, {} ({}) chunks found", LocalChunkingStats.FilesProcessed.load(), - OutLocalFolderContent.Paths.size(), + FolderState.Paths.size(), NiceBytes(LocalChunkingStats.BytesHashed.load()), NiceBytes(ByteCountToScan), NiceNum(FilteredBytesHashed.GetCurrent()), @@ -1661,7 +1450,7 @@ namespace { ProgressBar.UpdateState({.Task = "Scanning files ", .Details = Details, .TotalCount = ByteCountToScan, - .RemainingCount = (ByteCountToScan - LocalChunkingStats.BytesHashed.load()), + .RemainingCount = ByteCountToScan - LocalChunkingStats.BytesHashed.load(), .Status = ProgressBar::State::CalculateStatus(IsAborted, IsPaused)}, false); }, @@ -1670,12 +1459,161 @@ namespace { ChunkingStats += LocalChunkingStats; FilteredBytesHashed.Stop(); ProgressBar.Finish(); - if (AbortFlag) + } + + return BuildSaveState{.State = BuildState{.ChunkedContent = std::move(ChunkedContent)}, + .FolderState = FolderState, + .LocalPath = Path}; + } + + BuildSaveState GetLocalContent(ThreadWorkers& Workers, + GetFolderContentStatistics& LocalFolderScanStats, + ChunkingStatistics& ChunkingStats, + const std::filesystem::path& Path, + const std::filesystem::path& StateFilePath, + ChunkingController& ChunkController) + { + Stopwatch ReadStateTimer; + bool FileExists = IsFile(StateFilePath); + if (!FileExists) + { + ZEN_CONSOLE("No known local state file in {}, falling back to scanning", Path); + return {}; + } + + BuildSaveState SavedLocalState; + try + { + SavedLocalState = ReadBuildSaveStateFile(StateFilePath); + if (!IsQuiet) + { + ZEN_CONSOLE("Read local state file {} in {}", StateFilePath, NiceTimeSpanMs(ReadStateTimer.GetElapsedTimeMs())); + } + } + catch (const std::exception& Ex) + { + ZEN_CONSOLE_WARN("Failed reading state file {}, falling back to scannning. Reason: {}", StateFilePath, Ex.what()); + return {}; + } + + FolderContent CurrentLocalFolderState; + { + ProgressBar ProgressBar(ProgressMode, "Check Known Files"); + CurrentLocalFolderState = GetValidFolderContent( + Workers, + LocalFolderScanStats, + Path, + SavedLocalState.FolderState.Paths, + [&ProgressBar, &LocalFolderScanStats](uint64_t PathCount, uint64_t CompletedPathCount) { + std::string Details = + fmt::format("{}/{} checked, {} found", CompletedPathCount, PathCount, LocalFolderScanStats.FoundFileCount.load()); + ProgressBar.UpdateState({.Task = "Checking files ", + .Details = Details, + .TotalCount = PathCount, + .RemainingCount = PathCount - CompletedPathCount, + .Status = ProgressBar::State::CalculateStatus(AbortFlag, PauseFlag)}, + false); + }); + ProgressBar.Finish(); + } + if (AbortFlag) + { + return {}; + } + + if (!SavedLocalState.FolderState.AreKnownFilesEqual(CurrentLocalFolderState)) + { + const size_t LocalStatePathCount = SavedLocalState.FolderState.Paths.size(); + std::vector<std::filesystem::path> DeletedPaths; + FolderContent UpdatedContent = GetUpdatedContent(SavedLocalState.FolderState, CurrentLocalFolderState, DeletedPaths); + if (!DeletedPaths.empty()) + { + SavedLocalState.State.ChunkedContent = DeletePathsFromChunkedContent(SavedLocalState.State.ChunkedContent, DeletedPaths); + } + + if (!IsQuiet) + { + ZEN_CONSOLE("Updating state, {} local files deleted and {} local files updated out of {}", + DeletedPaths.size(), + UpdatedContent.Paths.size(), + LocalStatePathCount); + } + if (UpdatedContent.Paths.size() > 0) + { + uint64_t ByteCountToScan = 0; + for (const uint64_t RawSize : UpdatedContent.RawSizes) + { + ByteCountToScan += RawSize; + } + ProgressBar ProgressBar(ProgressMode, "Scan Known Files"); + FilteredRate FilteredBytesHashed; + FilteredBytesHashed.Start(); + ChunkingStatistics LocalChunkingStats; + ChunkedFolderContent UpdatedLocalContent = ChunkFolderContent( + LocalChunkingStats, + Workers.GetIOWorkerPool(), + Path, + UpdatedContent, + ChunkController, + GetUpdateDelayMS(ProgressMode), + [&](bool IsAborted, bool IsPaused, std::ptrdiff_t) { + FilteredBytesHashed.Update(LocalChunkingStats.BytesHashed.load()); + std::string Details = fmt::format("{}/{} ({}/{}, {}B/s) scanned, {} ({}) chunks found", + LocalChunkingStats.FilesProcessed.load(), + UpdatedContent.Paths.size(), + NiceBytes(LocalChunkingStats.BytesHashed.load()), + NiceBytes(ByteCountToScan), + NiceNum(FilteredBytesHashed.GetCurrent()), + LocalChunkingStats.UniqueChunksFound.load(), + NiceBytes(LocalChunkingStats.UniqueBytesFound.load())); + ProgressBar.UpdateState({.Task = "Scanning files ", + .Details = Details, + .TotalCount = ByteCountToScan, + .RemainingCount = ByteCountToScan - LocalChunkingStats.BytesHashed.load(), + .Status = ProgressBar::State::CalculateStatus(IsAborted, IsPaused)}, + false); + }, + AbortFlag, + PauseFlag); + + ChunkingStats += LocalChunkingStats; + + FilteredBytesHashed.Stop(); + ProgressBar.Finish(); + if (AbortFlag) + { + return {}; + } + SavedLocalState.State.ChunkedContent = + MergeChunkedFolderContents(SavedLocalState.State.ChunkedContent, {{UpdatedLocalContent}}); + } + } + else + { + // Remove files from LocalContent no longer in LocalFolderState + tsl::robin_set<std::string> LocalFolderPaths; + LocalFolderPaths.reserve(SavedLocalState.FolderState.Paths.size()); + for (const std::filesystem::path& LocalFolderPath : SavedLocalState.FolderState.Paths) + { + LocalFolderPaths.insert(LocalFolderPath.generic_string()); + } + std::vector<std::filesystem::path> DeletedPaths; + for (const std::filesystem::path& LocalContentPath : SavedLocalState.State.ChunkedContent.Paths) + { + if (!LocalFolderPaths.contains(LocalContentPath.generic_string())) + { + DeletedPaths.push_back(LocalContentPath); + } + } + if (!DeletedPaths.empty()) { - return {}; + SavedLocalState.State.ChunkedContent = DeletePathsFromChunkedContent(SavedLocalState.State.ChunkedContent, DeletedPaths); } } - return LocalContent; + + SavedLocalState.FolderState = CurrentLocalFolderState; + + return SavedLocalState; } ChunkedFolderContent ScanAndChunkFolder( @@ -1705,17 +1643,17 @@ namespace { return {}; } - FolderContent _; - ChunkedFolderContent Result = GetLocalContent(Workers, - GetFolderContentStats, - ChunkingStats, - Path, - ZenStateFilePath(Path / ZenFolderName), - ChunkController, - Content.Paths, - {}, - {}, - _); + BuildState LocalContent = + GetLocalContent(Workers, GetFolderContentStats, ChunkingStats, Path, ZenStateFilePath(Path / ZenFolderName), ChunkController) + .State; + + std::vector<std::filesystem::path> UntrackedPaths = GetNewPaths(LocalContent.ChunkedContent.Paths, Content.Paths); + + BuildState UntrackedLocalContent = + GetLocalStateFromPaths(Workers, GetFolderContentStats, ChunkingStats, Path, ChunkController, UntrackedPaths).State; + + ChunkedFolderContent Result = MergeChunkedFolderContents(LocalContent.ChunkedContent, + std::vector<ChunkedFolderContent>{UntrackedLocalContent.ChunkedContent}); const uint64_t TotalRawSize = std::accumulate(Result.RawSizes.begin(), Result.RawSizes.end(), std::uint64_t(0)); const uint64_t ChunkedRawSize = @@ -1751,6 +1689,7 @@ namespace { std::vector<std::string> ExcludeWildcards; uint64_t MaximumInMemoryPayloadSize = 512u * 1024u; bool PopulateCache = true; + bool AppendNewContent = false; }; void DownloadFolder(ThreadWorkers& Workers, @@ -1790,8 +1729,10 @@ namespace { std::uint64_t PreferredMultipartChunkSize = 32u * 1024u * 1024u; + CbObject BuildObject = GetBuild(*Storage.BuildStorage, BuildId); + std::vector<std::pair<Oid, std::string>> AllBuildParts = - ResolveBuildPartNames(*Storage.BuildStorage, BuildId, BuildPartIds, BuildPartNames, PreferredMultipartChunkSize); + ResolveBuildPartNames(BuildObject, BuildId, BuildPartIds, BuildPartNames, PreferredMultipartChunkSize); std::vector<ChunkedFolderContent> PartContents; @@ -1811,12 +1752,15 @@ namespace { PartContents, BlockDescriptions, LooseChunkHashes); - +#if ZEN_BUILD_DEBUG + ValidateChunkedFolderContent(RemoteContent, BlockDescriptions, LooseChunkHashes, {}, {}); +#endif // ZEN_BUILD_DEBUG const std::uint64_t LargeAttachmentSize = Options.AllowMultiparts ? PreferredMultipartChunkSize * 4u : (std::uint64_t)-1; GetFolderContentStatistics LocalFolderScanStats; ChunkingStatistics ChunkingStats; - ChunkedFolderContent LocalContent; - FolderContent LocalFolderContent; + + BuildSaveState LocalState; + if (!Options.PrimeCacheOnly) { if (IsDir(Path)) @@ -1827,18 +1771,52 @@ namespace { ChunkController = CreateStandardChunkingController(StandardChunkingControllerSettings{}); } - LocalContent = GetLocalContent(Workers, - LocalFolderScanStats, - ChunkingStats, - Path, - ZenStateFilePath(Options.ZenFolderPath), - *ChunkController, - RemoteContent.Paths, - Options.IncludeWildcards, - Options.ExcludeWildcards, - LocalFolderContent); + LocalState = GetLocalContent(Workers, + LocalFolderScanStats, + ChunkingStats, + Path, + ZenStateFilePath(Path / ZenFolderName), + *ChunkController); + + std::vector<std::filesystem::path> UntrackedPaths = GetNewPaths(LocalState.State.ChunkedContent.Paths, RemoteContent.Paths); + + BuildSaveState UntrackedLocalContent = + GetLocalStateFromPaths(Workers, LocalFolderScanStats, ChunkingStats, Path, *ChunkController, UntrackedPaths); + + if (!UntrackedLocalContent.State.ChunkedContent.Paths.empty()) + { + LocalState.State.ChunkedContent = + MergeChunkedFolderContents(LocalState.State.ChunkedContent, + std::vector<ChunkedFolderContent>{UntrackedLocalContent.State.ChunkedContent}); + + // TODO: Helper + LocalState.FolderState.Paths.insert(LocalState.FolderState.Paths.begin(), + UntrackedLocalContent.FolderState.Paths.begin(), + UntrackedLocalContent.FolderState.Paths.end()); + LocalState.FolderState.RawSizes.insert(LocalState.FolderState.RawSizes.begin(), + UntrackedLocalContent.FolderState.RawSizes.begin(), + UntrackedLocalContent.FolderState.RawSizes.end()); + LocalState.FolderState.Attributes.insert(LocalState.FolderState.Attributes.begin(), + UntrackedLocalContent.FolderState.Attributes.begin(), + UntrackedLocalContent.FolderState.Attributes.end()); + LocalState.FolderState.ModificationTicks.insert(LocalState.FolderState.ModificationTicks.begin(), + UntrackedLocalContent.FolderState.ModificationTicks.begin(), + UntrackedLocalContent.FolderState.ModificationTicks.end()); + } + + if (Options.AppendNewContent) + { + RemoteContent = ApplyChunkedContentOverlay(LocalState.State.ChunkedContent, + RemoteContent, + Options.IncludeWildcards, + Options.ExcludeWildcards); + } #if ZEN_BUILD_DEBUG - ValidateChunkedFolderContent(LocalContent, {}, LocalContent.ChunkedContent.ChunkHashes); + ValidateChunkedFolderContent(RemoteContent, + BlockDescriptions, + LooseChunkHashes, + Options.IncludeWildcards, + Options.ExcludeWildcards); #endif // ZEN_BUILD_DEBUG } else @@ -1851,7 +1829,32 @@ namespace { return; } - if (Options.EnableTargetFolderScavenging && !Options.CleanTargetFolder && CompareChunkedContent(RemoteContent, LocalContent)) + LocalState.LocalPath = Path; + + { + BuildsSelection::Build RemoteBuildState = {.Id = BuildId, + .IncludeWildcards = Options.IncludeWildcards, + .ExcludeWildcards = Options.ExcludeWildcards}; + RemoteBuildState.Parts.reserve(BuildPartIds.size()); + for (size_t PartIndex = 0; PartIndex < BuildPartIds.size(); PartIndex++) + { + RemoteBuildState.Parts.push_back( + {BuildsSelection::BuildPart{.Id = BuildPartIds[PartIndex], + .Name = PartIndex < BuildPartNames.size() ? BuildPartNames[PartIndex] : ""}}); + } + + if (Options.AppendNewContent) + { + LocalState.State.Selection.Builds.emplace_back(std::move(RemoteBuildState)); + } + else + { + LocalState.State.Selection.Builds = std::vector<BuildsSelection::Build>{std::move(RemoteBuildState)}; + } + } + + if ((Options.EnableTargetFolderScavenging || Options.AppendNewContent) && !Options.CleanTargetFolder && + CompareChunkedContent(RemoteContent, LocalState.State.ChunkedContent)) { if (!IsQuiet) { @@ -1860,7 +1863,8 @@ namespace { } Stopwatch WriteStateTimer; - CbObject StateObject = CreateStateObject(BuildId, AllBuildParts, PartContents, LocalFolderContent, Path); + + CbObject StateObject = CreateBuildSaveStateObject(LocalState); CreateDirectories(ZenStateFilePath(Options.ZenFolderPath).parent_path()); TemporaryFile::SafeWriteFile(ZenStateFilePath(Options.ZenFolderPath), StateObject.GetView()); if (!IsQuiet) @@ -1868,7 +1872,11 @@ namespace { ZEN_CONSOLE("Wrote local state in {}", NiceTimeSpanMs(WriteStateTimer.GetElapsedTimeMs())); } - AddDownloadedPath(Options.SystemRootDir, BuildId, AllBuildParts, ZenStateFilePath(Options.ZenFolderPath), Path); + AddDownloadedPath(Options.SystemRootDir, + BuildsDownloadInfo{.Selection = LocalState.State.Selection, + .LocalPath = Path, + .StateFilePath = ZenStateFilePath(Options.ZenFolderPath), + .Iso8601Date = DateTime::Now().ToIso8601()}); } else { @@ -1884,9 +1892,8 @@ namespace { { ZEN_CONSOLE("Downloading build {}, parts:{} to '{}' ({})", BuildId, BuildPartString.ToView(), Path, NiceBytes(RawSize)); } - FolderContent LocalFolderState; - const ChunkedContentLookup LocalLookup = BuildChunkedContentLookup(LocalContent); + const ChunkedContentLookup LocalLookup = BuildChunkedContentLookup(LocalState.State.ChunkedContent); const ChunkedContentLookup RemoteLookup = BuildChunkedContentLookup(RemoteContent); ProgressBar::SetLogOperationProgress(ProgressMode, TaskSteps::Download, TaskSteps::StepCount); @@ -1901,34 +1908,39 @@ namespace { Workers.GetNetworkPool(), BuildId, Path, - LocalContent, + LocalState.State.ChunkedContent, LocalLookup, RemoteContent, RemoteLookup, BlockDescriptions, LooseChunkHashes, - BuildsOperationUpdateFolder::Options{.IsQuiet = IsQuiet, - .IsVerbose = IsVerbose, - .AllowFileClone = Options.AllowFileClone, - .UseSparseFiles = UseSparseFiles, - .SystemRootDir = Options.SystemRootDir, - .ZenFolderPath = Options.ZenFolderPath, - .LargeAttachmentSize = LargeAttachmentSize, - .PreferredMultipartChunkSize = PreferredMultipartChunkSize, - .PartialBlockRequestMode = Options.PartialBlockRequestMode, - .WipeTargetFolder = Options.CleanTargetFolder, - .PrimeCacheOnly = Options.PrimeCacheOnly, - .EnableOtherDownloadsScavenging = Options.EnableOtherDownloadsScavenging, - .EnableTargetFolderScavenging = Options.EnableTargetFolderScavenging, - .ValidateCompletedSequences = Options.PostDownloadVerify, - .ExcludeFolders = DefaultExcludeFolders, - .ExcludeExtensions = DefaultExcludeExtensions, - .MaximumInMemoryPayloadSize = Options.MaximumInMemoryPayloadSize, - .PopulateCache = Options.PopulateCache}); + BuildsOperationUpdateFolder::Options{ + .IsQuiet = IsQuiet, + .IsVerbose = IsVerbose, + .AllowFileClone = Options.AllowFileClone, + .UseSparseFiles = UseSparseFiles, + .SystemRootDir = Options.SystemRootDir, + .ZenFolderPath = Options.ZenFolderPath, + .LargeAttachmentSize = LargeAttachmentSize, + .PreferredMultipartChunkSize = PreferredMultipartChunkSize, + .PartialBlockRequestMode = Options.PartialBlockRequestMode, + .WipeTargetFolder = Options.CleanTargetFolder, + .PrimeCacheOnly = Options.PrimeCacheOnly, + .EnableOtherDownloadsScavenging = Options.EnableOtherDownloadsScavenging, + .EnableTargetFolderScavenging = Options.EnableTargetFolderScavenging || Options.AppendNewContent, + .ValidateCompletedSequences = Options.PostDownloadVerify, + .ExcludeFolders = DefaultExcludeFolders, + .ExcludeExtensions = DefaultExcludeExtensions, + .MaximumInMemoryPayloadSize = Options.MaximumInMemoryPayloadSize, + .PopulateCache = Options.PopulateCache}); { ProgressBar::PushLogOperation(ProgressMode, "Download"); - auto _ = MakeGuard([]() { ProgressBar::PopLogOperation(ProgressMode); }); - Updater.Execute(LocalFolderState); + auto _ = MakeGuard([]() { ProgressBar::PopLogOperation(ProgressMode); }); + FolderContent UpdatedLocalFolderState; + Updater.Execute(UpdatedLocalFolderState); + + LocalState.State.ChunkedContent = RemoteContent; + LocalState.FolderState = std::move(UpdatedLocalFolderState); } VerifyFolderStatistics VerifyFolderStats; @@ -1936,14 +1948,18 @@ namespace { { if (!Options.PrimeCacheOnly) { - AddDownloadedPath(Options.SystemRootDir, BuildId, AllBuildParts, ZenStateFilePath(Options.ZenFolderPath), Path); + AddDownloadedPath(Options.SystemRootDir, + BuildsDownloadInfo{.Selection = LocalState.State.Selection, + .LocalPath = Path, + .StateFilePath = ZenStateFilePath(Options.ZenFolderPath), + .Iso8601Date = DateTime::Now().ToIso8601()}); ProgressBar::SetLogOperationProgress(ProgressMode, TaskSteps::Verify, TaskSteps::StepCount); VerifyFolder(Workers, RemoteContent, RemoteLookup, Path, Options.PostDownloadVerify, VerifyFolderStats); Stopwatch WriteStateTimer; - CbObject StateObject = CreateStateObject(BuildId, AllBuildParts, PartContents, LocalFolderState, Path); + CbObject StateObject = CreateBuildSaveStateObject(LocalState); CreateDirectories(ZenStateFilePath(Options.ZenFolderPath).parent_path()); TemporaryFile::SafeWriteFile(ZenStateFilePath(Options.ZenFolderPath), StateObject.GetView()); @@ -2036,8 +2052,10 @@ namespace { { std::uint64_t PreferredMultipartChunkSize = 32u * 1024u * 1024u; + CbObject BuildObject = GetBuild(*Storage.BuildStorage, BuildId); + std::vector<std::pair<Oid, std::string>> AllBuildParts = - ResolveBuildPartNames(*Storage.BuildStorage, BuildId, BuildPartIds, BuildPartNames, PreferredMultipartChunkSize); + ResolveBuildPartNames(BuildObject, BuildId, BuildPartIds, BuildPartNames, PreferredMultipartChunkSize); Stopwatch GetBuildPartTimer; @@ -2452,6 +2470,20 @@ BuildsCommand::BuildsCommand() "<allowpartialblockrequests>"); }; + auto AddAppendNewContentOptions = [this](cxxopts::Options& Ops) { + Ops.add_option("", + "", + "append", + "Decides if the remote data should replace or append to the current local data.\n" + " false = the local content will be replaced by the remote content\n" + " auto = if no path wildcards are given the local content will be replaced, if wildcards are given the " + "remote data will complement the local data\n" + " true = the remote data will be overlayed on top of local data\n" + "Defaults to 'auto'.", + cxxopts::value(m_AppendNewContent), + "<append>"); + }; + m_Options.add_option("", "v", "verb", @@ -2609,6 +2641,7 @@ BuildsCommand::BuildsCommand() AddZenFolderOptions(m_DownloadOptions); AddWorkerOptions(m_DownloadOptions); AddWildcardOptions(m_DownloadOptions); + AddAppendNewContentOptions(m_DownloadOptions); m_DownloadOptions.add_option("cache", "", @@ -2730,6 +2763,9 @@ BuildsCommand::BuildsCommand() m_TestOptions.add_option("", "l", "local-path", "Root file system folder used as base", cxxopts::value(m_Path), "<local-path>"); AddMultipartOptions(m_TestOptions); AddPartialBlockRequestOptions(m_TestOptions); + AddWildcardOptions(m_TestOptions); + AddAppendNewContentOptions(m_TestOptions); + m_TestOptions.add_option("", "", "enable-scavenge", @@ -3159,7 +3195,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ForEachStrTok(Wildcard, ';', [&Wildcards](std::string_view Wildcard) { if (!Wildcard.empty()) { - std::string CleanWildcard(Wildcard); + std::string CleanWildcard(ToLower(Wildcard)); for (auto It = begin(CleanWildcard); It != end(CleanWildcard); It++) { if (*It == '\\') @@ -3663,6 +3699,15 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) return Mode; }; + auto ParseAppendNewContent = [&]() -> EAppendNewContentMode { + EAppendNewContentMode Mode = AppendNewContentModeFromString(m_AppendNewContent); + if (Mode == EAppendNewContentMode::Invalid) + { + throw OptionParseException(fmt::format("'--append' ('{}') is invalid", m_AppendNewContent), SubOption->help()); + } + return Mode; + }; + if (SubOption == &m_DownloadOptions) { if (!IsQuiet) @@ -3726,6 +3771,25 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) std::vector<std::string> BuildPartNames = ParseBuildPartNames(); EPartialBlockRequestMode PartialBlockRequestMode = ParseAllowPartialBlockRequests(); + EAppendNewContentMode AppendNewContentMode = ParseAppendNewContent(); + + bool AppendNewContent = AppendNewContentMode == EAppendNewContentMode::Append; + if (AppendNewContentMode == EAppendNewContentMode::Auto) + { + if (IncludeWildcards.empty() && ExcludeWildcards.empty()) + { + AppendNewContent = false; + } + else + { + AppendNewContent = true; + } + } + + if (AppendNewContent && m_Clean) + { + throw OptionParseException("'--append' conflicts with '--clean'", SubOption->help()); + } DownloadFolder(Workers, Storage, @@ -3746,7 +3810,8 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) .IncludeWildcards = IncludeWildcards, .ExcludeWildcards = ExcludeWildcards, .MaximumInMemoryPayloadSize = GetMaxMemoryBufferSize(m_BoostWorkerMemory), - .PopulateCache = m_UploadToZenCache}); + .PopulateCache = m_UploadToZenCache, + .AppendNewContent = AppendNewContent}); if (AbortFlag) { @@ -3858,8 +3923,10 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) std::uint64_t PreferredMultipartChunkSize = 32u * 1024u * 1024u; + CbObject BuildObject = GetBuild(*Storage.BuildStorage, BuildId); + std::vector<std::pair<Oid, std::string>> AllBuildParts = - ResolveBuildPartNames(*Storage.BuildStorage, BuildId, BuildPartIds, BuildPartNames, PreferredMultipartChunkSize); + ResolveBuildPartNames(BuildObject, BuildId, BuildPartIds, BuildPartNames, PreferredMultipartChunkSize); std::vector<Oid> AllBuildPartIds; AllBuildPartIds.reserve(AllBuildParts.size()); @@ -4160,6 +4227,10 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) const std::filesystem::path DownloadPath2 = m_Path.parent_path() / (m_BuildPartName + "_test2"); const std::filesystem::path DownloadPath3 = m_Path.parent_path() / (m_BuildPartName + "_test3"); + CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), DownloadPath); + CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), DownloadPath2); + CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), DownloadPath3); + auto ___ = MakeGuard([&Workers, DownloadPath, DownloadPath2, DownloadPath3]() { CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), DownloadPath); CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), DownloadPath2); @@ -4233,6 +4304,95 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ValidateBuildPart(Workers, *Storage.BuildStorage, BuildId, BuildPartId, m_BuildPartName); + if (!m_IncludeWildcard.empty() || !m_ExcludeWildcard.empty()) + { + auto __ = MakeGuard([&]() { CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), DownloadPath); }); + + ZEN_CONSOLE("\nDownload Filtered Build {}, Part {} ({}) to '{}'", BuildId, BuildPartId, m_BuildPartName, DownloadPath); + + std::vector<std::string> IncludeWildcards; + std::vector<std::string> ExcludeWildcards; + ParseFileFilters(IncludeWildcards, ExcludeWildcards); + + DownloadFolder(Workers, + Storage, + BuildId, + {BuildPartId}, + {}, + DownloadPath, + DownloadOptions{.SystemRootDir = m_SystemRootDir, + .ZenFolderPath = DownloadPath / ZenFolderName, + .AllowMultiparts = m_AllowMultiparts, + .PartialBlockRequestMode = PartialBlockRequestMode, + .CleanTargetFolder = true, + .PostDownloadVerify = true, + .PrimeCacheOnly = false, + .EnableOtherDownloadsScavenging = m_EnableScavenging, + .EnableTargetFolderScavenging = false, + .AllowFileClone = m_AllowFileClone, + .IncludeWildcards = IncludeWildcards, + .ExcludeWildcards = ExcludeWildcards, + .AppendNewContent = false}); + if (AbortFlag) + { + throw std::runtime_error("Test aborted. (Download build)"); + } + + ZEN_CONSOLE("\nDownload Filtered Out Remaining of Build {}, Part {} ({}) to '{}'", + BuildId, + BuildPartId, + m_BuildPartName, + DownloadPath); + DownloadFolder(Workers, + Storage, + BuildId, + {BuildPartId}, + {}, + DownloadPath, + DownloadOptions{.SystemRootDir = m_SystemRootDir, + .ZenFolderPath = DownloadPath / ZenFolderName, + .AllowMultiparts = m_AllowMultiparts, + .PartialBlockRequestMode = PartialBlockRequestMode, + .CleanTargetFolder = true, + .PostDownloadVerify = true, + .PrimeCacheOnly = false, + .EnableOtherDownloadsScavenging = m_EnableScavenging, + .EnableTargetFolderScavenging = true, + .AllowFileClone = m_AllowFileClone, + .IncludeWildcards = ExcludeWildcards, + .ExcludeWildcards = IncludeWildcards, + .AppendNewContent = true}); + if (AbortFlag) + { + throw std::runtime_error("Test aborted. (Download build)"); + } + + ZEN_CONSOLE("\nDownload Full Build {}, Part {} ({}) to '{}'", BuildId, BuildPartId, m_BuildPartName, DownloadPath); + DownloadFolder(Workers, + Storage, + BuildId, + {BuildPartId}, + {}, + DownloadPath, + DownloadOptions{.SystemRootDir = m_SystemRootDir, + .ZenFolderPath = DownloadPath / ZenFolderName, + .AllowMultiparts = m_AllowMultiparts, + .PartialBlockRequestMode = PartialBlockRequestMode, + .CleanTargetFolder = false, + .PostDownloadVerify = true, + .PrimeCacheOnly = false, + .EnableOtherDownloadsScavenging = m_EnableScavenging, + .EnableTargetFolderScavenging = true, + .AllowFileClone = m_AllowFileClone, + .IncludeWildcards = {}, + .ExcludeWildcards = {}, + .AppendNewContent = false}); + if (AbortFlag) + { + throw std::runtime_error("Test aborted. (Download build)"); + } + } + ZEN_CONSOLE("\nDownload Build {}, Part {} ({}) to '{}'", BuildId, BuildPartId, m_BuildPartName, DownloadPath); DownloadFolder(Workers, Storage, diff --git a/src/zen/cmds/builds_cmd.h b/src/zen/cmds/builds_cmd.h index ec752e581..db8826cba 100644 --- a/src/zen/cmds/builds_cmd.h +++ b/src/zen/cmds/builds_cmd.h @@ -67,6 +67,7 @@ private: std::string m_BuildPartId; // Defaults to a generated id when creating part, looked up when downloading using m_BuildPartName bool m_Clean = false; bool m_Force = false; + std::string m_AppendNewContent = "auto"; uint8_t m_BlockReuseMinPercentLimit = 85; bool m_AllowMultiparts = true; std::string m_AllowPartialBlockRequests = "mixed"; diff --git a/src/zenremotestore/builds/buildcontent.cpp b/src/zenremotestore/builds/buildcontent.cpp new file mode 100644 index 000000000..e842550b4 --- /dev/null +++ b/src/zenremotestore/builds/buildcontent.cpp @@ -0,0 +1,253 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zenremotestore/builds/buildcontent.h> + +#include <zencore/compactbinaryutil.h> +#include <zencore/fmtutils.h> + +ZEN_THIRD_PARTY_INCLUDES_START +#include <tsl/robin_set.h> +ZEN_THIRD_PARTY_INCLUDES_END + +namespace zen { + +using namespace std::literals; + +void +WriteBuildContentToCompactBinary(CbObjectWriter& PartManifestWriter, + const SourcePlatform Platform, + std::span<const std::filesystem::path> Paths, + std::span<const IoHash> RawHashes, + std::span<const uint64_t> RawSizes, + std::span<const uint32_t> Attributes, + std::span<const IoHash> SequenceRawHashes, + std::span<const uint32_t> ChunkCounts, + std::span<const IoHash> LocalChunkHashes, + std::span<const uint64_t> LocalChunkRawSizes, + const std::vector<uint32_t>& AbsoluteChunkOrders, + const std::span<const uint32_t> LooseLocalChunkIndexes, + const std::span<IoHash> BlockHashes) +{ + ZEN_ASSERT(Platform != SourcePlatform::_Count); + PartManifestWriter.AddString("platform"sv, ToString(Platform)); + + uint64_t TotalSize = 0; + for (const uint64_t Size : RawSizes) + { + TotalSize += Size; + } + PartManifestWriter.AddInteger("totalSize", TotalSize); + + PartManifestWriter.BeginObject("files"sv); + { + compactbinary_helpers::WriteArray(Paths, "paths"sv, PartManifestWriter); + compactbinary_helpers::WriteArray(RawHashes, "rawhashes"sv, PartManifestWriter); + compactbinary_helpers::WriteArray(RawSizes, "rawsizes"sv, PartManifestWriter); + if (Platform == SourcePlatform::Windows) + { + compactbinary_helpers::WriteArray(Attributes, "attributes"sv, PartManifestWriter); + } + if (Platform == SourcePlatform::Linux || Platform == SourcePlatform::MacOS) + { + compactbinary_helpers::WriteArray(Attributes, "mode"sv, PartManifestWriter); + } + } + PartManifestWriter.EndObject(); // files + + PartManifestWriter.BeginObject("chunkedContent"); + { + compactbinary_helpers::WriteArray(SequenceRawHashes, "sequenceRawHashes"sv, PartManifestWriter); + compactbinary_helpers::WriteArray(ChunkCounts, "chunkcounts"sv, PartManifestWriter); + compactbinary_helpers::WriteArray(AbsoluteChunkOrders, "chunkorders"sv, PartManifestWriter); + } + PartManifestWriter.EndObject(); // chunkedContent + + size_t LooseChunkCount = LooseLocalChunkIndexes.size(); + if (LooseChunkCount > 0) + { + PartManifestWriter.BeginObject("chunkAttachments"); + { + PartManifestWriter.BeginArray("rawHashes"sv); + for (uint32_t ChunkIndex : LooseLocalChunkIndexes) + { + PartManifestWriter.AddBinaryAttachment(LocalChunkHashes[ChunkIndex]); + } + PartManifestWriter.EndArray(); // rawHashes + + PartManifestWriter.BeginArray("chunkRawSizes"sv); + for (uint32_t ChunkIndex : LooseLocalChunkIndexes) + { + PartManifestWriter.AddInteger(LocalChunkRawSizes[ChunkIndex]); + } + PartManifestWriter.EndArray(); // chunkSizes + } + PartManifestWriter.EndObject(); // + } + + if (BlockHashes.size() > 0) + { + PartManifestWriter.BeginObject("blockAttachments"); + { + compactbinary_helpers::WriteBinaryAttachmentArray(BlockHashes, "rawHashes"sv, PartManifestWriter); + } + PartManifestWriter.EndObject(); // blocks + } +} + +void +ReadBuildContentFromCompactBinary(CbObjectView BuildPartManifest, + SourcePlatform& OutPlatform, + std::vector<std::filesystem::path>& OutPaths, + std::vector<IoHash>& OutRawHashes, + std::vector<uint64_t>& OutRawSizes, + std::vector<uint32_t>& OutAttributes, + std::vector<IoHash>& OutSequenceRawHashes, + std::vector<uint32_t>& OutChunkCounts, + std::vector<uint32_t>& OutAbsoluteChunkOrders, + std::vector<IoHash>& OutLooseChunkHashes, + std::vector<uint64_t>& OutLooseChunkRawSizes, + std::vector<IoHash>& OutBlockRawHashes) +{ + OutPlatform = FromString(BuildPartManifest["platform"sv].AsString(), SourcePlatform::_Count); + + CbObjectView FilesObject = BuildPartManifest["files"sv].AsObjectView(); + + OutPaths = compactbinary_helpers::ReadArray<std::filesystem::path>("paths"sv, FilesObject); + OutRawHashes = compactbinary_helpers::ReadArray<IoHash>("rawhashes"sv, FilesObject); + OutRawSizes = compactbinary_helpers::ReadArray<uint64_t>("rawsizes"sv, FilesObject); + + uint64_t PathCount = OutPaths.size(); + if (OutRawHashes.size() != PathCount) + { + throw std::runtime_error(fmt::format("Number of raw hashes entries does not match number of paths")); + } + if (OutRawSizes.size() != PathCount) + { + throw std::runtime_error(fmt::format("Number of raw sizes entries does not match number of paths")); + } + + std::vector<uint32_t> ModeArray = compactbinary_helpers::ReadArray<uint32_t>("mode"sv, FilesObject); + if (ModeArray.size() != PathCount && ModeArray.size() != 0) + { + throw std::runtime_error(fmt::format("Number of attribute entries does not match number of paths")); + } + + std::vector<uint32_t> AttributeArray = compactbinary_helpers::ReadArray<uint32_t>("attributes"sv, FilesObject); + if (AttributeArray.size() != PathCount && AttributeArray.size() != 0) + { + throw std::runtime_error(fmt::format("Number of attribute entries does not match number of paths")); + } + + if (ModeArray.size() > 0) + { + if (OutPlatform == SourcePlatform::_Count) + { + OutPlatform = SourcePlatform::Linux; // Best guess - under dev format + } + OutAttributes = std::move(ModeArray); + } + else if (AttributeArray.size() > 0) + { + if (OutPlatform == SourcePlatform::_Count) + { + OutPlatform = SourcePlatform::Windows; + } + OutAttributes = std::move(AttributeArray); + } + else + { + if (OutPlatform == SourcePlatform::_Count) + { + OutPlatform = GetSourceCurrentPlatform(); + } + } + + if (CbObjectView ChunkContentView = BuildPartManifest["chunkedContent"sv].AsObjectView(); ChunkContentView) + { + OutSequenceRawHashes = compactbinary_helpers::ReadArray<IoHash>("sequenceRawHashes"sv, ChunkContentView); + OutChunkCounts = compactbinary_helpers::ReadArray<uint32_t>("chunkcounts"sv, ChunkContentView); + if (OutChunkCounts.size() != OutSequenceRawHashes.size()) + { + throw std::runtime_error(fmt::format("Number of chunk count entries does not match number of paths")); + } + OutAbsoluteChunkOrders = compactbinary_helpers::ReadArray<uint32_t>("chunkorders"sv, ChunkContentView); + } + else if (FilesObject["chunkcounts"sv]) + { + // Legacy zen style + + std::vector<uint32_t> LegacyChunkCounts = compactbinary_helpers::ReadArray<uint32_t>("chunkcounts"sv, FilesObject); + if (LegacyChunkCounts.size() != PathCount) + { + throw std::runtime_error(fmt::format("Number of chunk count entries does not match number of paths")); + } + std::vector<uint32_t> LegacyAbsoluteChunkOrders = compactbinary_helpers::ReadArray<uint32_t>("chunkorders"sv, FilesObject); + + CbArrayView ChunkOrdersArray = BuildPartManifest["chunkorders"sv].AsArrayView(); + const uint64_t ChunkOrdersCount = ChunkOrdersArray.Num(); + + tsl::robin_set<IoHash, IoHash::Hasher> FoundRawHashes; + FoundRawHashes.reserve(PathCount); + + OutChunkCounts.reserve(PathCount); + OutAbsoluteChunkOrders.reserve(ChunkOrdersCount); + + uint32_t OrderIndexOffset = 0; + for (uint32_t PathIndex = 0; PathIndex < OutPaths.size(); PathIndex++) + { + const IoHash& PathRawHash = OutRawHashes[PathIndex]; + uint32_t LegacyChunkCount = LegacyChunkCounts[PathIndex]; + + if (FoundRawHashes.insert(PathRawHash).second) + { + OutSequenceRawHashes.push_back(PathRawHash); + OutChunkCounts.push_back(LegacyChunkCount); + std::span<uint32_t> AbsoluteChunkOrder = + std::span<uint32_t>(LegacyAbsoluteChunkOrders).subspan(OrderIndexOffset, LegacyChunkCount); + OutAbsoluteChunkOrders.insert(OutAbsoluteChunkOrders.end(), AbsoluteChunkOrder.begin(), AbsoluteChunkOrder.end()); + } + OrderIndexOffset += LegacyChunkCounts[PathIndex]; + } + } + else + { + // Legacy C# style + + tsl::robin_set<IoHash, IoHash::Hasher> FoundRawHashes; + FoundRawHashes.reserve(PathCount); + uint32_t OrderIndexOffset = 0; + for (uint32_t PathIndex = 0; PathIndex < OutPaths.size(); PathIndex++) + { + if (OutRawSizes[PathIndex] > 0) + { + const IoHash& PathRawHash = OutRawHashes[PathIndex]; + if (FoundRawHashes.insert(PathRawHash).second) + { + OutSequenceRawHashes.push_back(PathRawHash); + OutChunkCounts.push_back(1); + OutAbsoluteChunkOrders.push_back(OrderIndexOffset); + OutLooseChunkHashes.push_back(PathRawHash); + OutLooseChunkRawSizes.push_back(OutRawSizes[PathIndex]); + OrderIndexOffset += 1; + } + } + } + } + + CbObjectView ChunkAttachmentsView = BuildPartManifest["chunkAttachments"sv].AsObjectView(); + { + OutLooseChunkHashes = compactbinary_helpers::ReadBinaryAttachmentArray("rawHashes"sv, ChunkAttachmentsView); + OutLooseChunkRawSizes = compactbinary_helpers::ReadArray<uint64_t>("chunkRawSizes"sv, ChunkAttachmentsView); + if (OutLooseChunkHashes.size() != OutLooseChunkRawSizes.size()) + { + throw std::runtime_error(fmt::format("Number of attachment chunk hashes does not match number of attachemnt chunk raw sizes")); + } + } + + CbObjectView BlocksView = BuildPartManifest["blockAttachments"sv].AsObjectView(); + { + OutBlockRawHashes = compactbinary_helpers::ReadBinaryAttachmentArray("rawHashes"sv, BlocksView); + } +} + +} // namespace zen diff --git a/src/zenremotestore/builds/buildsavedstate.cpp b/src/zenremotestore/builds/buildsavedstate.cpp index cf46668f9..5a86ee865 100644 --- a/src/zenremotestore/builds/buildsavedstate.cpp +++ b/src/zenremotestore/builds/buildsavedstate.cpp @@ -13,7 +13,10 @@ ZEN_THIRD_PARTY_INCLUDES_START #include <tsl/robin_set.h> ZEN_THIRD_PARTY_INCLUDES_END -#define EXTRA_VERIFY 0 +#if ZEN_WITH_TESTS +# include <zencore/testing.h> +# include <zencore/testutils.h> +#endif // ZEN_WITH_TESTS namespace zen { @@ -26,299 +29,181 @@ namespace { } } // namespace -CbObject -CreateStateObject(const Oid& BuildId, - const std::vector<std::pair<Oid, std::string>>& AllBuildParts, - std::span<const ChunkedFolderContent> PartContents, - const FolderContent& LocalFolderState, - const std::filesystem::path& LocalPath) +///////////////////////// BuildsSelection + +void +BuildsSelection::Write(const BuildsSelection& Selection, CbWriter& Output) { - CbObjectWriter CurrentStateWriter; - CurrentStateWriter.AddString("path", (const char*)LocalPath.u8string().c_str()); - CurrentStateWriter.BeginArray("builds"sv); + Output.BeginArray("builds"sv); + for (const BuildsSelection::Build& Build : Selection.Builds) { - CurrentStateWriter.BeginObject(); + Output.BeginObject(); { - CurrentStateWriter.AddObjectId("buildId"sv, BuildId); - CurrentStateWriter.BeginArray("parts"sv); - for (size_t PartIndex = 0; PartIndex < AllBuildParts.size(); PartIndex++) + ZEN_ASSERT(Build.Id != Oid::Zero); + Output.AddObjectId("buildId"sv, Build.Id); + compactbinary_helpers::WriteArray(Build.IncludeWildcards, "includeWildcards"sv, Output); + compactbinary_helpers::WriteArray(Build.ExcludeWildcards, "excludeWildcards"sv, Output); + + Output.BeginArray("parts"sv); + for (const BuildsSelection::BuildPart& BuildPart : Build.Parts) { - const Oid BuildPartId = AllBuildParts[PartIndex].first; - CurrentStateWriter.BeginObject(); + Output.BeginObject(); { - CurrentStateWriter.AddObjectId("partId"sv, BuildPartId); - CurrentStateWriter.AddString("partName"sv, AllBuildParts[PartIndex].second); - CurrentStateWriter.BeginObject("content"); - { - SaveChunkedFolderContentToCompactBinary(PartContents[PartIndex], CurrentStateWriter); - } - CurrentStateWriter.EndObject(); + ZEN_ASSERT(BuildPart.Id != Oid::Zero); + Output.AddObjectId("partId"sv, BuildPart.Id); + Output.AddString("partName"sv, BuildPart.Name); } - CurrentStateWriter.EndObject(); + Output.EndObject(); } - CurrentStateWriter.EndArray(); // parts + Output.EndArray(); // parts } - CurrentStateWriter.EndObject(); + Output.EndObject(); } - CurrentStateWriter.EndArray(); // builds - - CurrentStateWriter.BeginObject("localFolderState"sv); - { - SaveFolderContentToCompactBinary(LocalFolderState, CurrentStateWriter); - } - CurrentStateWriter.EndObject(); // localFolderState - - return CurrentStateWriter.Save(); + Output.EndArray(); // builds } -void -ReadBuildContentFromCompactBinary(CbObjectView BuildPartManifest, - SourcePlatform& OutPlatform, - std::vector<std::filesystem::path>& OutPaths, - std::vector<IoHash>& OutRawHashes, - std::vector<uint64_t>& OutRawSizes, - std::vector<uint32_t>& OutAttributes, - std::vector<IoHash>& OutSequenceRawHashes, - std::vector<uint32_t>& OutChunkCounts, - std::vector<uint32_t>& OutAbsoluteChunkOrders, - std::vector<IoHash>& OutLooseChunkHashes, - std::vector<uint64_t>& OutLooseChunkRawSizes, - std::vector<IoHash>& OutBlockRawHashes) +BuildsSelection +BuildsSelection::Read(CbObjectView& Input, std::vector<ChunkedFolderContent>* OptionalOutLegacyPartsContent) { - OutPlatform = FromString(BuildPartManifest["platform"sv].AsString(), SourcePlatform::_Count); - - CbObjectView FilesObject = BuildPartManifest["files"sv].AsObjectView(); - - OutPaths = compactbinary_helpers::ReadArray<std::filesystem::path>("paths"sv, FilesObject); - OutRawHashes = compactbinary_helpers::ReadArray<IoHash>("rawhashes"sv, FilesObject); - OutRawSizes = compactbinary_helpers::ReadArray<uint64_t>("rawsizes"sv, FilesObject); - - uint64_t PathCount = OutPaths.size(); - if (OutRawHashes.size() != PathCount) - { - throw std::runtime_error(fmt::format("Number of raw hashes entries does not match number of paths")); - } - if (OutRawSizes.size() != PathCount) - { - throw std::runtime_error(fmt::format("Number of raw sizes entries does not match number of paths")); - } + std::vector<BuildsSelection::Build> Builds; - std::vector<uint32_t> ModeArray = compactbinary_helpers::ReadArray<uint32_t>("mode"sv, FilesObject); - if (ModeArray.size() != PathCount && ModeArray.size() != 0) + CbArrayView BuildsArray = Input["builds"sv].AsArrayView(); + Builds.reserve(BuildsArray.Num()); + for (CbFieldView BuildView : BuildsArray) { - throw std::runtime_error(fmt::format("Number of attribute entries does not match number of paths")); - } - - std::vector<uint32_t> AttributeArray = compactbinary_helpers::ReadArray<uint32_t>("attributes"sv, FilesObject); - if (AttributeArray.size() != PathCount && AttributeArray.size() != 0) - { - throw std::runtime_error(fmt::format("Number of attribute entries does not match number of paths")); - } - - if (ModeArray.size() > 0) - { - if (OutPlatform == SourcePlatform::_Count) - { - OutPlatform = SourcePlatform::Linux; // Best guess - under dev format - } - OutAttributes = std::move(ModeArray); - } - else if (AttributeArray.size() > 0) - { - if (OutPlatform == SourcePlatform::_Count) + std::vector<BuildsSelection::BuildPart> Parts; + CbObjectView BuildObjectView = BuildView.AsObjectView(); + Oid BuildId = BuildObjectView["buildId"sv].AsObjectId(); + if (BuildId == Oid::Zero) { - OutPlatform = SourcePlatform::Windows; + throw std::runtime_error(fmt::format("BuildsSelection build id is invalid '{}'", BuildId)); } - OutAttributes = std::move(AttributeArray); - } - else - { - if (OutPlatform == SourcePlatform::_Count) - { - OutPlatform = GetSourceCurrentPlatform(); - } - } - if (CbObjectView ChunkContentView = BuildPartManifest["chunkedContent"sv].AsObjectView(); ChunkContentView) - { - OutSequenceRawHashes = compactbinary_helpers::ReadArray<IoHash>("sequenceRawHashes"sv, ChunkContentView); - OutChunkCounts = compactbinary_helpers::ReadArray<uint32_t>("chunkcounts"sv, ChunkContentView); - if (OutChunkCounts.size() != OutSequenceRawHashes.size()) - { - throw std::runtime_error(fmt::format("Number of chunk count entries does not match number of paths")); - } - OutAbsoluteChunkOrders = compactbinary_helpers::ReadArray<uint32_t>("chunkorders"sv, ChunkContentView); - } - else if (FilesObject["chunkcounts"sv]) - { - // Legacy zen style - - std::vector<uint32_t> LegacyChunkCounts = compactbinary_helpers::ReadArray<uint32_t>("chunkcounts"sv, FilesObject); - if (LegacyChunkCounts.size() != PathCount) - { - throw std::runtime_error(fmt::format("Number of chunk count entries does not match number of paths")); - } - std::vector<uint32_t> LegacyAbsoluteChunkOrders = compactbinary_helpers::ReadArray<uint32_t>("chunkorders"sv, FilesObject); - - CbArrayView ChunkOrdersArray = BuildPartManifest["chunkorders"sv].AsArrayView(); - const uint64_t ChunkOrdersCount = ChunkOrdersArray.Num(); - - tsl::robin_set<IoHash, IoHash::Hasher> FoundRawHashes; - FoundRawHashes.reserve(PathCount); - - OutChunkCounts.reserve(PathCount); - OutAbsoluteChunkOrders.reserve(ChunkOrdersCount); + std::vector<std::string> IncludeWildcards = compactbinary_helpers::ReadArray<std::string>("includeWildcards"sv, BuildObjectView); + std::vector<std::string> ExcludeWildcards = compactbinary_helpers::ReadArray<std::string>("excludeWildcards"sv, BuildObjectView); - uint32_t OrderIndexOffset = 0; - for (uint32_t PathIndex = 0; PathIndex < OutPaths.size(); PathIndex++) + CbArrayView BuildPartsArray = BuildObjectView["parts"sv].AsArrayView(); + Parts.reserve(BuildPartsArray.Num()); + for (CbFieldView PartView : BuildPartsArray) { - const IoHash& PathRawHash = OutRawHashes[PathIndex]; - uint32_t LegacyChunkCount = LegacyChunkCounts[PathIndex]; - - if (FoundRawHashes.insert(PathRawHash).second) + CbObjectView PartObjectView = PartView.AsObjectView(); + Oid BuildPartId = PartObjectView["partId"sv].AsObjectId(); + if (BuildPartId == Oid::Zero) { - OutSequenceRawHashes.push_back(PathRawHash); - OutChunkCounts.push_back(LegacyChunkCount); - std::span<uint32_t> AbsoluteChunkOrder = - std::span<uint32_t>(LegacyAbsoluteChunkOrders).subspan(OrderIndexOffset, LegacyChunkCount); - OutAbsoluteChunkOrders.insert(OutAbsoluteChunkOrders.end(), AbsoluteChunkOrder.begin(), AbsoluteChunkOrder.end()); + throw std::runtime_error(fmt::format("BuildsSelection build part id in build '{}' is invalid '{}'", BuildId, BuildPartId)); } - OrderIndexOffset += LegacyChunkCounts[PathIndex]; - } - } - else - { - // Legacy C# style + std::string_view BuildPartName = PartObjectView["partName"sv].AsString(); - tsl::robin_set<IoHash, IoHash::Hasher> FoundRawHashes; - FoundRawHashes.reserve(PathCount); - uint32_t OrderIndexOffset = 0; - for (uint32_t PathIndex = 0; PathIndex < OutPaths.size(); PathIndex++) - { - if (OutRawSizes[PathIndex] > 0) + if (OptionalOutLegacyPartsContent != nullptr) { - const IoHash& PathRawHash = OutRawHashes[PathIndex]; - if (FoundRawHashes.insert(PathRawHash).second) + CbObjectView PartContentObjectView = PartObjectView["content"sv].AsObjectView(); + if (PartContentObjectView) { - OutSequenceRawHashes.push_back(PathRawHash); - OutChunkCounts.push_back(1); - OutAbsoluteChunkOrders.push_back(OrderIndexOffset); - OutLooseChunkHashes.push_back(PathRawHash); - OutLooseChunkRawSizes.push_back(OutRawSizes[PathIndex]); - OrderIndexOffset += 1; + OptionalOutLegacyPartsContent->push_back(LoadChunkedFolderContentFromCompactBinary(PartContentObjectView)); } } + Parts.push_back(BuildsSelection::BuildPart{.Id = BuildPartId, .Name = std::string(BuildPartName)}); } - } - CbObjectView ChunkAttachmentsView = BuildPartManifest["chunkAttachments"sv].AsObjectView(); - { - OutLooseChunkHashes = compactbinary_helpers::ReadBinaryAttachmentArray("rawHashes"sv, ChunkAttachmentsView); - OutLooseChunkRawSizes = compactbinary_helpers::ReadArray<uint64_t>("chunkRawSizes"sv, ChunkAttachmentsView); - if (OutLooseChunkHashes.size() != OutLooseChunkRawSizes.size()) - { - throw std::runtime_error(fmt::format("Number of attachment chunk hashes does not match number of attachemnt chunk raw sizes")); - } - } - - CbObjectView BlocksView = BuildPartManifest["blockAttachments"sv].AsObjectView(); - { - OutBlockRawHashes = compactbinary_helpers::ReadBinaryAttachmentArray("rawHashes"sv, BlocksView); + Builds.push_back(BuildsSelection::Build{.Id = BuildId, + .Parts = std::move(Parts), + .IncludeWildcards = std::move(IncludeWildcards), + .ExcludeWildcards = std::move(ExcludeWildcards)}); } + return BuildsSelection{.Builds = std::move(Builds)}; } +///////////////////////// BuildState + void -CalculateLocalChunkOrders(const std::span<const uint32_t>& AbsoluteChunkOrders, - const std::span<const IoHash> LooseChunkHashes, - const std::span<const uint64_t> LooseChunkRawSizes, - const std::span<const ChunkBlockDescription>& BlockDescriptions, - std::vector<IoHash>& OutLocalChunkHashes, - std::vector<uint64_t>& OutLocalChunkRawSizes, - std::vector<uint32_t>& OutLocalChunkOrders) +BuildState::Write(const BuildState& State, CbWriter& Output) { - ZEN_TRACE_CPU("CalculateLocalChunkOrders"); + BuildsSelection::Write(State.Selection, Output); - std::vector<IoHash> AbsoluteChunkHashes; - std::vector<uint64_t> AbsoluteChunkRawSizes; - AbsoluteChunkHashes.insert(AbsoluteChunkHashes.end(), LooseChunkHashes.begin(), LooseChunkHashes.end()); - AbsoluteChunkRawSizes.insert(AbsoluteChunkRawSizes.end(), LooseChunkRawSizes.begin(), LooseChunkRawSizes.end()); - for (const ChunkBlockDescription& Block : BlockDescriptions) + Output.BeginObject("content"); { - AbsoluteChunkHashes.insert(AbsoluteChunkHashes.end(), Block.ChunkRawHashes.begin(), Block.ChunkRawHashes.end()); - AbsoluteChunkRawSizes.insert(AbsoluteChunkRawSizes.end(), Block.ChunkRawLengths.begin(), Block.ChunkRawLengths.end()); + SaveChunkedFolderContentToCompactBinary(State.ChunkedContent, Output); } - OutLocalChunkHashes.reserve(AbsoluteChunkHashes.size()); - OutLocalChunkRawSizes.reserve(AbsoluteChunkRawSizes.size()); - OutLocalChunkOrders.reserve(AbsoluteChunkOrders.size()); + Output.EndObject(); // content +} - tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> ChunkHashToChunkIndex; - ChunkHashToChunkIndex.reserve(AbsoluteChunkHashes.size()); +BuildState +BuildState::Read(CbObjectView& Input) +{ + std::vector<ChunkedFolderContent> LegacyPartsContent; + BuildsSelection Selection = BuildsSelection::Read(Input, &LegacyPartsContent); - for (uint32_t AbsoluteChunkOrderIndex = 0; AbsoluteChunkOrderIndex < AbsoluteChunkOrders.size(); AbsoluteChunkOrderIndex++) + ChunkedFolderContent ChunkedContent; + CbObjectView ContentObjectView = Input["content"sv].AsObjectView(); + if (ContentObjectView) { - const uint32_t AbsoluteChunkIndex = AbsoluteChunkOrders[AbsoluteChunkOrderIndex]; - const IoHash& AbsoluteChunkHash = AbsoluteChunkHashes[AbsoluteChunkIndex]; - const uint64_t AbsoluteChunkRawSize = AbsoluteChunkRawSizes[AbsoluteChunkIndex]; - - if (auto It = ChunkHashToChunkIndex.find(AbsoluteChunkHash); It != ChunkHashToChunkIndex.end()) + ZEN_ASSERT(LegacyPartsContent.empty()); + ChunkedContent = LoadChunkedFolderContentFromCompactBinary(ContentObjectView); + } + else + { + if (LegacyPartsContent.size() == 1) { - const uint32_t LocalChunkIndex = It->second; - OutLocalChunkOrders.push_back(LocalChunkIndex); + ChunkedContent = std::move(LegacyPartsContent.front()); } else { - uint32_t LocalChunkIndex = gsl::narrow<uint32_t>(OutLocalChunkHashes.size()); - OutLocalChunkHashes.push_back(AbsoluteChunkHash); - OutLocalChunkRawSizes.push_back(AbsoluteChunkRawSize); - OutLocalChunkOrders.push_back(LocalChunkIndex); - ChunkHashToChunkIndex.insert_or_assign(AbsoluteChunkHash, LocalChunkIndex); - } -#if EXTRA_VERIFY - const uint32_t LocalChunkIndex = OutLocalChunkOrders[AbsoluteChunkOrderIndex]; - const IoHash& LocalChunkHash = OutLocalChunkHashes[LocalChunkIndex]; - const uint64_t& LocalChunkRawSize = OutLocalChunkRawSizes[LocalChunkIndex]; - ZEN_ASSERT(LocalChunkHash == AbsoluteChunkHash); - ZEN_ASSERT(LocalChunkRawSize == AbsoluteChunkRawSize); -#endif // EXTRA_VERIFY + ChunkedContent = + MergeChunkedFolderContents(LegacyPartsContent[0], std::span<const ChunkedFolderContent>(LegacyPartsContent).subspan(1)); + } } -#if EXTRA_VERIFY - for (uint32_t OrderIndex = 0; OrderIndex < OutLocalChunkOrders.size(); OrderIndex++) - { - uint32_t LocalChunkIndex = OutLocalChunkOrders[OrderIndex]; - const IoHash LocalChunkHash = OutLocalChunkHashes[LocalChunkIndex]; - uint64_t LocalChunkRawSize = OutLocalChunkRawSizes[LocalChunkIndex]; - uint32_t VerifyChunkIndex = AbsoluteChunkOrders[OrderIndex]; - const IoHash VerifyChunkHash = AbsoluteChunkHashes[VerifyChunkIndex]; - uint64_t VerifyChunkRawSize = AbsoluteChunkRawSizes[VerifyChunkIndex]; + return BuildState{.Selection = std::move(Selection), .ChunkedContent = std::move(ChunkedContent)}; +} + +///////////////////////// BuildSaveState - ZEN_ASSERT(LocalChunkHash == VerifyChunkHash); - ZEN_ASSERT(LocalChunkRawSize == VerifyChunkRawSize); +void +BuildSaveState::Write(const BuildSaveState& SaveState, CbWriter& Output) +{ + ZEN_ASSERT(!SaveState.LocalPath.empty()); + + Output.AddString("path", (const char*)SaveState.LocalPath.u8string().c_str()); + BuildsSelection::Write(SaveState.State.Selection, Output); + + Output.BeginObject("content"); + { + SaveChunkedFolderContentToCompactBinary(SaveState.State.ChunkedContent, Output); } -#endif // EXTRA_VERIFY + Output.EndObject(); // content + + Output.BeginObject("localFolderState"sv); + { + SaveFolderContentToCompactBinary(SaveState.FolderState, Output); + } + Output.EndObject(); // localFolderState } -void -ReadStateObject(CbObjectView StateView, - Oid& OutBuildId, - std::vector<Oid>& BuildPartsIds, - std::vector<std::string>& BuildPartsNames, - std::vector<ChunkedFolderContent>& OutPartContents, - FolderContent& OutLocalFolderState) +BuildSaveState +BuildSaveState::Read(CbObjectView& Input) { - CbObjectView BuildView = StateView["builds"sv].AsArrayView().CreateViewIterator().AsObjectView(); - OutBuildId = BuildView["buildId"sv].AsObjectId(); - for (CbFieldView PartView : BuildView["parts"sv].AsArrayView()) + BuildState State = BuildState::Read(Input); + CbObjectView LocalFolderStateObject = Input["localFolderState"sv].AsObjectView(); + FolderContent FolderState = LoadFolderContentToCompactBinary(LocalFolderStateObject); + std::filesystem::path LocalPath = Input["path"sv].AsU8String(); + if (LocalPath.empty()) { - CbObjectView PartObjectView = PartView.AsObjectView(); - BuildPartsIds.push_back(PartObjectView["partId"sv].AsObjectId()); - BuildPartsNames.push_back(std::string(PartObjectView["partName"sv].AsString())); - OutPartContents.push_back(LoadChunkedFolderContentToCompactBinary(PartObjectView["content"sv].AsObjectView())); + throw std::runtime_error("BuildSaveState is invalid, 'path' field is empty"); } - OutLocalFolderState = LoadFolderContentToCompactBinary(StateView["localFolderState"sv].AsObjectView()); + + return BuildSaveState{.State = std::move(State), .FolderState = std::move(FolderState), .LocalPath = std::move(LocalPath)}; } -void -ReadStateFile(const std::filesystem::path& StateFilePath, FolderContent& OutLocalFolderState, ChunkedFolderContent& OutLocalContent) + +CbObject +CreateBuildSaveStateObject(const BuildSaveState& SaveState) +{ + CbObjectWriter CurrentStateWriter; + BuildSaveState::Write(SaveState, CurrentStateWriter); + return CurrentStateWriter.Save(); +} + +BuildSaveState +ReadBuildSaveStateFile(const std::filesystem::path& StateFilePath) { ZEN_TRACE_CPU("ReadStateFile"); @@ -329,28 +214,11 @@ ReadStateFile(const std::filesystem::path& StateFilePath, FolderContent& OutLoca { if (CurrentStateObject) { - Oid CurrentBuildId; - std::vector<Oid> SavedBuildPartIds; - std::vector<std::string> SavedBuildPartsNames; - std::vector<ChunkedFolderContent> SavedPartContents; - ReadStateObject(CurrentStateObject, - CurrentBuildId, - SavedBuildPartIds, - SavedBuildPartsNames, - SavedPartContents, - OutLocalFolderState); - if (!SavedPartContents.empty()) - { - if (SavedPartContents.size() == 1) - { - OutLocalContent = std::move(SavedPartContents[0]); - } - else - { - OutLocalContent = MergeChunkedFolderContents(SavedPartContents[0], - std::span<const ChunkedFolderContent>(SavedPartContents).subspan(1)); - } - } + return BuildSaveState::Read(CurrentStateObject); + } + else + { + throw std::runtime_error(fmt::format("Compact binary object read from '{}' is empty", StateFilePath)); } } else @@ -360,48 +228,61 @@ ReadStateFile(const std::filesystem::path& StateFilePath, FolderContent& OutLoca } } +///////////////////////// BuildsDownloadInfo + +void +BuildsDownloadInfo::Write(const BuildsDownloadInfo& Info, CbWriter& Output) +{ + ZEN_ASSERT(!Info.LocalPath.empty()); + ZEN_ASSERT(!Info.StateFilePath.empty()); + ZEN_ASSERT(!Info.Iso8601Date.empty()); + + Output.AddString("path", (const char*)Info.LocalPath.u8string().c_str()); + Output.AddString("statePath", (const char*)Info.StateFilePath.u8string().c_str()); + Output.AddString("date", Info.Iso8601Date); + + BuildsSelection::Write(Info.Selection, Output); +} + +BuildsDownloadInfo +BuildsDownloadInfo::Read(CbObjectView& Input) +{ + BuildsSelection Selection = BuildsSelection::Read(Input, /*OptionalOutLegacyPartsContent*/ nullptr); + + std::filesystem::path LocalPath = Input["path"sv].AsU8String(); + if (LocalPath.empty()) + { + throw std::runtime_error("BuildsDownloadInfo is invalid, 'path' field is empty"); + } + std::filesystem::path StateFilePath = Input["statePath"sv].AsU8String(); + if (StateFilePath.empty()) + { + throw std::runtime_error("BuildsDownloadInfo is invalid, 'statePath' field is empty"); + } + std::string_view Iso8601Date = Input["date"sv].AsString(); + + return BuildsDownloadInfo{.Selection = std::move(Selection), + .LocalPath = std::move(LocalPath), + .StateFilePath = std::move(StateFilePath), + .Iso8601Date = std::string(Iso8601Date)}; +} + void -AddDownloadedPath(const std::filesystem::path& SystemRootDir, - const Oid& BuildId, - const std::vector<std::pair<Oid, std::string>>& BuildParts, - const std::filesystem::path& StateFilePath, - const std::filesystem::path& LocalPath) +AddDownloadedPath(const std::filesystem::path& SystemRootDir, const BuildsDownloadInfo& Info) { ZEN_TRACE_CPU("AddDownloadedPath"); ZEN_ASSERT(!SystemRootDir.empty()); - ZEN_ASSERT(!StateFilePath.empty()); - ZEN_ASSERT(!LocalPath.empty()); - const std::u8string LocalPathString = LocalPath.generic_u8string(); + ZEN_ASSERT(!Info.StateFilePath.empty()); + ZEN_ASSERT(!Info.LocalPath.empty()); + const std::u8string LocalPathString = Info.LocalPath.generic_u8string(); IoHash PathHash = IoHash::HashBuffer(LocalPathString.data(), LocalPathString.length()); const std::filesystem::path StateDownloadFolder = ZenStateDownloadFolder(SystemRootDir); CreateDirectories(StateDownloadFolder); std::filesystem::path WritePath = StateDownloadFolder / (PathHash.ToHexString() + ".json"); CbObjectWriter Writer; - Writer.AddString("path", (const char*)LocalPath.u8string().c_str()); - Writer.AddString("statePath", (const char*)StateFilePath.u8string().c_str()); - Writer.AddDateTime("date", DateTime::Now()); - Writer.BeginArray("builds"sv); - { - Writer.BeginObject(); - { - Writer.AddObjectId("buildId", BuildId); - Writer.BeginArray("parts"); - for (const auto& It : BuildParts) - { - Writer.BeginObject(); - { - Writer.AddObjectId("partId", It.first); - Writer.AddString("partName", It.second); - } - Writer.EndObject(); - } - Writer.EndArray(); // parts - } - Writer.EndObject(); - } - Writer.EndArray(); // builds + BuildsDownloadInfo::Write(Info, Writer); CbObject Payload = Writer.Save(); ExtendableStringBuilder<512> SB; @@ -410,6 +291,21 @@ AddDownloadedPath(const std::filesystem::path& SystemRootDir, TemporaryFile::SafeWriteFile(WritePath, JsonPayload); } +BuildsDownloadInfo +ReadDownloadedInfoFile(const std::filesystem::path& DownloadInfoPath) +{ + IoBuffer MetaDataJson = ReadFile(DownloadInfoPath).Flatten(); + std::string_view Json(reinterpret_cast<const char*>(MetaDataJson.GetData()), MetaDataJson.GetSize()); + std::string JsonError; + CbObject DownloadInfo = LoadCompactBinaryFromJson(Json, JsonError).AsObject(); + if (!JsonError.empty()) + { + throw std::runtime_error(fmt::format("Invalid download state file at {}. '{}'", DownloadInfoPath, JsonError)); + } + + return BuildsDownloadInfo::Read(DownloadInfo); +} + std::vector<std::filesystem::path> GetDownloadedStatePaths(const std::filesystem::path& SystemRootDir) { @@ -436,4 +332,365 @@ GetDownloadedStatePaths(const std::filesystem::path& SystemRootDir) return Result; } +#if ZEN_WITH_TESTS + +void +buildsavedstate_forcelink() +{ +} + +namespace buildsavestate_test { + bool Equal(const BuildsSelection& Lhs, const BuildsSelection& Rhs) + { + if (Lhs.Builds.size() != Rhs.Builds.size()) + { + return false; + } + for (size_t BuildIndex = 0; BuildIndex < Lhs.Builds.size(); BuildIndex++) + { + const BuildsSelection::Build LhsBuild = Lhs.Builds[BuildIndex]; + const BuildsSelection::Build RhsBuild = Rhs.Builds[BuildIndex]; + if (LhsBuild.Id != RhsBuild.Id) + { + return false; + } + if (LhsBuild.Parts.size() != RhsBuild.Parts.size()) + { + return false; + } + for (size_t PartIndex = 0; PartIndex < LhsBuild.Parts.size(); PartIndex++) + { + const BuildsSelection::BuildPart LhsPart = LhsBuild.Parts[PartIndex]; + const BuildsSelection::BuildPart RhsPart = RhsBuild.Parts[PartIndex]; + if (LhsPart.Id != RhsPart.Id) + { + return false; + } + if (LhsPart.Name != RhsPart.Name) + { + return false; + } + } + if (LhsBuild.IncludeWildcards != RhsBuild.IncludeWildcards) + { + return false; + } + if (LhsBuild.ExcludeWildcards != RhsBuild.ExcludeWildcards) + { + return false; + } + } + return true; + } + bool Equal(const ChunkedContentData& Lhs, const ChunkedContentData& Rhs) + { + if (Lhs.SequenceRawHashes != Rhs.SequenceRawHashes) + { + return false; + } + if (Lhs.ChunkCounts != Rhs.ChunkCounts) + { + return false; + } + if (Lhs.ChunkOrders != Rhs.ChunkOrders) + { + return false; + } + if (Lhs.ChunkHashes != Rhs.ChunkHashes) + { + return false; + } + if (Lhs.ChunkRawSizes != Rhs.ChunkRawSizes) + { + return false; + } + return true; + } + + bool Equal(const ChunkedFolderContent& Lhs, const ChunkedFolderContent& Rhs) + { + if (Lhs.Platform != Rhs.Platform) + { + return false; + } + if (Lhs.Paths != Rhs.Paths) + { + return false; + } + if (Lhs.RawSizes != Rhs.RawSizes) + { + return false; + } + if (Lhs.Attributes != Rhs.Attributes) + { + return false; + } + if (Lhs.RawHashes != Rhs.RawHashes) + { + return false; + } + return Equal(Lhs.ChunkedContent, Rhs.ChunkedContent); + } + + bool Equal(const BuildState& Lhs, const BuildState& Rhs) + { + if (!Equal(Lhs.Selection, Rhs.Selection)) + { + return false; + } + if (!Equal(Lhs.ChunkedContent, Rhs.ChunkedContent)) + { + return false; + } + return true; + } + + bool Equal(const FolderContent& Lhs, const FolderContent& Rhs) + { + if (Lhs.Platform != Rhs.Platform) + { + return false; + } + if (Lhs.Paths != Rhs.Paths) + { + return false; + } + if (Lhs.RawSizes != Rhs.RawSizes) + { + return false; + } + if (Lhs.Attributes != Rhs.Attributes) + { + return false; + } + if (Lhs.ModificationTicks != Rhs.ModificationTicks) + { + return false; + } + return true; + } + + bool Equal(const BuildSaveState& Lhs, const BuildSaveState& Rhs) + { + if (!Equal(Lhs.State, Rhs.State)) + { + return false; + } + if (!Equal(Lhs.FolderState, Rhs.FolderState)) + { + return false; + } + if (Lhs.LocalPath != Rhs.LocalPath) + { + return false; + } + return true; + } + + bool Equal(const BuildsDownloadInfo& Lhs, const BuildsDownloadInfo& Rhs) + { + if (!Equal(Lhs.Selection, Rhs.Selection)) + { + return false; + } + if (Lhs.LocalPath != Rhs.LocalPath) + { + return false; + } + if (Lhs.StateFilePath != Rhs.StateFilePath) + { + return false; + } + if (Lhs.Iso8601Date != Rhs.Iso8601Date) + { + return false; + } + return true; + } + + BuildsSelection MakeBuildsSelectionA() + { + return BuildsSelection{ + .Builds = std::vector<BuildsSelection::Build>{ + BuildsSelection::Build{ + .Id = Oid::NewOid(), + .Parts = std::vector<BuildsSelection::BuildPart>{BuildsSelection::BuildPart{.Id = Oid::NewOid(), .Name = "default"}}}, + BuildsSelection::Build{ + .Id = Oid::NewOid(), + .Parts = + std::vector<BuildsSelection::BuildPart>{BuildsSelection::BuildPart{.Id = Oid::NewOid(), .Name = "first_part"}, + BuildsSelection::BuildPart{.Id = Oid::NewOid(), .Name = "second_part"}}, + .IncludeWildcards = std::vector<std::string>{std::string{"*.exe"}, std::string{"*.dll"}}, + .ExcludeWildcards = std::vector<std::string>{std::string{"*.pdb"}}}, + BuildsSelection::Build{ + .Id = Oid::NewOid(), + .Parts = std::vector<BuildsSelection::BuildPart>{BuildsSelection::BuildPart{.Id = Oid::NewOid(), .Name = "default"}}, + .IncludeWildcards = std::vector<std::string>{std::string{"*.ucas"}, std::string{"*.utok"}}, + .ExcludeWildcards = std::vector<std::string>{std::string{"*.pak"}}}}}; + } + + BuildsSelection MakeBuildsSelectionB() + { + return BuildsSelection{ + .Builds = std::vector<BuildsSelection::Build>{ + BuildsSelection::Build{ + .Id = Oid::NewOid(), + .Parts = std::vector<BuildsSelection::BuildPart>{BuildsSelection::BuildPart{.Id = Oid::NewOid(), .Name = "default"}}}, + BuildsSelection::Build{ + .Id = Oid::NewOid(), + .Parts = std::vector<BuildsSelection::BuildPart>{BuildsSelection::BuildPart{.Id = Oid::NewOid(), .Name = "default"}}, + .IncludeWildcards = std::vector<std::string>{std::string{"*.exe"}, std::string{"*.dll"}}, + .ExcludeWildcards = std::vector<std::string>{std::string{"*.pdb"}, std::string{"*.xSYM"}}}}}; + } + + BuildState MakeBuildStateA(FastRandom& Random) + { + BuildsSelection Selection = MakeBuildsSelectionA(); + std::vector<IoBuffer> Chunks; + + ChunkedFolderContent Content = chunkedcontent_testutils::CreateChunkedFolderContent( + Random, + std::vector<std::pair<const std::string, uint64_t>>{ + std::pair<std::string, uint64_t>{"file_1", 6u * 1024u}, + std::pair<std::string, uint64_t>{"file_2.exe", 0}, + std::pair<std::string, uint64_t>{"file_3.txt", 798}, + std::pair<std::string, uint64_t>{"dir_1/dir1_file_1.exe", 19u * 1024u}, + std::pair<std::string, uint64_t>{"dir_1/dir1_file_2.pdb", 7u * 1024u}, + std::pair<std::string, uint64_t>{"dir_1/dir1_file_3.txt", 93}, + std::pair<std::string, uint64_t>{"dir_2/dir2_dir1/dir2_dir1_file_1.exe", 31u * 1024u}, + std::pair<std::string, uint64_t>{"dir_2/dir2_dir1/dir2_dir1_file_2.pdb", 17u * 1024u}, + std::pair<std::string, uint64_t>{"dir_2/dir2_dir1/dir2_dir1_file_3.dll", 13u * 1024u}, + std::pair<std::string, uint64_t>{"dir_2/dir2_dir2/dir2_dir2_file_1.txt", 2u * 1024u}, + std::pair<std::string, uint64_t>{"dir_2/dir2_dir2/dir2_dir2_file_2.json", 3u * 1024u}}, + 4u * 1024u, + Chunks); + return BuildState{.Selection = std::move(Selection), .ChunkedContent = std::move(Content)}; + } + + FolderContent MakeFolderContent(ChunkedFolderContent& Content) + { + FolderContent Result = {.Platform = Content.Platform}; + Result.Paths = Content.Paths; + Result.RawSizes = Content.RawSizes; + Result.Attributes = Content.Attributes; + Result.ModificationTicks.resize(Result.Paths.size(), 0); + uint64_t AttributeIndex = 0; + for (uint64_t& ModificationTick : Result.ModificationTicks) + { + ModificationTick = ++AttributeIndex; + } + return Result; + } +} // namespace buildsavestate_test + +TEST_CASE("buildsavestate.BuildsSelection") +{ + using namespace buildsavestate_test; + + const BuildsSelection Selection = MakeBuildsSelectionA(); + + CbObjectWriter Writer; + BuildsSelection::Write(Selection, Writer); + CbObject SavedObject = Writer.Save(); + + BuildsSelection ReadbackSelection = BuildsSelection::Read(SavedObject, nullptr); + + CHECK(Equal(Selection, ReadbackSelection)); +} + +TEST_CASE("buildsavestate.BuildsState") +{ + using namespace buildsavestate_test; + + FastRandom Random; + + BuildState State = MakeBuildStateA(Random); + + CbObjectWriter Writer; + BuildState::Write(State, Writer); + CbObject SavedObject = Writer.Save(); + + BuildState ReadbackState = BuildState::Read(SavedObject); + + CHECK(Equal(State, ReadbackState)); +} + +TEST_CASE("buildsavestate.BuildsSaveState") +{ + using namespace buildsavestate_test; + + FastRandom Random; + + BuildState State = MakeBuildStateA(Random); + FolderContent FolderState = MakeFolderContent(State.ChunkedContent); + BuildSaveState SaveState = {.State = std::move(State), + .FolderState = std::move(FolderState), + .LocalPath = "\\\\?\\E:\\temp\\localpath"}; + + CbObjectWriter Writer; + BuildSaveState::Write(SaveState, Writer); + CbObject SavedObject = Writer.Save(); + + BuildSaveState ReadbackSavedState = BuildSaveState::Read(SavedObject); + + CHECK(Equal(SaveState, ReadbackSavedState)); +} + +TEST_CASE("buildsavestate.BuildsDownloadInfo") +{ + using namespace buildsavestate_test; + + BuildsDownloadInfo DownloadInfo = {.Selection = MakeBuildsSelectionA(), + .LocalPath = "\\\\?\\E:\\temp\\localpath", + .StateFilePath = "\\\\?\\E:\\temp\\localpath\\.zen\\state.cbo", + .Iso8601Date = DateTime::Now().ToIso8601()}; + + CbObjectWriter Writer; + BuildsDownloadInfo::Write(DownloadInfo, Writer); + CbObject SavedObject = Writer.Save(); + + BuildsDownloadInfo ReadbackDownloadInfo = BuildsDownloadInfo::Read(SavedObject); + + CHECK(Equal(DownloadInfo, ReadbackDownloadInfo)); +} + +TEST_CASE("buildsavestate.DownloadedPaths") +{ + using namespace buildsavestate_test; + + ScopedTemporaryDirectory SystemDir; + + BuildsDownloadInfo DownloadInfoA = {.Selection = MakeBuildsSelectionA(), + .LocalPath = "\\\\?\\E:\\temp\\localpathA", + .StateFilePath = "\\\\?\\E:\\temp\\localpathA\\.zen\\state.cbo", + .Iso8601Date = DateTime(DateTime::Now().GetTicks() - 200).ToIso8601()}; + + BuildsDownloadInfo DownloadInfoB = {.Selection = MakeBuildsSelectionB(), + .LocalPath = "\\\\?\\E:\\temp\\localpathB", + .StateFilePath = "\\\\?\\E:\\temp\\localpathB\\.zen\\state.cbo", + .Iso8601Date = DateTime(DateTime::Now().GetTicks() - 100).ToIso8601()}; + + AddDownloadedPath(SystemDir.Path(), DownloadInfoA); + AddDownloadedPath(SystemDir.Path(), DownloadInfoB); + + std::vector<std::filesystem::path> DownloadedPaths = GetDownloadedStatePaths(SystemDir.Path()); + + CHECK_EQ(2u, DownloadedPaths.size()); + BuildsDownloadInfo ReadBackDownloadInfo0 = ReadDownloadedInfoFile(DownloadedPaths[0]); + BuildsDownloadInfo ReadBackDownloadInfo1 = ReadDownloadedInfoFile(DownloadedPaths[1]); + + if (DownloadInfoA.LocalPath == ReadBackDownloadInfo0.LocalPath) + { + CHECK(Equal(DownloadInfoA, ReadBackDownloadInfo0)); + CHECK(Equal(DownloadInfoB, ReadBackDownloadInfo1)); + } + else + { + CHECK(Equal(DownloadInfoA, ReadBackDownloadInfo1)); + CHECK(Equal(DownloadInfoB, ReadBackDownloadInfo0)); + } +} + +#endif // ZEN_WITH_TESTS + } // namespace zen diff --git a/src/zenremotestore/builds/buildstorageoperations.cpp b/src/zenremotestore/builds/buildstorageoperations.cpp index 798ef4dae..92017e98d 100644 --- a/src/zenremotestore/builds/buildstorageoperations.cpp +++ b/src/zenremotestore/builds/buildstorageoperations.cpp @@ -2,6 +2,7 @@ #include <zenremotestore/builds/buildstorageoperations.h> +#include <zenremotestore/builds/buildcontent.h> #include <zenremotestore/builds/buildsavedstate.h> #include <zenremotestore/builds/buildstorage.h> #include <zenremotestore/builds/buildstoragecache.h> @@ -30,8 +31,6 @@ ZEN_THIRD_PARTY_INCLUDES_START #include <tsl/robin_set.h> ZEN_THIRD_PARTY_INCLUDES_END -#define EXTRA_VERIFY 0 - namespace zen { using namespace std::literals; @@ -1104,6 +1103,14 @@ BuildsOperationUpdateFolder::Execute(FolderContent& OutLocalFolderState) NeededBlockChunkIndexes.push_back(ChunkBlockIndex); } } + else + { + ZEN_ASSERT(!RemoteChunkIndexNeedsCopyFromSourceFlags[RemoteChunkIndex]); + } + } + else + { + ZEN_DEBUG("Chunk {} not found in block {}", ChunkHash, BlockDescription.BlockHash); } } return NeededBlockChunkIndexes; @@ -1973,9 +1980,6 @@ BuildsOperationUpdateFolder::Execute(FolderContent& OutLocalFolderState) if (!m_Options.PrimeCacheOnly) { - ZEN_ASSERT(m_WrittenChunkByteCount == BytesToWrite); - ZEN_ASSERT(m_ValidatedChunkByteCount == BytesToValidate); - uint32_t RawSequencesMissingWriteCount = 0; for (uint32_t SequenceIndex = 0; SequenceIndex < SequenceIndexChunksLeftToWriteCounters.size(); SequenceIndex++) { @@ -1999,6 +2003,8 @@ BuildsOperationUpdateFolder::Execute(FolderContent& OutLocalFolderState) } } ZEN_ASSERT(RawSequencesMissingWriteCount == 0); + ZEN_ASSERT(m_WrittenChunkByteCount == BytesToWrite); + ZEN_ASSERT(m_ValidatedChunkByteCount == BytesToValidate); } const uint64_t DownloadedBytes = m_DownloadStats.DownloadedChunkByteCount.load() + @@ -2702,46 +2708,43 @@ BuildsOperationUpdateFolder::FindScavengeSources() std::vector<ScavengeSource> Result; for (const std::filesystem::path& EntryPath : StatePaths) { - bool DeleteEntry = false; - - // Read state and verify that it is valid - IoBuffer MetaDataJson = ReadFile(EntryPath).Flatten(); - std::string_view Json(reinterpret_cast<const char*>(MetaDataJson.GetData()), MetaDataJson.GetSize()); - std::string JsonError; - CbObject DownloadInfo = LoadCompactBinaryFromJson(Json, JsonError).AsObject(); - if (JsonError.empty()) + if (IsFile(EntryPath)) { - std::filesystem::path StateFilePath = DownloadInfo["statePath"].AsU8String(); - if (IsFile(StateFilePath)) + bool DeleteEntry = false; + + try { - std::filesystem::path Path = DownloadInfo["path"].AsU8String(); - if (!std::filesystem::equivalent(Path, m_Path)) + BuildsDownloadInfo Info = ReadDownloadedInfoFile(EntryPath); + if (!Info.LocalPath.empty()) { - if (IsDir(Path)) - { - Result.push_back({.StateFilePath = std::move(StateFilePath), .Path = std::move(Path)}); - } - else + if (!std::filesystem::equivalent(Info.LocalPath, m_Path)) { - DeleteEntry = true; + if (IsDir(Info.LocalPath) && IsFile(Info.StateFilePath)) + { + Result.push_back({.StateFilePath = std::move(Info.StateFilePath), .Path = std::move(Info.LocalPath)}); + } + else + { + DeleteEntry = true; + } } } + else + { + DeleteEntry = true; + } } - else + catch (const std::exception& Ex) { + ZEN_OPERATION_LOG_WARN(m_LogOutput, "{}", Ex.what()); DeleteEntry = true; } - } - else - { - ZEN_OPERATION_LOG_WARN(m_LogOutput, "Invalid download state file at {}. '{}'", EntryPath, JsonError); - DeleteEntry = true; - } - if (DeleteEntry) - { - std::error_code DummyEc; - std::filesystem::remove(EntryPath, DummyEc); + if (DeleteEntry) + { + std::error_code DummyEc; + std::filesystem::remove(EntryPath, DummyEc); + } } } return Result; @@ -2828,7 +2831,9 @@ BuildsOperationUpdateFolder::FindScavengeContent(const ScavengeSource& Source, FolderContent LocalFolderState; try { - ReadStateFile(Source.StateFilePath, LocalFolderState, OutScavengedLocalContent); + BuildSaveState SavedState = ReadBuildSaveStateFile(Source.StateFilePath); + OutScavengedLocalContent = std::move(SavedState.State.ChunkedContent); + LocalFolderState = std::move(SavedState.FolderState); } catch (const std::exception& Ex) { @@ -5127,51 +5132,53 @@ BuildsOperationUploadFolder::Execute() { AllChunkBlockHashes.push_back(BlockDescription.BlockHash); } -#if EXTRA_VERIFY - tsl::robin_map<IoHash, size_t, IoHash::Hasher> ChunkHashToAbsoluteChunkIndex; - std::vector<IoHash> AbsoluteChunkHashes; - AbsoluteChunkHashes.reserve(LocalContent.ChunkedContent.ChunkHashes.size()); - for (uint32_t ChunkIndex : LooseChunkIndexes) + std::vector<IoHash> AbsoluteChunkHashes; + if (m_Options.DoExtraContentValidation) { - ChunkHashToAbsoluteChunkIndex.insert({LocalContent.ChunkedContent.ChunkHashes[ChunkIndex], AbsoluteChunkHashes.size()}); - AbsoluteChunkHashes.push_back(LocalContent.ChunkedContent.ChunkHashes[ChunkIndex]); - } - for (const ChunkBlockDescription& Block : AllChunkBlockDescriptions) - { - for (const IoHash& ChunkHash : Block.ChunkRawHashes) + tsl::robin_map<IoHash, size_t, IoHash::Hasher> ChunkHashToAbsoluteChunkIndex; + AbsoluteChunkHashes.reserve(LocalContent.ChunkedContent.ChunkHashes.size()); + for (uint32_t ChunkIndex : LooseChunkIndexes) { - ChunkHashToAbsoluteChunkIndex.insert({ChunkHash, AbsoluteChunkHashes.size()}); - AbsoluteChunkHashes.push_back(ChunkHash); + ChunkHashToAbsoluteChunkIndex.insert({LocalContent.ChunkedContent.ChunkHashes[ChunkIndex], AbsoluteChunkHashes.size()}); + AbsoluteChunkHashes.push_back(LocalContent.ChunkedContent.ChunkHashes[ChunkIndex]); + } + for (const ChunkBlockDescription& Block : AllChunkBlockDescriptions) + { + for (const IoHash& ChunkHash : Block.ChunkRawHashes) + { + ChunkHashToAbsoluteChunkIndex.insert({ChunkHash, AbsoluteChunkHashes.size()}); + AbsoluteChunkHashes.push_back(ChunkHash); + } + } + for (const IoHash& ChunkHash : LocalContent.ChunkedContent.ChunkHashes) + { + ZEN_ASSERT(AbsoluteChunkHashes[ChunkHashToAbsoluteChunkIndex.at(ChunkHash)] == ChunkHash); + ZEN_ASSERT(LocalContent.ChunkedContent.ChunkHashes[LocalLookup.ChunkHashToChunkIndex.at(ChunkHash)] == ChunkHash); + } + for (const uint32_t ChunkIndex : LocalContent.ChunkedContent.ChunkOrders) + { + ZEN_ASSERT(AbsoluteChunkHashes[ChunkHashToAbsoluteChunkIndex.at(LocalContent.ChunkedContent.ChunkHashes[ChunkIndex])] == + LocalContent.ChunkedContent.ChunkHashes[ChunkIndex]); + ZEN_ASSERT(LocalLookup.ChunkHashToChunkIndex.at(LocalContent.ChunkedContent.ChunkHashes[ChunkIndex]) == ChunkIndex); } } - for (const IoHash& ChunkHash : LocalContent.ChunkedContent.ChunkHashes) - { - ZEN_ASSERT(AbsoluteChunkHashes[ChunkHashToAbsoluteChunkIndex.at(ChunkHash)] == ChunkHash); - ZEN_ASSERT(LocalContent.ChunkedContent.ChunkHashes[LocalLookup.ChunkHashToChunkIndex.at(ChunkHash)] == ChunkHash); - } - for (const uint32_t ChunkIndex : LocalContent.ChunkedContent.ChunkOrders) - { - ZEN_ASSERT(AbsoluteChunkHashes[ChunkHashToAbsoluteChunkIndex.at(LocalContent.ChunkedContent.ChunkHashes[ChunkIndex])] == - LocalContent.ChunkedContent.ChunkHashes[ChunkIndex]); - ZEN_ASSERT(LocalLookup.ChunkHashToChunkIndex.at(LocalContent.ChunkedContent.ChunkHashes[ChunkIndex]) == ChunkIndex); - } -#endif // EXTRA_VERIFY std::vector<uint32_t> AbsoluteChunkOrders = CalculateAbsoluteChunkOrders(LocalContent.ChunkedContent.ChunkHashes, LocalContent.ChunkedContent.ChunkOrders, LocalLookup.ChunkHashToChunkIndex, LooseChunkIndexes, AllChunkBlockDescriptions); -#if EXTRA_VERIFY - for (uint32_t ChunkOrderIndex = 0; ChunkOrderIndex < LocalContent.ChunkedContent.ChunkOrders.size(); ChunkOrderIndex++) + if (m_Options.DoExtraContentValidation) { - uint32_t LocalChunkIndex = LocalContent.ChunkedContent.ChunkOrders[ChunkOrderIndex]; - uint32_t AbsoluteChunkIndex = AbsoluteChunkOrders[ChunkOrderIndex]; - const IoHash& LocalChunkHash = LocalContent.ChunkedContent.ChunkHashes[LocalChunkIndex]; - const IoHash& AbsoluteChunkHash = AbsoluteChunkHashes[AbsoluteChunkIndex]; - ZEN_ASSERT(LocalChunkHash == AbsoluteChunkHash); + for (uint32_t ChunkOrderIndex = 0; ChunkOrderIndex < LocalContent.ChunkedContent.ChunkOrders.size(); ChunkOrderIndex++) + { + uint32_t LocalChunkIndex = LocalContent.ChunkedContent.ChunkOrders[ChunkOrderIndex]; + uint32_t AbsoluteChunkIndex = AbsoluteChunkOrders[ChunkOrderIndex]; + const IoHash& LocalChunkHash = LocalContent.ChunkedContent.ChunkHashes[LocalChunkIndex]; + const IoHash& AbsoluteChunkHash = AbsoluteChunkHashes[AbsoluteChunkIndex]; + ZEN_ASSERT(LocalChunkHash == AbsoluteChunkHash); + } } -#endif // EXTRA_VERIFY WriteBuildContentToCompactBinary(PartManifestWriter, LocalContent.Platform, @@ -5187,7 +5194,7 @@ BuildsOperationUploadFolder::Execute() LooseChunkIndexes, AllChunkBlockHashes); -#if EXTRA_VERIFY + if (m_Options.DoExtraContentValidation) { ChunkedFolderContent VerifyFolderContent; @@ -5226,7 +5233,8 @@ BuildsOperationUploadFolder::Execute() AllChunkBlockDescriptions, VerifyFolderContent.ChunkedContent.ChunkHashes, VerifyFolderContent.ChunkedContent.ChunkRawSizes, - VerifyFolderContent.ChunkedContent.ChunkOrders); + VerifyFolderContent.ChunkedContent.ChunkOrders, + m_Options.DoExtraContentValidation); ZEN_ASSERT(LocalContent.Paths == VerifyFolderContent.Paths); ZEN_ASSERT(LocalContent.RawHashes == VerifyFolderContent.RawHashes); @@ -5249,7 +5257,6 @@ BuildsOperationUploadFolder::Execute() ZEN_ASSERT(LocalChunkRawSize == VerifyChunkRawSize); } } -#endif // EXTRA_VERIFY PartManifest = PartManifestWriter.Save(); } @@ -6097,19 +6104,21 @@ BuildsOperationUploadFolder::CalculateAbsoluteChunkOrders( { ZEN_TRACE_CPU("CalculateAbsoluteChunkOrders"); -#if EXTRA_VERIFY std::vector<IoHash> TmpAbsoluteChunkHashes; - TmpAbsoluteChunkHashes.reserve(LocalChunkHashes.size()); -#endif // EXTRA_VERIFY + if (m_Options.DoExtraContentValidation) + { + TmpAbsoluteChunkHashes.reserve(LocalChunkHashes.size()); + } std::vector<uint32_t> LocalChunkIndexToAbsoluteChunkIndex; LocalChunkIndexToAbsoluteChunkIndex.resize(LocalChunkHashes.size(), (uint32_t)-1); std::uint32_t AbsoluteChunkCount = 0; for (uint32_t ChunkIndex : LooseChunkIndexes) { LocalChunkIndexToAbsoluteChunkIndex[ChunkIndex] = AbsoluteChunkCount; -#if EXTRA_VERIFY - TmpAbsoluteChunkHashes.push_back(LocalChunkHashes[ChunkIndex]); -#endif // EXTRA_VERIFY + if (m_Options.DoExtraContentValidation) + { + TmpAbsoluteChunkHashes.push_back(LocalChunkHashes[ChunkIndex]); + } AbsoluteChunkCount++; } for (const ChunkBlockDescription& Block : BlockDescriptions) @@ -6122,9 +6131,10 @@ BuildsOperationUploadFolder::CalculateAbsoluteChunkOrders( ZEN_ASSERT_SLOW(LocalChunkHashes[LocalChunkIndex] == ChunkHash); LocalChunkIndexToAbsoluteChunkIndex[LocalChunkIndex] = AbsoluteChunkCount; } -#if EXTRA_VERIFY - TmpAbsoluteChunkHashes.push_back(ChunkHash); -#endif // EXTRA_VERIFY + if (m_Options.DoExtraContentValidation) + { + TmpAbsoluteChunkHashes.push_back(ChunkHash); + } AbsoluteChunkCount++; } } @@ -6133,12 +6143,13 @@ BuildsOperationUploadFolder::CalculateAbsoluteChunkOrders( for (const uint32_t LocalChunkIndex : LocalChunkOrder) { const uint32_t AbsoluteChunkIndex = LocalChunkIndexToAbsoluteChunkIndex[LocalChunkIndex]; -#if EXTRA_VERIFY - ZEN_ASSERT(LocalChunkHashes[LocalChunkIndex] == TmpAbsoluteChunkHashes[AbsoluteChunkIndex]); -#endif // EXTRA_VERIFY + if (m_Options.DoExtraContentValidation) + { + ZEN_ASSERT(LocalChunkHashes[LocalChunkIndex] == TmpAbsoluteChunkHashes[AbsoluteChunkIndex]); + } AbsoluteChunkOrder.push_back(AbsoluteChunkIndex); } -#if EXTRA_VERIFY + if (m_Options.DoExtraContentValidation) { uint32_t OrderIndex = 0; while (OrderIndex < LocalChunkOrder.size()) @@ -6151,91 +6162,9 @@ BuildsOperationUploadFolder::CalculateAbsoluteChunkOrders( OrderIndex++; } } -#endif // EXTRA_VERIFY return AbsoluteChunkOrder; } -void -BuildsOperationUploadFolder::WriteBuildContentToCompactBinary(CbObjectWriter& PartManifestWriter, - const SourcePlatform Platform, - std::span<const std::filesystem::path> Paths, - std::span<const IoHash> RawHashes, - std::span<const uint64_t> RawSizes, - std::span<const uint32_t> Attributes, - std::span<const IoHash> SequenceRawHashes, - std::span<const uint32_t> ChunkCounts, - std::span<const IoHash> LocalChunkHashes, - std::span<const uint64_t> LocalChunkRawSizes, - const std::vector<uint32_t>& AbsoluteChunkOrders, - const std::span<const uint32_t> LooseLocalChunkIndexes, - const std::span<IoHash> BlockHashes) -{ - ZEN_ASSERT(Platform != SourcePlatform::_Count); - PartManifestWriter.AddString("platform"sv, ToString(Platform)); - - uint64_t TotalSize = 0; - for (const uint64_t Size : RawSizes) - { - TotalSize += Size; - } - PartManifestWriter.AddInteger("totalSize", TotalSize); - - PartManifestWriter.BeginObject("files"sv); - { - compactbinary_helpers::WriteArray(Paths, "paths"sv, PartManifestWriter); - compactbinary_helpers::WriteArray(RawHashes, "rawhashes"sv, PartManifestWriter); - compactbinary_helpers::WriteArray(RawSizes, "rawsizes"sv, PartManifestWriter); - if (Platform == SourcePlatform::Windows) - { - compactbinary_helpers::WriteArray(Attributes, "attributes"sv, PartManifestWriter); - } - if (Platform == SourcePlatform::Linux || Platform == SourcePlatform::MacOS) - { - compactbinary_helpers::WriteArray(Attributes, "mode"sv, PartManifestWriter); - } - } - PartManifestWriter.EndObject(); // files - - PartManifestWriter.BeginObject("chunkedContent"); - { - compactbinary_helpers::WriteArray(SequenceRawHashes, "sequenceRawHashes"sv, PartManifestWriter); - compactbinary_helpers::WriteArray(ChunkCounts, "chunkcounts"sv, PartManifestWriter); - compactbinary_helpers::WriteArray(AbsoluteChunkOrders, "chunkorders"sv, PartManifestWriter); - } - PartManifestWriter.EndObject(); // chunkedContent - - size_t LooseChunkCount = LooseLocalChunkIndexes.size(); - if (LooseChunkCount > 0) - { - PartManifestWriter.BeginObject("chunkAttachments"); - { - PartManifestWriter.BeginArray("rawHashes"sv); - for (uint32_t ChunkIndex : LooseLocalChunkIndexes) - { - PartManifestWriter.AddBinaryAttachment(LocalChunkHashes[ChunkIndex]); - } - PartManifestWriter.EndArray(); // rawHashes - - PartManifestWriter.BeginArray("chunkRawSizes"sv); - for (uint32_t ChunkIndex : LooseLocalChunkIndexes) - { - PartManifestWriter.AddInteger(LocalChunkRawSizes[ChunkIndex]); - } - PartManifestWriter.EndArray(); // chunkSizes - } - PartManifestWriter.EndObject(); // - } - - if (BlockHashes.size() > 0) - { - PartManifestWriter.BeginObject("blockAttachments"); - { - compactbinary_helpers::WriteBinaryAttachmentArray(BlockHashes, "rawHashes"sv, PartManifestWriter); - } - PartManifestWriter.EndObject(); // blocks - } -} - CompositeBuffer BuildsOperationUploadFolder::FetchChunk(const ChunkedFolderContent& Content, const ChunkedContentLookup& Lookup, diff --git a/src/zenremotestore/chunking/chunkedcontent.cpp b/src/zenremotestore/chunking/chunkedcontent.cpp index ac979a64b..5f1876908 100644 --- a/src/zenremotestore/chunking/chunkedcontent.cpp +++ b/src/zenremotestore/chunking/chunkedcontent.cpp @@ -3,6 +3,7 @@ #include <zenremotestore/chunking/chunkedcontent.h> #include <zencore/compactbinaryutil.h> +#include <zencore/compositebuffer.h> #include <zencore/filesystem.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> @@ -15,6 +16,12 @@ #include <zenremotestore/chunking/chunkingcontroller.h> #include <zenutil/wildcard.h> +#if ZEN_WITH_TESTS +# include <zencore/testing.h> +# include <zencore/testutils.h> +# include <numeric> +#endif // ZEN_WITH_TESTS + ZEN_THIRD_PARTY_INCLUDES_START #include <tsl/robin_set.h> #include <gsl/gsl-lite.hpp> @@ -170,6 +177,31 @@ namespace { std::string PathCompareString(const std::filesystem::path& Path) { return ToLower(Path.generic_string()); } + tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> BuildHashLookup(std::span<const IoHash> Hashes) + { + tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> Lookup; + Lookup.reserve(Hashes.size()); + for (uint32_t Index = 0; Index < Hashes.size(); Index++) + { + bool IsNew = Lookup.insert_or_assign(Hashes[Index], Index).second; + ZEN_ASSERT(IsNew); + } + return Lookup; + } + + std::vector<uint32_t> BuildChunkOrderOffset(std::span<const uint32_t> ChunkCounts) + { + std::vector<uint32_t> ChunkOffsets; + ChunkOffsets.reserve(ChunkCounts.size()); + uint32_t Offset = 0; + for (uint32_t SequenceIndex = 0; SequenceIndex < ChunkCounts.size(); SequenceIndex++) + { + ChunkOffsets.push_back(Offset); + Offset += ChunkCounts[SequenceIndex]; + } + return ChunkOffsets; + } + } // namespace std::string_view FolderContentSourcePlatformNames[(size_t)SourcePlatform::_Count] = {"Windows"sv, "Linux"sv, "MacOS"sv}; @@ -539,9 +571,9 @@ SaveChunkedFolderContentToCompactBinary(const ChunkedFolderContent& Content, CbW } ChunkedFolderContent -LoadChunkedFolderContentToCompactBinary(CbObjectView Input) +LoadChunkedFolderContentFromCompactBinary(CbObjectView Input) { - ZEN_TRACE_CPU("LoadChunkedFolderContentToCompactBinary"); + ZEN_TRACE_CPU("LoadChunkedFolderContentFromCompactBinary"); ChunkedFolderContent Content; Content.Platform = FromString(Input["platform"sv].AsString(), GetSourceCurrentPlatform()); Content.Paths = compactbinary_helpers::ReadArray<std::filesystem::path>("paths"sv, Input); @@ -836,30 +868,135 @@ CompareChunkedContent(const ChunkedFolderContent& Lhs, const ChunkedFolderConten return true; }; -static tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> -BuildHashLookup(std::span<const IoHash> Hashes) +ChunkedFolderContent +ApplyChunkedContentOverlay(const ChunkedFolderContent& Base, + const ChunkedFolderContent& Overlay, + std::span<const std::string> OverlayIncludeWildcards, + std::span<const std::string> OverlayExcludeWildcards) { - tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> Lookup; - Lookup.reserve(Hashes.size()); - for (uint32_t Index = 0; Index < Hashes.size(); Index++) + ChunkedFolderContent Result = {.Platform = Base.Platform}; + + tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> BaseSequenceHashToSequenceIndex = + BuildHashLookup(Base.ChunkedContent.SequenceRawHashes); + tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> BaseChunkHashToChunkIndex = BuildHashLookup(Base.ChunkedContent.ChunkHashes); + std::vector<uint32_t> BaseSequenceChunkOrderOffset = BuildChunkOrderOffset(Base.ChunkedContent.ChunkCounts); + + tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> OverlaySequenceHashToSequenceIndex = + BuildHashLookup(Overlay.ChunkedContent.SequenceRawHashes); + tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> OverlayChunkHashToChunkIndex = BuildHashLookup(Overlay.ChunkedContent.ChunkHashes); + std::vector<uint32_t> OverlaySequenceChunkOrderOffset = BuildChunkOrderOffset(Overlay.ChunkedContent.ChunkCounts); + + tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> ResultSequenceHashToSequenceIndex; + tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> ResultChunkHashToChunkIndex; + + const size_t EstimatedPathCount = Max(Base.Paths.size(), Overlay.Paths.size()); + + Result.Attributes.reserve(EstimatedPathCount); + Result.Paths.reserve(EstimatedPathCount); + Result.RawSizes.reserve(EstimatedPathCount); + Result.RawHashes.reserve(EstimatedPathCount); + + const size_t EstimatedSequenceCount = + Max(Base.ChunkedContent.SequenceRawHashes.size(), Overlay.ChunkedContent.SequenceRawHashes.size()); + Result.ChunkedContent.SequenceRawHashes.reserve(EstimatedSequenceCount); + + const size_t EstimatedChunkCount = Max(Base.ChunkedContent.ChunkHashes.size(), Overlay.ChunkedContent.ChunkHashes.size()); + Result.ChunkedContent.ChunkHashes.reserve(EstimatedChunkCount); + Result.ChunkedContent.ChunkRawSizes.reserve(EstimatedChunkCount); + + const size_t EstimatedChunkOrderCount = Max(Base.ChunkedContent.ChunkOrders.size(), Overlay.ChunkedContent.ChunkOrders.size()); + Result.ChunkedContent.ChunkOrders.reserve(EstimatedChunkOrderCount); + + auto AddPath = [&Result, &ResultSequenceHashToSequenceIndex, &ResultChunkHashToChunkIndex]( + const ChunkedFolderContent& Source, + uint32_t SourcePathIndex, + const tsl::robin_map<IoHash, uint32_t, IoHash::Hasher>& SourceSequenceHashToSequenceIndex, + const std::vector<uint32_t>& SourceSequenceChunkOrderOffset) { + Result.Attributes.push_back(Source.Attributes[SourcePathIndex]); + Result.Paths.push_back(Source.Paths[SourcePathIndex]); + Result.RawSizes.push_back(Source.RawSizes[SourcePathIndex]); + Result.RawHashes.push_back(Source.RawHashes[SourcePathIndex]); + if (Source.RawSizes[SourcePathIndex] > 0) + { + if (!ResultSequenceHashToSequenceIndex.contains(Source.RawHashes[SourcePathIndex])) + { + const uint32_t ResultSequenceIndex = gsl::narrow<uint32_t>(Result.ChunkedContent.SequenceRawHashes.size()); + ResultSequenceHashToSequenceIndex.insert_or_assign(Source.RawHashes[SourcePathIndex], ResultSequenceIndex); + Result.ChunkedContent.SequenceRawHashes.push_back(Source.RawHashes[SourcePathIndex]); + auto SourceSequenceIndexIt = SourceSequenceHashToSequenceIndex.find(Source.RawHashes[SourcePathIndex]); + ZEN_ASSERT(SourceSequenceIndexIt != SourceSequenceHashToSequenceIndex.end()); + const uint32_t SourceSequenceIndex = SourceSequenceIndexIt->second; + const uint32_t ChunkOrderOffset = SourceSequenceChunkOrderOffset[SourceSequenceIndex]; + const uint32_t ChunkCount = Source.ChunkedContent.ChunkCounts[SourceSequenceIndex]; + Result.ChunkedContent.ChunkCounts.push_back(ChunkCount); + + std::span<const uint32_t> SourceChunkIndexes = + std::span<const uint32_t>(Source.ChunkedContent.ChunkOrders).subspan(ChunkOrderOffset, ChunkCount); + for (uint32_t SourceChunkIndex : SourceChunkIndexes) + { + const IoHash& ChunkHash = Source.ChunkedContent.ChunkHashes[SourceChunkIndex]; + if (auto It = ResultChunkHashToChunkIndex.find(ChunkHash); It != ResultChunkHashToChunkIndex.end()) + { + const uint32_t ResultChunkIndex = It->second; + Result.ChunkedContent.ChunkOrders.push_back(ResultChunkIndex); + } + else + { + const uint32_t ResultChunkIndex = gsl::narrow<uint32_t>(Result.ChunkedContent.ChunkHashes.size()); + Result.ChunkedContent.ChunkHashes.push_back(ChunkHash); + Result.ChunkedContent.ChunkRawSizes.push_back(Source.ChunkedContent.ChunkRawSizes[SourceChunkIndex]); + Result.ChunkedContent.ChunkOrders.push_back(ResultChunkIndex); + ResultChunkHashToChunkIndex.insert_or_assign(ChunkHash, ResultChunkIndex); + } + } + } + } + }; + + if (OverlayIncludeWildcards.empty() && OverlayExcludeWildcards.empty()) { - Lookup.insert_or_assign(Hashes[Index], Index); - } - return Lookup; -} + tsl::robin_set<std::string> OverlayPaths; + OverlayPaths.reserve(Overlay.Paths.size()); + for (uint32_t OverlayPathIndex = 0; OverlayPathIndex < Overlay.Paths.size(); OverlayPathIndex++) + { + const std::string PathString = ToLower(Overlay.Paths[OverlayPathIndex].generic_string()); + OverlayPaths.insert(PathString); + } + for (uint32_t BasePathIndex = 0; BasePathIndex < Base.Paths.size(); BasePathIndex++) + { + const std::string PathString = ToLower(Base.Paths[BasePathIndex].generic_string()); + if (!OverlayPaths.contains(PathString)) + { + AddPath(Base, BasePathIndex, BaseSequenceHashToSequenceIndex, BaseSequenceChunkOrderOffset); + } + } -static std::vector<uint32_t> -BuildChunkOrderOffset(std::span<const uint32_t> ChunkCounts) -{ - std::vector<uint32_t> ChunkOffsets; - ChunkOffsets.reserve(ChunkCounts.size()); - uint32_t Offset = 0; - for (uint32_t SequenceIndex = 0; SequenceIndex < ChunkCounts.size(); SequenceIndex++) + for (uint32_t OverlayPathIndex = 0; OverlayPathIndex < Overlay.Paths.size(); OverlayPathIndex++) + { + AddPath(Overlay, OverlayPathIndex, OverlaySequenceHashToSequenceIndex, OverlaySequenceChunkOrderOffset); + } + } + else { - ChunkOffsets.push_back(Offset); - Offset += ChunkCounts[SequenceIndex]; + for (uint32_t BasePathIndex = 0; BasePathIndex < Base.Paths.size(); BasePathIndex++) + { + const std::string PathString = ToLower(Base.Paths[BasePathIndex].generic_string()); + if (!IncludePath(OverlayIncludeWildcards, OverlayExcludeWildcards, PathString, /*CaseSensitive*/ true)) + { + AddPath(Base, BasePathIndex, BaseSequenceHashToSequenceIndex, BaseSequenceChunkOrderOffset); + } + } + + for (uint32_t OverlayPathIndex = 0; OverlayPathIndex < Overlay.Paths.size(); OverlayPathIndex++) + { + const std::string PathString = ToLower(Overlay.Paths[OverlayPathIndex].generic_string()); + if (IncludePath(OverlayIncludeWildcards, OverlayExcludeWildcards, PathString, /*CaseSensitive*/ true)) + { + AddPath(Overlay, OverlayPathIndex, OverlaySequenceHashToSequenceIndex, OverlaySequenceChunkOrderOffset); + } + } } - return ChunkOffsets; + return Result; } ChunkedFolderContent @@ -1058,9 +1195,85 @@ BuildChunkedContentLookup(const ChunkedFolderContent& Content) } void +CalculateLocalChunkOrders(const std::span<const uint32_t>& AbsoluteChunkOrders, + const std::span<const IoHash> LooseChunkHashes, + const std::span<const uint64_t> LooseChunkRawSizes, + const std::span<const ChunkBlockDescription>& BlockDescriptions, + std::vector<IoHash>& OutLocalChunkHashes, + std::vector<uint64_t>& OutLocalChunkRawSizes, + std::vector<uint32_t>& OutLocalChunkOrders, + bool DoExtraVerify) +{ + ZEN_TRACE_CPU("CalculateLocalChunkOrders"); + + std::vector<IoHash> AbsoluteChunkHashes; + std::vector<uint64_t> AbsoluteChunkRawSizes; + AbsoluteChunkHashes.insert(AbsoluteChunkHashes.end(), LooseChunkHashes.begin(), LooseChunkHashes.end()); + AbsoluteChunkRawSizes.insert(AbsoluteChunkRawSizes.end(), LooseChunkRawSizes.begin(), LooseChunkRawSizes.end()); + for (const ChunkBlockDescription& Block : BlockDescriptions) + { + AbsoluteChunkHashes.insert(AbsoluteChunkHashes.end(), Block.ChunkRawHashes.begin(), Block.ChunkRawHashes.end()); + AbsoluteChunkRawSizes.insert(AbsoluteChunkRawSizes.end(), Block.ChunkRawLengths.begin(), Block.ChunkRawLengths.end()); + } + OutLocalChunkHashes.reserve(AbsoluteChunkHashes.size()); + OutLocalChunkRawSizes.reserve(AbsoluteChunkRawSizes.size()); + OutLocalChunkOrders.reserve(AbsoluteChunkOrders.size()); + + tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> ChunkHashToChunkIndex; + ChunkHashToChunkIndex.reserve(AbsoluteChunkHashes.size()); + + for (uint32_t AbsoluteChunkOrderIndex = 0; AbsoluteChunkOrderIndex < AbsoluteChunkOrders.size(); AbsoluteChunkOrderIndex++) + { + const uint32_t AbsoluteChunkIndex = AbsoluteChunkOrders[AbsoluteChunkOrderIndex]; + const IoHash& AbsoluteChunkHash = AbsoluteChunkHashes[AbsoluteChunkIndex]; + const uint64_t AbsoluteChunkRawSize = AbsoluteChunkRawSizes[AbsoluteChunkIndex]; + + if (auto It = ChunkHashToChunkIndex.find(AbsoluteChunkHash); It != ChunkHashToChunkIndex.end()) + { + const uint32_t LocalChunkIndex = It->second; + OutLocalChunkOrders.push_back(LocalChunkIndex); + } + else + { + uint32_t LocalChunkIndex = gsl::narrow<uint32_t>(OutLocalChunkHashes.size()); + OutLocalChunkHashes.push_back(AbsoluteChunkHash); + OutLocalChunkRawSizes.push_back(AbsoluteChunkRawSize); + OutLocalChunkOrders.push_back(LocalChunkIndex); + ChunkHashToChunkIndex.insert_or_assign(AbsoluteChunkHash, LocalChunkIndex); + } + if (DoExtraVerify) + { + const uint32_t LocalChunkIndex = OutLocalChunkOrders[AbsoluteChunkOrderIndex]; + const IoHash& LocalChunkHash = OutLocalChunkHashes[LocalChunkIndex]; + const uint64_t& LocalChunkRawSize = OutLocalChunkRawSizes[LocalChunkIndex]; + ZEN_ASSERT(LocalChunkHash == AbsoluteChunkHash); + ZEN_ASSERT(LocalChunkRawSize == AbsoluteChunkRawSize); + } + } + if (DoExtraVerify) + { + for (uint32_t OrderIndex = 0; OrderIndex < OutLocalChunkOrders.size(); OrderIndex++) + { + uint32_t LocalChunkIndex = OutLocalChunkOrders[OrderIndex]; + const IoHash LocalChunkHash = OutLocalChunkHashes[LocalChunkIndex]; + uint64_t LocalChunkRawSize = OutLocalChunkRawSizes[LocalChunkIndex]; + + uint32_t VerifyChunkIndex = AbsoluteChunkOrders[OrderIndex]; + const IoHash VerifyChunkHash = AbsoluteChunkHashes[VerifyChunkIndex]; + uint64_t VerifyChunkRawSize = AbsoluteChunkRawSizes[VerifyChunkIndex]; + + ZEN_ASSERT(LocalChunkHash == VerifyChunkHash); + ZEN_ASSERT(LocalChunkRawSize == VerifyChunkRawSize); + } + } +} + +void ValidateChunkedFolderContent(const ChunkedFolderContent& Content, std::span<const ChunkBlockDescription> BlockDescriptions, - std::span<const IoHash> LooseChunks) + std::span<const IoHash> LooseChunks, + std::span<const std::string> IncludeWildcards, + std::span<const std::string> ExcludeWildcards) { size_t TotalKnownChunkCount = LooseChunks.size(); for (const ChunkBlockDescription& BlockDescription : BlockDescriptions) @@ -1101,6 +1314,8 @@ ValidateChunkedFolderContent(const ChunkedFolderContent& Content, std::span<const uint32_t> ChunkIndexes = std::span<const uint32_t>(Content.ChunkedContent.ChunkOrders).subspan(ChunkOrderOffset, ChunkCount); + bool VerifyIfChunkExists = IncludePath(IncludeWildcards, ExcludeWildcards, ToLower(Path.generic_string()), true); + IoHashStream Hasher; uint64_t SizeSum = 0; for (uint32_t ChunkIndex : ChunkIndexes) @@ -1116,9 +1331,13 @@ ValidateChunkedFolderContent(const ChunkedFolderContent& Content, { throw std::runtime_error("Chunked folder content contains zero chunk hash"); } - if (!KnownChunks.contains(ChunkRawHash)) + + if (VerifyIfChunkExists) { - throw std::runtime_error(fmt::format("Chunked folder content references an unknown chunk '{}'", ChunkRawHash)); + if (!KnownChunks.contains(ChunkRawHash)) + { + throw std::runtime_error(fmt::format("Chunked folder content references an unknown chunk '{}'", ChunkRawHash)); + } } SizeSum += ChunkSize; } @@ -1186,35 +1405,41 @@ chunkedcontent_forcelink() { } -namespace chunked_test_utils { - struct ChunkedFile +namespace chunkedcontent_testutils { + ChunkedFile CreateChunkedFile(FastRandom& Random, const size_t FinalSize, size_t ChunkingSize, const IoBuffer& LastUsedBuffer) { - IoHash RawHash; - std::vector<IoHash> ChunkHashes; - std::vector<uint64_t> ChunkSizes; - std::vector<IoBuffer> Chunks; - }; - - ChunkedFile CreateChunkedFile(FastRandom& Random, size_t Size, size_t ChunkingSize) - { - size_t ChunkCount = (Size + (ChunkingSize - 1)) / ChunkingSize; + size_t ChunkCount = (FinalSize + (ChunkingSize - 1)) / ChunkingSize; std::vector<IoHash> ChunkHashes; std::vector<uint64_t> ChunkSizes; std::vector<IoBuffer> Chunks; ChunkHashes.reserve(ChunkCount); ChunkSizes.reserve(ChunkCount); + size_t SizeLeft = FinalSize; IoHashStream HashStream; - while (Size > 0) + while (SizeLeft > 0) { - size_t ChunkSize = Min(Size, ChunkingSize); - IoBuffer ChunkBuffer = CreateRandomBlob(Random, ChunkSize); + size_t ChunkSize = Min(SizeLeft, ChunkingSize); + IoBuffer ChunkBuffer; + if (LastUsedBuffer && FinalSize == SizeLeft) + { + ChunkSize = Min(ChunkSize, LastUsedBuffer.GetSize()); + ChunkBuffer = IoBuffer(LastUsedBuffer, 0, ChunkSize); + } + else + { + ChunkBuffer = CreateRandomBlob(Random, ChunkSize); + } HashStream.Append(ChunkBuffer); ChunkHashes.push_back(IoHash::HashBuffer(ChunkBuffer)); ChunkSizes.push_back(ChunkSize); Chunks.emplace_back(std::move(ChunkBuffer)); - Size -= ChunkSize; + SizeLeft -= ChunkSize; } + ZEN_ASSERT(std::accumulate(ChunkSizes.begin(), ChunkSizes.end(), uint64_t(0)) == FinalSize); + ZEN_ASSERT(std::accumulate(Chunks.begin(), Chunks.end(), uint64_t(0), [](uint64_t Current, const IoBuffer& B) { + return Current + B.GetSize(); + }) == FinalSize); return ChunkedFile{.RawHash = HashStream.GetHash(), .ChunkHashes = std::move(ChunkHashes), .ChunkSizes = std::move(ChunkSizes), @@ -1233,6 +1458,8 @@ namespace chunked_test_utils { Result.RawHashes.reserve(PathAndSizes.size()); ChunkPayloads.reserve(PathAndSizes.size()); + IoBuffer LastChunkGenerated; + tsl::robin_map<IoHash, uint32_t> SequenceToIndex; tsl::robin_map<IoHash, uint32_t> ChunkToIndex; for (size_t PathIndex = 0; PathIndex < PathAndSizes.size(); PathIndex++) @@ -1246,7 +1473,8 @@ namespace chunked_test_utils { if (Size > 0) { - ChunkedFile File = CreateChunkedFile(Random, Size, ChunkingSize); + ChunkedFile File = CreateChunkedFile(Random, Size, ChunkingSize, LastChunkGenerated); + LastChunkGenerated = File.Chunks.back(); Result.RawHashes.push_back(File.RawHash); if (auto SequenceIt = SequenceToIndex.find(File.RawHash); SequenceIt == SequenceToIndex.end()) @@ -1259,12 +1487,13 @@ namespace chunked_test_utils { const IoHash& ChunkHash = File.ChunkHashes[ChunkIndex]; if (auto ChunkIt = ChunkToIndex.find(ChunkHash); ChunkIt == ChunkToIndex.end()) { - const uint32_t ChunkedContentChunkIndex = gsl::narrow<uint32_t>(Result.ChunkedContent.ChunkOrders.size()); + const uint32_t ChunkedContentChunkIndex = gsl::narrow<uint32_t>(Result.ChunkedContent.ChunkHashes.size()); Result.ChunkedContent.ChunkOrders.push_back(gsl::narrow<uint32_t>(ChunkedContentChunkIndex)); Result.ChunkedContent.ChunkHashes.push_back(ChunkHash); Result.ChunkedContent.ChunkRawSizes.push_back(File.ChunkSizes[ChunkIndex]); ChunkPayloads.push_back(std::move(File.Chunks[ChunkIndex])); + ChunkToIndex.insert_or_assign(ChunkHash, ChunkedContentChunkIndex); } else { @@ -1281,67 +1510,7 @@ namespace chunked_test_utils { } return Result; } -# if 0 - void ValidateChunkedFolderContent(const ChunkedFolderContent& Content, std::span<const IoBuffer> Chunks) - { - std::vector<uint32_t> ChunkOrderOffsets = BuildChunkOrderOffset(Content.ChunkedContent.ChunkCounts); - tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> SequenceIndexLookup = BuildHashLookup(Content.ChunkedContent.SequenceRawHashes); - std::vector<size_t> SequenceUseCount(Content.ChunkedContent.SequenceRawHashes.size(), 0); - std::vector<size_t> ChunkUseCount(Content.ChunkedContent.ChunkHashes.size(), 0); - for (size_t PathIndex = 0; PathIndex < Content.Paths.size(); PathIndex++) - { - const std::filesystem::path& Path = Content.Paths[PathIndex]; - ZEN_ASSERT(!Path.empty()); - const uint64_t RawSize = Content.RawSizes[PathIndex]; - const IoHash RawHash = Content.RawHashes[PathIndex]; - if (RawSize > 0) - { - if (auto It = SequenceIndexLookup.find(RawHash); It != SequenceIndexLookup.end()) - { - const uint32_t SourceSequenceIndex = It->second; - SequenceUseCount[SourceSequenceIndex]++; - const uint32_t ChunkOrderOffset = ChunkOrderOffsets[SourceSequenceIndex]; - const uint32_t ChunkCount = Content.ChunkedContent.ChunkCounts[SourceSequenceIndex]; - - std::span<const uint32_t> ChunkIndexes = - std::span<const uint32_t>(Content.ChunkedContent.ChunkOrders).subspan(ChunkOrderOffset, ChunkCount); - - IoHashStream Hasher; - uint64_t SizeSum = 0; - for (uint32_t ChunkIndex : ChunkIndexes) - { - ChunkUseCount[ChunkIndex]++; - const IoBuffer& ChunkBuffer = Chunks[ChunkIndex]; - const uint64_t ChunkSize = Content.ChunkedContent.ChunkRawSizes[ChunkIndex]; - const IoHash& ChunkRawHash = Content.ChunkedContent.ChunkHashes[ChunkIndex]; - SizeSum += ChunkSize; - CHECK_EQ(ChunkRawHash, IoHash::HashBuffer(ChunkBuffer)); - Hasher.Append(ChunkBuffer); - } - CHECK_EQ(RawHash, Hasher.GetHash()); - CHECK_EQ(SizeSum, RawSize); - } - else - { - CHECK(false); - } - } - else - { - CHECK(RawHash == IoHash::Zero); - } - } - for (uint32_t SequenceIndex = 0; SequenceIndex < SequenceUseCount.size(); SequenceIndex++) - { - CHECK(SequenceUseCount[SequenceIndex] > 0); - } - for (uint32_t ChunkIndex = 0; ChunkIndex < ChunkUseCount.size(); ChunkIndex++) - { - CHECK(ChunkUseCount[ChunkIndex] > 0); - } - } -# endif // 0 std::vector<IoBuffer> GetChunkPayloads(std::span<const IoHash> BaseHashes, std::span<const IoBuffer> BaseChunks, std::span<const IoHash> OverlayHashes, @@ -1382,41 +1551,9 @@ namespace chunked_test_utils { return Result; } - bool IncludePath(std::span<const std::string> IncludeWildcards, - std::span<const std::string> ExcludeWildcards, - const std::filesystem::path& Path) - { - const std::string PathString = Path.generic_string(); - bool IncludePath = true; - if (!IncludeWildcards.empty()) - { - IncludePath = false; - for (const std::string& IncludeWildcard : IncludeWildcards) - { - if (MatchWildcard(IncludeWildcard, PathString, /*CaseSensitive*/ false)) - { - IncludePath = true; - break; - } - } - if (!IncludePath) - { - return false; - } - } - for (const std::string& ExcludeWildcard : ExcludeWildcards) - { - if (MatchWildcard(ExcludeWildcard, PathString, /*CaseSensitive*/ false)) - { - return false; - } - } - return true; - } - -} // namespace chunked_test_utils +} // namespace chunkedcontent_testutils -TEST_CASE("DeletePathsFromContent") +TEST_CASE("chunkedcontent.DeletePathsFromContent") { FastRandom BaseRandom; @@ -1448,8 +1585,8 @@ TEST_CASE("DeletePathsFromContent") {BasePaths[9], BaseSizes[9]}, {BasePaths[10], BaseSizes[10]}}; - ChunkedFolderContent Base = chunked_test_utils::CreateChunkedFolderContent(BaseRandom, BasePathAndSizes, 4u * 1024u, BaseChunks); - ValidateChunkedFolderContent(Base, {}, Base.ChunkedContent.ChunkHashes); + ChunkedFolderContent Base = chunkedcontent_testutils::CreateChunkedFolderContent(BaseRandom, BasePathAndSizes, 4u * 1024u, BaseChunks); + ValidateChunkedFolderContent(Base, {}, Base.ChunkedContent.ChunkHashes, {}, {}); tsl::robin_map<IoHash, size_t, IoHash::Hasher> BaseChunksLookup; for (size_t Index = 0; Index < BaseChunks.size(); Index++) @@ -1463,7 +1600,7 @@ TEST_CASE("DeletePathsFromContent") std::vector<std::filesystem::path> DeletedPaths; for (const std::filesystem::path& RemotePath : Base.Paths) { - if (!chunked_test_utils::IncludePath(IncludeWildcards, ExcludeWildcards, RemotePath)) + if (!IncludePath(IncludeWildcards, ExcludeWildcards, ToLower(RemotePath.generic_string()), true)) { DeletedPaths.push_back(RemotePath); } @@ -1477,7 +1614,161 @@ TEST_CASE("DeletePathsFromContent") InlineRemoveUnusedHashes(FilteredChunks, FilteredContent.ChunkedContent.ChunkHashes); - ValidateChunkedFolderContent(FilteredContent, {}, FilteredChunks); + ValidateChunkedFolderContent(FilteredContent, {}, FilteredChunks, {}, {}); +} + +TEST_CASE("chunkedcontent.ApplyChunkedContentOverlay") +{ + FastRandom BaseRandom; + + std::vector<IoBuffer> BaseChunks; + + const std::string BasePaths[11] = {{"file_1"}, + {"file_2.exe"}, + {"file_3.txt"}, + {"dir_1/dir1_file_1.exe"}, + {"dir_1/dir1_file_2.pdb"}, + {"dir_1/dir1_file_3.txt"}, + {"dir_2/dir2_dir1/dir2_dir1_file_1.exe"}, + {"dir_2/dir2_dir1/dir2_dir1_file_2.pdb"}, + {"dir_2/dir2_dir1/dir2_dir1_file_3.dll"}, + {"dir_2/dir2_dir2/dir2_dir2_file_1.txt"}, + {"dir_2/dir2_dir2/dir2_dir2_file_2.json"}}; + const uint64_t BaseSizes[11] = + {6u * 1024u, 0, 798, 19u * 1024u, 7u * 1024u, 93, 31u * 1024u, 17u * 1024u, 13u * 1024u, 2u * 1024u, 3u * 1024u}; + + std::pair<const std::string, uint64_t> BasePathAndSizes[11] = {{BasePaths[0], BaseSizes[0]}, + {BasePaths[1], BaseSizes[1]}, + {BasePaths[2], BaseSizes[2]}, + {BasePaths[3], BaseSizes[3]}, + {BasePaths[4], BaseSizes[4]}, + {BasePaths[5], BaseSizes[5]}, + {BasePaths[6], BaseSizes[6]}, + {BasePaths[7], BaseSizes[7]}, + {BasePaths[8], BaseSizes[8]}, + {BasePaths[9], BaseSizes[9]}, + {BasePaths[10], BaseSizes[10]}}; + + const std::string OverlayPaths[6] = {{"file_1"}, + {"file_4"}, + {"dir_1/dir1_file_1.exe"}, + {"dir_1/dir1_file_2.pdb"}, + {"dir_2/dir2_dir1/dir2_dir1_file_1.self"}, + {"dir_2/dir2_dir1/dir2_dir1_file_2.sym"}}; + const uint64_t OverlaySizes[6] = {7u * 1024u, 1249, 17u * 1024u, 9u * 1024u, 0, 17u * 1024u}; + + std::pair<const std::string, uint64_t> OverlayPathAndSizes[6] = {{OverlayPaths[0], OverlaySizes[0]}, + {OverlayPaths[1], OverlaySizes[1]}, + {OverlayPaths[2], OverlaySizes[2]}, + {OverlayPaths[3], OverlaySizes[3]}, + {OverlayPaths[4], OverlaySizes[4]}, + {OverlayPaths[5], OverlaySizes[5]}}; + + ChunkedFolderContent Base = chunkedcontent_testutils::CreateChunkedFolderContent(BaseRandom, BasePathAndSizes, 4u * 1024u, BaseChunks); + ValidateChunkedFolderContent(Base, {}, Base.ChunkedContent.ChunkHashes, {}, {}); + tsl::robin_map<std::string, uint32_t> BasePathLookup = chunkedcontent_testutils::BuildPathLookup(Base.Paths); + + std::vector<IoBuffer> OverlayChunks; + ChunkedFolderContent Overlay = + chunkedcontent_testutils::CreateChunkedFolderContent(BaseRandom, OverlayPathAndSizes, 4u * 1024u, OverlayChunks); + ValidateChunkedFolderContent(Overlay, {}, Overlay.ChunkedContent.ChunkHashes, {}, {}); + + tsl::robin_map<std::string, uint32_t> OverlayPathLookup = chunkedcontent_testutils::BuildPathLookup(Overlay.Paths); + + auto PathMatchesBase = + [&](const std::string& Path, const ChunkedFolderContent& MergedContent, tsl::robin_map<std::string, uint32_t> MergedPathLookup) { + return MergedContent.RawHashes[MergedPathLookup.at(Path)] == Base.RawHashes[BasePathLookup.at(Path)]; + }; + + auto PathMatchesOverlay = + [&](const std::string& Path, const ChunkedFolderContent& MergedContent, tsl::robin_map<std::string, uint32_t> MergedPathLookup) { + return MergedContent.RawHashes[MergedPathLookup.at(Path)] == Overlay.RawHashes[OverlayPathLookup.at(Path)]; + }; + + { + ChunkedFolderContent AllMergedContent = ApplyChunkedContentOverlay(Base, Overlay, {}, {}); + CHECK_EQ(AllMergedContent.Paths.size(), 14); + + std::vector<IoBuffer> AllMergedChunks = chunkedcontent_testutils::GetChunkPayloads(Base.ChunkedContent.ChunkHashes, + BaseChunks, + Overlay.ChunkedContent.ChunkHashes, + OverlayChunks, + AllMergedContent.ChunkedContent.ChunkHashes); + ValidateChunkedFolderContent(AllMergedContent, {}, AllMergedContent.ChunkedContent.ChunkHashes, {}, {}); + + tsl::robin_map<std::string, uint32_t> AllMergedPathLookup = chunkedcontent_testutils::BuildPathLookup(AllMergedContent.Paths); + CHECK(PathMatchesBase("file_2.exe", AllMergedContent, AllMergedPathLookup)); + CHECK(PathMatchesBase("file_3.txt", AllMergedContent, AllMergedPathLookup)); + CHECK(PathMatchesBase("dir_1/dir1_file_3.txt", AllMergedContent, AllMergedPathLookup)); + CHECK(PathMatchesBase("dir_2/dir2_dir1/dir2_dir1_file_1.exe", AllMergedContent, AllMergedPathLookup)); + CHECK(PathMatchesBase("dir_2/dir2_dir1/dir2_dir1_file_2.pdb", AllMergedContent, AllMergedPathLookup)); + CHECK(PathMatchesBase("dir_2/dir2_dir1/dir2_dir1_file_3.dll", AllMergedContent, AllMergedPathLookup)); + CHECK(PathMatchesBase("dir_2/dir2_dir2/dir2_dir2_file_1.txt", AllMergedContent, AllMergedPathLookup)); + CHECK(PathMatchesBase("dir_2/dir2_dir2/dir2_dir2_file_2.json", AllMergedContent, AllMergedPathLookup)); + + CHECK(PathMatchesOverlay("file_1", AllMergedContent, AllMergedPathLookup)); + CHECK(PathMatchesOverlay("file_4", AllMergedContent, AllMergedPathLookup)); + CHECK(PathMatchesOverlay("dir_1/dir1_file_1.exe", AllMergedContent, AllMergedPathLookup)); + CHECK(PathMatchesOverlay("dir_1/dir1_file_2.pdb", AllMergedContent, AllMergedPathLookup)); + CHECK(PathMatchesOverlay("dir_2/dir2_dir1/dir2_dir1_file_1.self", AllMergedContent, AllMergedPathLookup)); + CHECK(PathMatchesOverlay("dir_2/dir2_dir1/dir2_dir1_file_2.sym", AllMergedContent, AllMergedPathLookup)); + } + + { + ChunkedFolderContent ReplaceExecutablesContent = + ApplyChunkedContentOverlay(Base, Overlay, std::vector<std::string>{"*.exe", "*.self"}, {}); + CHECK_EQ(ReplaceExecutablesContent.Paths.size(), 10); + + std::vector<IoBuffer> ReplaceExecutablesChunks = + chunkedcontent_testutils::GetChunkPayloads(Base.ChunkedContent.ChunkHashes, + BaseChunks, + Overlay.ChunkedContent.ChunkHashes, + OverlayChunks, + ReplaceExecutablesContent.ChunkedContent.ChunkHashes); + ValidateChunkedFolderContent(ReplaceExecutablesContent, {}, ReplaceExecutablesContent.ChunkedContent.ChunkHashes, {}, {}); + + tsl::robin_map<std::string, uint32_t> ReplaceExecutablesPathLookup = + chunkedcontent_testutils::BuildPathLookup(ReplaceExecutablesContent.Paths); + CHECK(PathMatchesBase("file_1", ReplaceExecutablesContent, ReplaceExecutablesPathLookup)); + CHECK(PathMatchesBase("file_3.txt", ReplaceExecutablesContent, ReplaceExecutablesPathLookup)); + CHECK(PathMatchesBase("dir_1/dir1_file_2.pdb", ReplaceExecutablesContent, ReplaceExecutablesPathLookup)); + CHECK(PathMatchesBase("dir_1/dir1_file_3.txt", ReplaceExecutablesContent, ReplaceExecutablesPathLookup)); + + CHECK(PathMatchesBase("dir_2/dir2_dir1/dir2_dir1_file_2.pdb", ReplaceExecutablesContent, ReplaceExecutablesPathLookup)); + CHECK(PathMatchesBase("dir_2/dir2_dir1/dir2_dir1_file_3.dll", ReplaceExecutablesContent, ReplaceExecutablesPathLookup)); + CHECK(PathMatchesBase("dir_2/dir2_dir2/dir2_dir2_file_1.txt", ReplaceExecutablesContent, ReplaceExecutablesPathLookup)); + CHECK(PathMatchesBase("dir_2/dir2_dir2/dir2_dir2_file_2.json", ReplaceExecutablesContent, ReplaceExecutablesPathLookup)); + + CHECK(PathMatchesOverlay("dir_1/dir1_file_1.exe", ReplaceExecutablesContent, ReplaceExecutablesPathLookup)); + CHECK(PathMatchesOverlay("dir_2/dir2_dir1/dir2_dir1_file_1.self", ReplaceExecutablesContent, ReplaceExecutablesPathLookup)); + } + + { + ChunkedFolderContent ReplaceDir1ExecutablesContent = ApplyChunkedContentOverlay(Base, + Overlay, + std::vector<std::string>{"dir_1/*.exe", "dir_2/*"}, + std::vector<std::string>{"dir_2/*.sym"}); + CHECK_EQ(ReplaceDir1ExecutablesContent.Paths.size(), 7); + + std::vector<IoBuffer> ReplaceDir1Chunks = + chunkedcontent_testutils::GetChunkPayloads(Base.ChunkedContent.ChunkHashes, + BaseChunks, + Overlay.ChunkedContent.ChunkHashes, + OverlayChunks, + ReplaceDir1ExecutablesContent.ChunkedContent.ChunkHashes); + ValidateChunkedFolderContent(ReplaceDir1ExecutablesContent, {}, ReplaceDir1ExecutablesContent.ChunkedContent.ChunkHashes, {}, {}); + + tsl::robin_map<std::string, uint32_t> ReplaceDir1ExecutablesPathLookup = + chunkedcontent_testutils::BuildPathLookup(ReplaceDir1ExecutablesContent.Paths); + + CHECK(PathMatchesBase("file_1", ReplaceDir1ExecutablesContent, ReplaceDir1ExecutablesPathLookup)); + CHECK(PathMatchesBase("file_2.exe", ReplaceDir1ExecutablesContent, ReplaceDir1ExecutablesPathLookup)); + CHECK(PathMatchesBase("file_3.txt", ReplaceDir1ExecutablesContent, ReplaceDir1ExecutablesPathLookup)); + CHECK(PathMatchesBase("dir_1/dir1_file_3.txt", ReplaceDir1ExecutablesContent, ReplaceDir1ExecutablesPathLookup)); + + CHECK(PathMatchesOverlay("dir_1/dir1_file_1.exe", ReplaceDir1ExecutablesContent, ReplaceDir1ExecutablesPathLookup)); + CHECK(PathMatchesOverlay("dir_2/dir2_dir1/dir2_dir1_file_1.self", ReplaceDir1ExecutablesContent, ReplaceDir1ExecutablesPathLookup)); + } } #endif // ZEN_WITH_TESTS diff --git a/src/zenremotestore/include/zenremotestore/builds/buildcontent.h b/src/zenremotestore/include/zenremotestore/builds/buildcontent.h new file mode 100644 index 000000000..67aebeda3 --- /dev/null +++ b/src/zenremotestore/include/zenremotestore/builds/buildcontent.h @@ -0,0 +1,43 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zenremotestore/chunking/chunkedcontent.h> + +namespace zen { + +class CbObjectView; +class CbObjectWriter; + +void WriteBuildContentToCompactBinary(CbObjectWriter& PartManifestWriter, + const SourcePlatform Platform, + std::span<const std::filesystem::path> Paths, + std::span<const IoHash> RawHashes, + std::span<const uint64_t> RawSizes, + std::span<const uint32_t> Attributes, + std::span<const IoHash> SequenceRawHashes, + std::span<const uint32_t> ChunkCounts, + std::span<const IoHash> LocalChunkHashes, + std::span<const uint64_t> LocalChunkRawSizes, + const std::vector<uint32_t>& AbsoluteChunkOrders, + const std::span<const uint32_t> LooseLocalChunkIndexes, + const std::span<IoHash> BlockHashes); + +void ReadBuildContentFromCompactBinary(CbObjectView BuildPartManifest, + SourcePlatform& OutPlatform, + std::vector<std::filesystem::path>& OutPaths, + std::vector<IoHash>& OutRawHashes, + std::vector<uint64_t>& OutRawSizes, + std::vector<uint32_t>& OutAttributes, + std::vector<IoHash>& OutSequenceRawHashes, + std::vector<uint32_t>& OutChunkCounts, + std::vector<uint32_t>& OutAbsoluteChunkOrders, + std::vector<IoHash>& OutLooseChunkHashes, + std::vector<uint64_t>& OutLooseChunkRawSizes, + std::vector<IoHash>& OutBlockRawHashes); + +#if ZEN_WITH_TESTS +void buildsavedstate_forcelink(); +#endif // ZEN_WITH_TESTS + +} // namespace zen diff --git a/src/zenremotestore/include/zenremotestore/builds/buildsavedstate.h b/src/zenremotestore/include/zenremotestore/builds/buildsavedstate.h index a11641b0d..f808a7a3b 100644 --- a/src/zenremotestore/include/zenremotestore/builds/buildsavedstate.h +++ b/src/zenremotestore/include/zenremotestore/builds/buildsavedstate.h @@ -13,48 +13,67 @@ namespace zen { class CbObjectView; -CbObject CreateStateObject(const Oid& BuildId, - const std::vector<std::pair<Oid, std::string>>& AllBuildParts, - std::span<const ChunkedFolderContent> PartContents, - const FolderContent& LocalFolderState, - const std::filesystem::path& LocalPath); - -void ReadBuildContentFromCompactBinary(CbObjectView BuildPartManifest, - SourcePlatform& OutPlatform, - std::vector<std::filesystem::path>& OutPaths, - std::vector<IoHash>& OutRawHashes, - std::vector<uint64_t>& OutRawSizes, - std::vector<uint32_t>& OutAttributes, - std::vector<IoHash>& OutSequenceRawHashes, - std::vector<uint32_t>& OutChunkCounts, - std::vector<uint32_t>& OutAbsoluteChunkOrders, - std::vector<IoHash>& OutLooseChunkHashes, - std::vector<uint64_t>& OutLooseChunkRawSizes, - std::vector<IoHash>& OutBlockRawHashes); - -void CalculateLocalChunkOrders(const std::span<const uint32_t>& AbsoluteChunkOrders, - const std::span<const IoHash> LooseChunkHashes, - const std::span<const uint64_t> LooseChunkRawSizes, - const std::span<const ChunkBlockDescription>& BlockDescriptions, - std::vector<IoHash>& OutLocalChunkHashes, - std::vector<uint64_t>& OutLocalChunkRawSizes, - std::vector<uint32_t>& OutLocalChunkOrders); - -void ReadStateObject(CbObjectView StateView, - Oid& OutBuildId, - std::vector<Oid>& BuildPartsIds, - std::vector<std::string>& BuildPartsNames, - std::vector<ChunkedFolderContent>& OutPartContents, - FolderContent& OutLocalFolderState); - -void ReadStateFile(const std::filesystem::path& StateFilePath, FolderContent& OutLocalFolderState, ChunkedFolderContent& OutLocalContent); - -void AddDownloadedPath(const std::filesystem::path& SystemRootDir, - const Oid& BuildId, - const std::vector<std::pair<Oid, std::string>>& BuildParts, - const std::filesystem::path& StateFilePath, - const std::filesystem::path& LocalPath); +struct BuildsSelection +{ + struct BuildPart + { + Oid Id; + std::string Name; + }; + struct Build + { + Oid Id; + std::vector<BuildPart> Parts; + std::vector<std::string> IncludeWildcards; + std::vector<std::string> ExcludeWildcards; + }; + std::vector<Build> Builds; + + static void Write(const BuildsSelection& Selection, CbWriter& Output); + static BuildsSelection Read(CbObjectView& Input, std::vector<ChunkedFolderContent>* OptionalOutLegacyPartsContent); +}; + +struct BuildState +{ + BuildsSelection Selection; + ChunkedFolderContent ChunkedContent; + + static void Write(const BuildState& Selection, CbWriter& Output); + static BuildState Read(CbObjectView& Input); +}; + +struct BuildSaveState +{ + BuildState State; + FolderContent FolderState; + std::filesystem::path LocalPath; + + static void Write(const BuildSaveState& SaveState, CbWriter& Output); + static BuildSaveState Read(CbObjectView& Input); +}; + +struct BuildsDownloadInfo +{ + BuildsSelection Selection; + std::filesystem::path LocalPath; + std::filesystem::path StateFilePath; + std::string Iso8601Date; + + static void Write(const BuildsDownloadInfo& Info, CbWriter& Output); + static BuildsDownloadInfo Read(CbObjectView& Input); +}; + +CbObject CreateBuildSaveStateObject(const BuildSaveState& State); +BuildSaveState ReadBuildSaveStateFile(const std::filesystem::path& StateFilePath); + +void AddDownloadedPath(const std::filesystem::path& SystemRootDir, const BuildsDownloadInfo& Info); + +BuildsDownloadInfo ReadDownloadedInfoFile(const std::filesystem::path& DownloadInfoPath); std::vector<std::filesystem::path> GetDownloadedStatePaths(const std::filesystem::path& SystemRootDir); +#if ZEN_WITH_TESTS +void buildsavedstate_forcelink(); +#endif // ZEN_WITH_TESTS + } // namespace zen diff --git a/src/zenremotestore/include/zenremotestore/builds/buildstorageoperations.h b/src/zenremotestore/include/zenremotestore/builds/buildstorageoperations.h index eba0966f7..3c4535d9c 100644 --- a/src/zenremotestore/include/zenremotestore/builds/buildstorageoperations.h +++ b/src/zenremotestore/include/zenremotestore/builds/buildstorageoperations.h @@ -497,8 +497,9 @@ public: struct Options { - bool IsQuiet = false; - bool IsVerbose = false; + bool IsQuiet = false; + bool IsVerbose = false; + bool DoExtraContentValidation = false; const uint64_t FindBlockMaxCount = 10000; const uint8_t BlockReuseMinPercentLimit = 85; @@ -579,27 +580,6 @@ private: const tsl::robin_map<IoHash, uint32_t, IoHash::Hasher>& ChunkHashToLocalChunkIndex, const std::span<const uint32_t>& LooseChunkIndexes, const std::span<const ChunkBlockDescription>& BlockDescriptions); - void CalculateLocalChunkOrders(const std::span<const uint32_t>& AbsoluteChunkOrders, - const std::span<const IoHash> LooseChunkHashes, - const std::span<const uint64_t> LooseChunkRawSizes, - const std::span<const ChunkBlockDescription>& BlockDescriptions, - std::vector<IoHash>& OutLocalChunkHashes, - std::vector<uint64_t>& OutLocalChunkRawSizes, - std::vector<uint32_t>& OutLocalChunkOrders); - - void WriteBuildContentToCompactBinary(CbObjectWriter& PartManifestWriter, - const SourcePlatform Platform, - std::span<const std::filesystem::path> Paths, - std::span<const IoHash> RawHashes, - std::span<const uint64_t> RawSizes, - std::span<const uint32_t> Attributes, - std::span<const IoHash> SequenceRawHashes, - std::span<const uint32_t> ChunkCounts, - std::span<const IoHash> LocalChunkHashes, - std::span<const uint64_t> LocalChunkRawSizes, - const std::vector<uint32_t>& AbsoluteChunkOrders, - const std::span<const uint32_t> LooseLocalChunkIndexes, - const std::span<IoHash> BlockHashes); CompositeBuffer FetchChunk(const ChunkedFolderContent& Content, const ChunkedContentLookup& Lookup, diff --git a/src/zenremotestore/include/zenremotestore/chunking/chunkedcontent.h b/src/zenremotestore/include/zenremotestore/chunking/chunkedcontent.h index 227d877d8..e4be7923a 100644 --- a/src/zenremotestore/include/zenremotestore/chunking/chunkedcontent.h +++ b/src/zenremotestore/include/zenremotestore/chunking/chunkedcontent.h @@ -113,7 +113,7 @@ struct ChunkedContentLookup }; void SaveChunkedFolderContentToCompactBinary(const ChunkedFolderContent& Content, CbWriter& Output); -ChunkedFolderContent LoadChunkedFolderContentToCompactBinary(CbObjectView Input); +ChunkedFolderContent LoadChunkedFolderContentFromCompactBinary(CbObjectView Input); ChunkedFolderContent MergeChunkedFolderContents(const ChunkedFolderContent& Base, std::span<const ChunkedFolderContent> Overlays); ChunkedFolderContent DeletePathsFromChunkedContent(const ChunkedFolderContent& Base, @@ -123,6 +123,11 @@ ChunkedFolderContent DeletePathsFromChunkedContent(const ChunkedFolderContent& B bool CompareChunkedContent(const ChunkedFolderContent& Lhs, const ChunkedFolderContent& Rhs); +ChunkedFolderContent ApplyChunkedContentOverlay(const ChunkedFolderContent& Base, + const ChunkedFolderContent& Overlay, + std::span<const std::string> OverlayIncludeWildcards, + std::span<const std::string> OverlayExcludeWildcards); + struct ChunkingStatistics { std::atomic<uint64_t> FilesProcessed = 0; @@ -198,14 +203,44 @@ GetFirstPathIndexForRawHash(const ChunkedContentLookup& Lookup, const IoHash& Ra struct ChunkBlockDescription; +void CalculateLocalChunkOrders(const std::span<const uint32_t>& AbsoluteChunkOrders, + const std::span<const IoHash> LooseChunkHashes, + const std::span<const uint64_t> LooseChunkRawSizes, + const std::span<const ChunkBlockDescription>& BlockDescriptions, + std::vector<IoHash>& OutLocalChunkHashes, + std::vector<uint64_t>& OutLocalChunkRawSizes, + std::vector<uint32_t>& OutLocalChunkOrders, + bool DoExtraVerify); + void ValidateChunkedFolderContent(const ChunkedFolderContent& Content, std::span<const ChunkBlockDescription> BlockDescriptions, - std::span<const IoHash> LooseChunks); + std::span<const IoHash> LooseChunks, + std::span<const std::string> IncludeWildcards, + std::span<const std::string> ExcludeWildcards); void InlineRemoveUnusedHashes(std::vector<IoHash>& InOutHashes, std::span<const IoHash> UsedHashes); #if ZEN_WITH_TESTS void chunkedcontent_forcelink(); + +struct FastRandom; + +namespace chunkedcontent_testutils { + struct ChunkedFile + { + IoHash RawHash; + std::vector<IoHash> ChunkHashes; + std::vector<uint64_t> ChunkSizes; + std::vector<IoBuffer> Chunks; + }; + + ChunkedFile CreateChunkedFile(FastRandom& Random, size_t Size, size_t ChunkingSize); + ChunkedFolderContent CreateChunkedFolderContent(FastRandom& Random, + std::span<const std::pair<const std::string, uint64_t>> PathAndSizes, + uint64_t ChunkingSize, + std::vector<IoBuffer>& ChunkPayloads); +} // namespace chunkedcontent_testutils + #endif // ZEN_WITH_TESTS } // namespace zen diff --git a/src/zenremotestore/zenremotestore.cpp b/src/zenremotestore/zenremotestore.cpp index 03baf1cb8..e074455b3 100644 --- a/src/zenremotestore/zenremotestore.cpp +++ b/src/zenremotestore/zenremotestore.cpp @@ -2,6 +2,7 @@ #include <zenremotestore/zenremotestore.h> +#include <zenremotestore/builds/buildsavedstate.h> #include <zenremotestore/chunking/chunkedcontent.h> #include <zenremotestore/chunking/chunkedfile.h> #include <zenremotestore/projectstore/remoteprojectstore.h> @@ -13,9 +14,11 @@ namespace zen { void zenremotestore_forcelinktests() { + buildsavedstate_forcelink(); chunkblock_forcelink(); chunkedcontent_forcelink(); chunkedfile_forcelink(); + chunkedcontent_forcelink(); remoteprojectstore_forcelink(); } diff --git a/src/zenutil/include/zenutil/wildcard.h b/src/zenutil/include/zenutil/wildcard.h index 9f402e100..db2d4bda0 100644 --- a/src/zenutil/include/zenutil/wildcard.h +++ b/src/zenutil/include/zenutil/wildcard.h @@ -8,6 +8,11 @@ namespace zen { bool MatchWildcard(std::string_view Wildcard, std::string_view String, bool CaseSensitive); +bool IncludePath(std::span<const std::string> IncludeWildcards, + std::span<const std::string> ExcludeWildcards, + const std::string& Path, + bool CaseSensitive); + void wildcard_forcelink(); // internal } // namespace zen diff --git a/src/zenutil/wildcard.cpp b/src/zenutil/wildcard.cpp index d9d5b384f..7a44c0498 100644 --- a/src/zenutil/wildcard.cpp +++ b/src/zenutil/wildcard.cpp @@ -78,6 +78,39 @@ MatchWildcard(std::string_view Wildcard, std::string_view String, bool CaseSensi } } +bool +IncludePath(std::span<const std::string> IncludeWildcards, + std::span<const std::string> ExcludeWildcards, + const std::string& Path, + bool CaseSensitive) +{ + bool IncludePath = true; + if (!IncludeWildcards.empty()) + { + IncludePath = false; + for (const std::string& IncludeWildcard : IncludeWildcards) + { + if (MatchWildcard(IncludeWildcard, Path, CaseSensitive)) + { + IncludePath = true; + break; + } + } + if (!IncludePath) + { + return false; + } + } + for (const std::string& ExcludeWildcard : ExcludeWildcards) + { + if (MatchWildcard(ExcludeWildcard, Path, CaseSensitive)) + { + return false; + } + } + return true; +} + #if ZEN_WITH_TESTS void |