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/zen/cmds/builds_cmd.cpp | |
| parent | changelog (#661) (diff) | |
| download | archived-zen-6dcdddbf733b0aa323ffb7ecbe56c04b15c6c16a.tar.xz archived-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/zen/cmds/builds_cmd.cpp')
| -rw-r--r-- | src/zen/cmds/builds_cmd.cpp | 886 |
1 files changed, 523 insertions, 363 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, |