// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if ZEN_WITH_TESTS # include # include # include # include #endif // ZEN_WITH_TESTS namespace zen { StorageInstance::~StorageInstance() { if (CacheLogSink) { if (Ref Broadcast = GetDefaultBroadcastSink()) { Broadcast->RemoveSink(CacheLogSink); } } } void StorageInstance::SetupCacheSession(std::string_view TargetUrl, std::string_view Mode, const Oid& SessionId) { CacheSession = std::make_unique(SessionsServiceClient::Options{ .TargetUrl = std::string(TargetUrl), .AppName = "zen", .Mode = std::string(Mode), .SessionId = SessionId, }); CacheSession->Announce(); CacheLogSink = CacheSession->CreateLogSink(); GetDefaultBroadcastSink()->AddSink(CacheLogSink); } using namespace std::literals; std::vector ParseBlockMetadatas(std::span BlockMetadatas) { std::vector UnorderedList; UnorderedList.reserve(BlockMetadatas.size()); for (size_t CacheBlockMetadataIndex = 0; CacheBlockMetadataIndex < BlockMetadatas.size(); CacheBlockMetadataIndex++) { const CbObject& CacheBlockMetadata = BlockMetadatas[CacheBlockMetadataIndex]; ChunkBlockDescription Description = ParseChunkBlockDescription(CacheBlockMetadata); if (Description.BlockHash != IoHash::Zero) { UnorderedList.emplace_back(std::move(Description)); } } return UnorderedList; } std::vector GetBlockDescriptions(LoggerRef InLog, BuildStorageBase& Storage, BuildStorageCache* OptionalCacheStorage, const Oid& BuildId, std::span BlockRawHashes, bool AttemptFallback, bool IsQuiet, bool IsVerbose) { using namespace std::literals; ZEN_SCOPED_LOG(InLog); std::vector UnorderedList; tsl::robin_map BlockDescriptionLookup; if (OptionalCacheStorage && !BlockRawHashes.empty()) { std::vector CacheBlockMetadatas = OptionalCacheStorage->GetBlobMetadatas(BuildId, BlockRawHashes); if (!CacheBlockMetadatas.empty()) { UnorderedList = ParseBlockMetadatas(CacheBlockMetadatas); for (size_t DescriptionIndex = 0; DescriptionIndex < UnorderedList.size(); DescriptionIndex++) { const ChunkBlockDescription& Description = UnorderedList[DescriptionIndex]; BlockDescriptionLookup.insert_or_assign(Description.BlockHash, DescriptionIndex); } } } if (UnorderedList.size() < BlockRawHashes.size()) { std::vector RemainingBlockHashes; RemainingBlockHashes.reserve(BlockRawHashes.size() - UnorderedList.size()); for (const IoHash& BlockRawHash : BlockRawHashes) { if (!BlockDescriptionLookup.contains(BlockRawHash)) { RemainingBlockHashes.push_back(BlockRawHash); } } CbObject BlockMetadatas = Storage.GetBlockMetadatas(BuildId, RemainingBlockHashes); std::vector RemainingList; { CbArrayView BlocksArray = BlockMetadatas["blocks"sv].AsArrayView(); std::vector FoundBlockHashes; std::vector FoundBlockMetadatas; for (CbFieldView Block : BlocksArray) { ChunkBlockDescription Description = ParseChunkBlockDescription(Block.AsObjectView()); if (Description.BlockHash == IoHash::Zero) { ZEN_WARN("Unexpected/invalid block metadata received from remote store, skipping block"); } else { if (OptionalCacheStorage) { UniqueBuffer MetaBuffer = UniqueBuffer::Alloc(Block.GetSize()); Block.CopyTo(MetaBuffer.GetMutableView()); CbObject BlockMetadata(MetaBuffer.MoveToShared()); FoundBlockHashes.push_back(Description.BlockHash); FoundBlockMetadatas.push_back(BlockMetadata); } RemainingList.emplace_back(std::move(Description)); } } if (OptionalCacheStorage && !FoundBlockHashes.empty()) { OptionalCacheStorage->PutBlobMetadatas(BuildId, FoundBlockHashes, FoundBlockMetadatas); } } for (size_t DescriptionIndex = 0; DescriptionIndex < RemainingList.size(); DescriptionIndex++) { const ChunkBlockDescription& Description = RemainingList[DescriptionIndex]; BlockDescriptionLookup.insert_or_assign(Description.BlockHash, UnorderedList.size() + DescriptionIndex); } UnorderedList.insert(UnorderedList.end(), RemainingList.begin(), RemainingList.end()); } std::vector Result; Result.reserve(BlockDescriptionLookup.size()); for (const IoHash& BlockHash : BlockRawHashes) { if (auto It = BlockDescriptionLookup.find(BlockHash); It != BlockDescriptionLookup.end()) { Result.push_back(std::move(UnorderedList[It->second])); } } if (Result.size() != BlockRawHashes.size()) { std::string ErrorDescription = fmt::format("All required blocks could not be found, {} blocks does not have metadata in this context.", BlockRawHashes.size() - Result.size()); if (IsVerbose) { for (const IoHash& BlockHash : BlockRawHashes) { if (auto It = std::find_if(Result.begin(), Result.end(), [BlockHash](const ChunkBlockDescription& Description) { return Description.BlockHash == BlockHash; }); It == Result.end()) { ErrorDescription += fmt::format("\n {}", BlockHash); } } } if (AttemptFallback) { ZEN_WARN("{} Attemping fallback options.", ErrorDescription); std::vector AugmentedBlockDescriptions; AugmentedBlockDescriptions.reserve(BlockRawHashes.size()); std::vector FoundBlocks = ParseChunkBlockDescriptionList(Storage.FindBlocks(BuildId, (uint64_t)-1)); for (const IoHash& BlockHash : BlockRawHashes) { if (auto It = std::find_if(Result.begin(), Result.end(), [BlockHash](const ChunkBlockDescription& Description) { return Description.BlockHash == BlockHash; }); It != Result.end()) { AugmentedBlockDescriptions.emplace_back(std::move(*It)); } else if (auto ListBlocksIt = std::find_if( FoundBlocks.begin(), FoundBlocks.end(), [BlockHash](const ChunkBlockDescription& Description) { return Description.BlockHash == BlockHash; }); ListBlocksIt != FoundBlocks.end()) { if (!IsQuiet) { ZEN_INFO("Found block {} via context find successfully", BlockHash); } AugmentedBlockDescriptions.emplace_back(std::move(*ListBlocksIt)); } else { IoBuffer BlockBuffer = Storage.GetBuildBlob(BuildId, BlockHash); if (!BlockBuffer) { throw std::runtime_error(fmt::format("Block {} could not be found", BlockHash)); } IoHash BlockRawHash; uint64_t BlockRawSize; CompressedBuffer CompressedBlockBuffer = CompressedBuffer::FromCompressed(SharedBuffer(std::move(BlockBuffer)), BlockRawHash, BlockRawSize); if (!CompressedBlockBuffer) { throw std::runtime_error(fmt::format("Block {} is not a compressed buffer", BlockHash)); } if (BlockRawHash != BlockHash) { throw std::runtime_error(fmt::format("Block {} header has a mismatching raw hash {}", BlockHash, BlockRawHash)); } CompositeBuffer DecompressedBlockBuffer = CompressedBlockBuffer.DecompressToComposite(); if (!DecompressedBlockBuffer) { throw std::runtime_error(fmt::format("Block {} failed to decompress", BlockHash)); } ChunkBlockDescription MissingChunkDescription = GetChunkBlockDescription(DecompressedBlockBuffer.Flatten(), BlockHash); AugmentedBlockDescriptions.emplace_back(std::move(MissingChunkDescription)); } } Result.swap(AugmentedBlockDescriptions); } else { throw std::runtime_error(ErrorDescription); } } return Result; } ////////////////////// Shared helpers std::filesystem::path ZenStateFilePath(const std::filesystem::path& ZenFolderPath) { return ZenFolderPath / "current_state.cbo"; } std::filesystem::path ZenTempFolderPath(const std::filesystem::path& ZenFolderPath) { return ZenFolderPath / "tmp"; } CbObject GetBuild(BuildStorageBase& Storage, const Oid& BuildId, bool IsQuiet) { 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; } uint64_t GetMaxMemoryBufferSize(size_t MaxBlockSize, bool BoostWorkerMemory) { return BoostWorkerMemory ? (MaxBlockSize + 16u * 1024u) : 1024u * 1024u; } void DownloadLargeBlob(BuildStorageBase& Storage, const std::filesystem::path& DownloadFolder, const Oid& BuildId, const IoHash& ChunkHash, const std::uint64_t PreferredMultipartChunkSize, ParallelWork& Work, WorkerThreadPool& NetworkPool, std::atomic& DownloadedChunkByteCount, std::atomic& MultipartAttachmentCount, std::function&& OnDownloadComplete) { ZEN_TRACE_CPU("DownloadLargeBlob"); struct WorkloadData { TemporaryFile TempFile; }; std::shared_ptr Workload(std::make_shared()); std::error_code Ec; Workload->TempFile.CreateTemporary(DownloadFolder, Ec); if (Ec) { throw std::runtime_error( fmt::format("Failed opening temporary file '{}', reason: ({}) {}", Workload->TempFile.GetPath(), Ec.message(), Ec.value())); } std::vector> WorkItems = Storage.GetLargeBuildBlob( BuildId, ChunkHash, PreferredMultipartChunkSize, [&Work, Workload, &DownloadedChunkByteCount](uint64_t Offset, const IoBuffer& Chunk) { DownloadedChunkByteCount += Chunk.GetSize(); if (!Work.IsAborted()) { ZEN_TRACE_CPU("Async_DownloadLargeBlob_OnReceive"); Workload->TempFile.Write(Chunk.GetView(), Offset); } }, [&Work, Workload, OnDownloadComplete = std::move(OnDownloadComplete)]() { if (!Work.IsAborted()) { ZEN_TRACE_CPU("Async_DownloadLargeBlob_OnComplete"); uint64_t PayloadSize = Workload->TempFile.FileSize(); void* FileHandle = Workload->TempFile.Detach(); ZEN_ASSERT(FileHandle != nullptr); IoBuffer Payload(IoBuffer::File, FileHandle, 0, PayloadSize, true); Payload.SetDeleteOnClose(true); OnDownloadComplete(std::move(Payload)); } }); if (!WorkItems.empty()) { MultipartAttachmentCount++; } for (auto& WorkItem : WorkItems) { Work.ScheduleWork(NetworkPool, [WorkItem = std::move(WorkItem)](std::atomic& AbortFlag) { if (!AbortFlag) { ZEN_TRACE_CPU("Async_DownloadLargeBlob_Work"); WorkItem(); } }); } } CompositeBuffer ValidateBlob(std::atomic& AbortFlag, IoBuffer&& Payload, const IoHash& BlobHash, uint64_t& OutCompressedSize, uint64_t& OutDecompressedSize) { ZEN_TRACE_CPU("ValidateBlob"); if (Payload.GetContentType() != ZenContentType::kCompressedBinary) { throw std::runtime_error(fmt::format("Blob {} ({} bytes) has unexpected content type '{}'", BlobHash, Payload.GetSize(), ToString(Payload.GetContentType()))); } IoHash RawHash; uint64_t RawSize; CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Payload), RawHash, RawSize); if (!Compressed) { throw std::runtime_error(fmt::format("Blob {} ({} bytes) compressed header is invalid", BlobHash, Payload.GetSize())); } if (RawHash != BlobHash) { throw std::runtime_error( fmt::format("Blob {} ({} bytes) compressed header has a mismatching raw hash {}", BlobHash, Payload.GetSize(), RawHash)); } IoHashStream Hash; bool CouldDecompress = Compressed.DecompressToStream( 0, RawSize, [&AbortFlag, &Hash](uint64_t SourceOffset, uint64_t SourceSize, uint64_t Offset, const CompositeBuffer& RangeBuffer) { ZEN_UNUSED(SourceOffset, SourceSize, Offset); if (!AbortFlag) { for (const SharedBuffer& Segment : RangeBuffer.GetSegments()) { Hash.Append(Segment.GetView()); } return true; } return false; }); if (AbortFlag) { return CompositeBuffer{}; } if (!CouldDecompress) { throw std::runtime_error( fmt::format("Blob {} ({} bytes) failed to decompress - header information mismatch", BlobHash, Payload.GetSize())); } IoHash ValidateRawHash = Hash.GetHash(); if (ValidateRawHash != BlobHash) { throw std::runtime_error(fmt::format("Blob {} ({} bytes) decompressed hash {} does not match header information", BlobHash, Payload.GetSize(), ValidateRawHash)); } OodleCompressor Compressor; OodleCompressionLevel CompressionLevel; uint64_t BlockSize; if (!Compressed.TryGetCompressParameters(Compressor, CompressionLevel, BlockSize)) { throw std::runtime_error(fmt::format("Blob {} ({} bytes) failed to get compression details", BlobHash, Payload.GetSize())); } OutCompressedSize = Payload.GetSize(); OutDecompressedSize = RawSize; if (CompressionLevel == OodleCompressionLevel::None) { // Only decompress to composite if we need it for block verification CompositeBuffer DecompressedComposite = Compressed.DecompressToComposite(); if (!DecompressedComposite) { throw std::runtime_error(fmt::format("Blob {} ({} bytes) failed to decompress to composite", BlobHash, Payload.GetSize())); } return DecompressedComposite; } return CompositeBuffer{}; } CompositeBuffer ValidateBlob(std::atomic& AbortFlag, BuildStorageBase& Storage, const Oid& BuildId, const IoHash& BlobHash, uint64_t& OutCompressedSize, uint64_t& OutDecompressedSize) { ZEN_TRACE_CPU("ValidateBlob"); IoBuffer Payload = Storage.GetBuildBlob(BuildId, BlobHash); if (!Payload) { throw std::runtime_error(fmt::format("Blob {} could not be found", BlobHash)); } return ValidateBlob(AbortFlag, std::move(Payload), BlobHash, OutCompressedSize, OutDecompressedSize); } std::vector> ResolveBuildPartNames(CbObjectView BuildObject, const Oid& BuildId, const std::vector& BuildPartIds, std::span BuildPartNames, std::uint64_t& OutPreferredMultipartChunkSize) { std::vector> Result; { CbObjectView PartsObject = BuildObject["parts"sv].AsObjectView(); if (!PartsObject) { throw std::runtime_error("Build object does not have a 'parts' object"); } OutPreferredMultipartChunkSize = BuildObject["chunkSize"sv].AsUInt64(OutPreferredMultipartChunkSize); std::vector> AvailableParts; for (CbFieldView PartView : PartsObject) { const std::string BuildPartName = std::string(PartView.GetName()); const Oid BuildPartId = PartView.AsObjectId(); if (BuildPartId == Oid::Zero) { ExtendableStringBuilder<128> SB; for (CbFieldView ScanPartView : PartsObject) { SB.Append(fmt::format("\n {}: {}", ScanPartView.GetName(), ScanPartView.AsObjectId())); } throw std::runtime_error(fmt::format("Build object parts does not have a '{}' object id{}", BuildPartName, SB.ToView())); } AvailableParts.push_back({BuildPartId, BuildPartName}); } if (BuildPartIds.empty() && BuildPartNames.empty()) { Result = AvailableParts; } else { for (const std::string& BuildPartName : BuildPartNames) { if (auto It = std::find_if(AvailableParts.begin(), AvailableParts.end(), [&BuildPartName](const auto& Part) { return Part.second == BuildPartName; }); It != AvailableParts.end()) { Result.push_back(*It); } else { throw std::runtime_error(fmt::format("Build {} object does not have a part named '{}'", BuildId, BuildPartName)); } } for (const Oid& BuildPartId : BuildPartIds) { if (auto It = std::find_if(AvailableParts.begin(), AvailableParts.end(), [&BuildPartId](const auto& Part) { return Part.first == BuildPartId; }); It != AvailableParts.end()) { Result.push_back(*It); } else { throw std::runtime_error(fmt::format("Build {} object does not have a part with id '{}'", BuildId, BuildPartId)); } } } if (Result.empty()) { throw std::runtime_error(fmt::format("Build object does not have any parts", BuildId)); } } return Result; } void ValidatePartSelection(std::vector& BuildPartIds, std::vector& BuildPartNames, std::string_view HelpText) { const bool HasWildcard = std::find(BuildPartNames.begin(), BuildPartNames.end(), "*") != BuildPartNames.end(); if (HasWildcard) { if (BuildPartNames.size() != 1 || !BuildPartIds.empty()) { throw OptionParseException("'*' cannot be combined with other part names or ids", std::string(HelpText)); } BuildPartNames.clear(); return; } } ChunkedFolderContent GetRemoteContent(LoggerRef InLog, StorageInstance& Storage, const Oid& BuildId, const std::vector>& BuildParts, const BuildManifest& Manifest, std::span IncludeWildcards, std::span ExcludeWildcards, std::unique_ptr& OutChunkController, std::vector& OutPartContents, std::vector& OutBlockDescriptions, std::vector& OutLooseChunkHashes, bool IsQuiet, bool IsVerbose, bool DoExtraContentVerify) { ZEN_TRACE_CPU("GetRemoteContent"); ZEN_SCOPED_LOG(InLog); Stopwatch GetBuildPartTimer; const Oid BuildPartId = BuildParts[0].first; const std::string_view BuildPartName = BuildParts[0].second; CbObject BuildPartManifest = Storage.BuildStorage->GetBuildPart(BuildId, BuildPartId); if (!IsQuiet) { ZEN_INFO("GetBuildPart {} ('{}') took {}. Payload size: {}", BuildPartId, BuildPartName, NiceTimeSpanMs(GetBuildPartTimer.GetElapsedTimeMs()), NiceBytes(BuildPartManifest.GetSize())); ZEN_INFO("{}", GetCbObjectAsNiceString(BuildPartManifest, " "sv, "\n"sv)); } { CbObjectView Chunker = BuildPartManifest["chunker"sv].AsObjectView(); std::string_view ChunkerName = Chunker["name"sv].AsString(); CbObjectView Parameters = Chunker["parameters"sv].AsObjectView(); OutChunkController = CreateChunkingController(ChunkerName, Parameters); } auto ParseBuildPartManifest = [&Log, IsQuiet, IsVerbose, DoExtraContentVerify](StorageInstance& Storage, const Oid& BuildId, const Oid& BuildPartId, CbObject BuildPartManifest, std::span IncludeWildcards, std::span ExcludeWildcards, const BuildManifest::Part* OptionalManifest, ChunkedFolderContent& OutRemoteContent, std::vector& OutBlockDescriptions, std::vector& OutLooseChunkHashes) { std::vector AbsoluteChunkOrders; std::vector LooseChunkRawSizes; std::vector BlockRawHashes; ReadBuildContentFromCompactBinary(BuildPartManifest, OutRemoteContent.Platform, OutRemoteContent.Paths, OutRemoteContent.RawHashes, OutRemoteContent.RawSizes, OutRemoteContent.Attributes, OutRemoteContent.ChunkedContent.SequenceRawHashes, OutRemoteContent.ChunkedContent.ChunkCounts, AbsoluteChunkOrders, OutLooseChunkHashes, LooseChunkRawSizes, BlockRawHashes); // TODO: GetBlockDescriptions for all BlockRawHashes in one go - check for local block descriptions when we cache them { if (!IsQuiet) { ZEN_INFO("Fetching metadata for {} blocks", BlockRawHashes.size()); } Stopwatch GetBlockMetadataTimer; bool AttemptFallback = false; OutBlockDescriptions = GetBlockDescriptions(Log(), *Storage.BuildStorage, Storage.CacheStorage.get(), BuildId, BlockRawHashes, AttemptFallback, IsQuiet, IsVerbose); if (!IsQuiet) { ZEN_INFO("GetBlockMetadata for {} took {}. Found {} blocks", BuildPartId, NiceTimeSpanMs(GetBlockMetadataTimer.GetElapsedTimeMs()), OutBlockDescriptions.size()); } } CalculateLocalChunkOrders(AbsoluteChunkOrders, OutLooseChunkHashes, LooseChunkRawSizes, OutBlockDescriptions, OutRemoteContent.ChunkedContent.ChunkHashes, OutRemoteContent.ChunkedContent.ChunkRawSizes, OutRemoteContent.ChunkedContent.ChunkOrders, DoExtraContentVerify); std::vector DeletedPaths; if (OptionalManifest) { tsl::robin_set PathsInManifest; PathsInManifest.reserve(OptionalManifest->Files.size()); for (const std::filesystem::path& ManifestPath : OptionalManifest->Files) { PathsInManifest.insert(ToLower(ManifestPath.generic_string())); } for (const std::filesystem::path& RemotePath : OutRemoteContent.Paths) { if (!PathsInManifest.contains(ToLower(RemotePath.generic_string()))) { DeletedPaths.push_back(RemotePath); } } } if (!IncludeWildcards.empty() || !ExcludeWildcards.empty()) { for (const std::filesystem::path& RemotePath : OutRemoteContent.Paths) { if (!IncludePath(IncludeWildcards, ExcludeWildcards, ToLower(RemotePath.generic_string()), /*CaseSensitive*/ true)) { DeletedPaths.push_back(RemotePath); } } } if (!DeletedPaths.empty()) { OutRemoteContent = DeletePathsFromChunkedContent(OutRemoteContent, DeletedPaths); InlineRemoveUnusedHashes(OutLooseChunkHashes, OutRemoteContent.ChunkedContent.ChunkHashes); } #if ZEN_BUILD_DEBUG ValidateChunkedFolderContent(OutRemoteContent, OutBlockDescriptions, OutLooseChunkHashes, IncludeWildcards, ExcludeWildcards); #endif // ZEN_BUILD_DEBUG }; auto FindManifest = [&Manifest](const Oid& BuildPartId, std::string_view BuildPartName) -> const BuildManifest::Part* { if (Manifest.Parts.empty()) { return nullptr; } if (Manifest.Parts.size() == 1) { if (Manifest.Parts[0].PartId == Oid::Zero && Manifest.Parts[0].PartName.empty()) { return &Manifest.Parts[0]; } } auto It = std::find_if(Manifest.Parts.begin(), Manifest.Parts.end(), [BuildPartId, BuildPartName](const BuildManifest::Part& Part) { if (Part.PartId != Oid::Zero) { return Part.PartId == BuildPartId; } if (!Part.PartName.empty()) { return Part.PartName == BuildPartName; } return false; }); if (It != Manifest.Parts.end()) { return &(*It); } return nullptr; }; OutPartContents.resize(1); ParseBuildPartManifest(Storage, BuildId, BuildPartId, BuildPartManifest, IncludeWildcards, ExcludeWildcards, FindManifest(BuildPartId, BuildPartName), OutPartContents[0], OutBlockDescriptions, OutLooseChunkHashes); ChunkedFolderContent RemoteContent; if (BuildParts.size() > 1) { std::vector OverlayBlockDescriptions; std::vector OverlayLooseChunkHashes; for (size_t PartIndex = 1; PartIndex < BuildParts.size(); PartIndex++) { const Oid& OverlayBuildPartId = BuildParts[PartIndex].first; const std::string& OverlayBuildPartName = BuildParts[PartIndex].second; Stopwatch GetOverlayBuildPartTimer; CbObject OverlayBuildPartManifest = Storage.BuildStorage->GetBuildPart(BuildId, OverlayBuildPartId); if (!IsQuiet) { ZEN_INFO("GetBuildPart {} ('{}') took {}. Payload size: {}", OverlayBuildPartId, OverlayBuildPartName, NiceTimeSpanMs(GetOverlayBuildPartTimer.GetElapsedTimeMs()), NiceBytes(OverlayBuildPartManifest.GetSize())); } ChunkedFolderContent OverlayPartContent; std::vector OverlayPartBlockDescriptions; std::vector OverlayPartLooseChunkHashes; ParseBuildPartManifest(Storage, BuildId, OverlayBuildPartId, OverlayBuildPartManifest, IncludeWildcards, ExcludeWildcards, FindManifest(OverlayBuildPartId, OverlayBuildPartName), OverlayPartContent, OverlayPartBlockDescriptions, OverlayPartLooseChunkHashes); OutPartContents.push_back(OverlayPartContent); OverlayBlockDescriptions.insert(OverlayBlockDescriptions.end(), OverlayPartBlockDescriptions.begin(), OverlayPartBlockDescriptions.end()); OverlayLooseChunkHashes.insert(OverlayLooseChunkHashes.end(), OverlayPartLooseChunkHashes.begin(), OverlayPartLooseChunkHashes.end()); } RemoteContent = MergeChunkedFolderContents(OutPartContents[0], std::span(OutPartContents).subspan(1)); { tsl::robin_set AllBlockHashes; for (const ChunkBlockDescription& Description : OutBlockDescriptions) { AllBlockHashes.insert(Description.BlockHash); } for (const ChunkBlockDescription& Description : OverlayBlockDescriptions) { if (!AllBlockHashes.contains(Description.BlockHash)) { AllBlockHashes.insert(Description.BlockHash); OutBlockDescriptions.push_back(Description); } } } { tsl::robin_set AllLooseChunkHashes(OutLooseChunkHashes.begin(), OutLooseChunkHashes.end()); for (const IoHash& OverlayLooseChunkHash : OverlayLooseChunkHashes) { if (!AllLooseChunkHashes.contains(OverlayLooseChunkHash)) { AllLooseChunkHashes.insert(OverlayLooseChunkHash); OutLooseChunkHashes.push_back(OverlayLooseChunkHash); } } } } else { RemoteContent = OutPartContents[0]; } return RemoteContent; } std::string GetCbObjectAsNiceString(CbObjectView Object, std::string_view Prefix, std::string_view Suffix) { ExtendableStringBuilder<512> SB; std::vector> NameStringValuePairs; for (CbFieldView Field : Object) { std::string_view Name = Field.GetName(); switch (CbValue Accessor = Field.GetValue(); Accessor.GetType()) { case CbFieldType::String: NameStringValuePairs.push_back({std::string(Name), std::string(Accessor.AsString())}); break; case CbFieldType::IntegerPositive: NameStringValuePairs.push_back({std::string(Name), fmt::format("{}", Accessor.AsIntegerPositive())}); break; case CbFieldType::IntegerNegative: NameStringValuePairs.push_back({std::string(Name), fmt::format("{}", Accessor.AsIntegerNegative())}); break; case CbFieldType::Float32: { const float Value = Accessor.AsFloat32(); if (std::isfinite(Value)) { NameStringValuePairs.push_back({std::string(Name), fmt::format("{:.9g}", Value)}); } else { NameStringValuePairs.push_back({std::string(Name), "null"}); } } break; case CbFieldType::Float64: { const double Value = Accessor.AsFloat64(); if (std::isfinite(Value)) { NameStringValuePairs.push_back({std::string(Name), fmt::format("{:.17g}", Value)}); } else { NameStringValuePairs.push_back({std::string(Name), "null"}); } } break; case CbFieldType::BoolFalse: NameStringValuePairs.push_back({std::string(Name), "false"}); break; case CbFieldType::BoolTrue: NameStringValuePairs.push_back({std::string(Name), "true"}); break; case CbFieldType::Hash: { NameStringValuePairs.push_back({std::string(Name), Accessor.AsHash().ToHexString()}); } break; case CbFieldType::Uuid: { StringBuilder Builder; Accessor.AsUuid().ToString(Builder); NameStringValuePairs.push_back({std::string(Name), Builder.ToString()}); } break; case CbFieldType::DateTime: { ExtendableStringBuilder<64> Builder; Builder << DateTime(Accessor.AsDateTimeTicks()).ToIso8601(); NameStringValuePairs.push_back({std::string(Name), Builder.ToString()}); } break; case CbFieldType::TimeSpan: { ExtendableStringBuilder<64> Builder; const TimeSpan Span(Accessor.AsTimeSpanTicks()); if (Span.GetDays() == 0) { Builder << Span.ToString("%h:%m:%s.%n"); } else { Builder << Span.ToString("%d.%h:%m:%s.%n"); } NameStringValuePairs.push_back({std::string(Name), Builder.ToString()}); break; } case CbFieldType::ObjectId: NameStringValuePairs.push_back({std::string(Name), Accessor.AsObjectId().ToString()}); break; } } std::string::size_type LongestKey = 0; for (const std::pair& KeyValue : NameStringValuePairs) { LongestKey = Max(KeyValue.first.length(), LongestKey); } for (const std::pair& KeyValue : NameStringValuePairs) { SB.Append(fmt::format("{}{:<{}}: {}{}", Prefix, KeyValue.first, LongestKey, KeyValue.second, Suffix)); } return SB.ToString(); } #if ZEN_WITH_TESTS namespace buildstorageoperations_testutils { struct TestState { TestState(const std::filesystem::path& InRootPath) : RootPath(InRootPath) , LogOutput(CreateStandardProgress(Log)) , ChunkController(CreateStandardChunkingController(StandardChunkingControllerSettings{})) , ChunkCache(CreateMemoryChunkingCache()) , WorkerPool(2) , NetworkPool(2) { } void Initialize() { StoragePath = RootPath / "storage"; TempPath = RootPath / "temp"; SystemRootDir = RootPath / "sysroot"; ZenFolderPath = RootPath / ".zen"; CreateDirectories(TempPath); CreateDirectories(StoragePath); Storage.BuildStorage = CreateFileBuildStorage(StoragePath, StorageStats, false); } void CreateSourceData(const std::filesystem::path& Source, std::span Paths, std::span Sizes) { const std::filesystem::path SourcePath = RootPath / Source; CreateDirectories(SourcePath); for (size_t FileIndex = 0; FileIndex < Paths.size(); FileIndex++) { const std::string& FilePath = Paths[FileIndex]; const uint64_t FileSize = Sizes[FileIndex]; IoBuffer FileData = FileSize > 0 ? CreateSemiRandomBlob(FileSize) : IoBuffer{}; WriteFile(SourcePath / FilePath, FileData); } } std::vector> Upload(const Oid& BuildId, const Oid& BuildPartId, const std::string_view BuildPartName, const std::filesystem::path& Source, const std::filesystem::path& ManifestPath) { const std::filesystem::path SourcePath = RootPath / Source; CbObject MetaData; BuildsOperationUploadFolder Upload(Log, *LogOutput, Storage, AbortFlag, PauseFlag, WorkerPool, NetworkPool, BuildId, SourcePath, true, MetaData, BuildsOperationUploadFolder::Options{.TempDir = TempPath}); return Upload.Execute(BuildPartId, BuildPartName, ManifestPath, *ChunkController, *ChunkCache); } void ValidateUpload(const Oid& BuildId, const std::vector>& Parts) { for (auto Part : Parts) { BuildsOperationValidateBuildPart Validate(Log, *LogOutput, *Storage.BuildStorage, AbortFlag, PauseFlag, WorkerPool, NetworkPool, BuildId, Part.first, Part.second, BuildsOperationValidateBuildPart::Options{.TempFolder = TempPath / "validate"}); Validate.Execute(); } } FolderContent Download(const Oid& BuildId, const Oid& BuildPartId, const std::string_view BuildPartName, const std::filesystem::path& Target, bool Append) { const std::filesystem::path TargetPath = RootPath / Target; CreateDirectories(TargetPath); uint64_t PreferredMultipartChunkSize = 32u * 1024u * 1024u; CbObject BuildObject = Storage.BuildStorage->GetBuild(BuildId); std::vector PartIds; if (BuildPartId != Oid::Zero) { PartIds.push_back(BuildPartId); } std::vector PartNames; if (!BuildPartName.empty()) { PartNames.push_back(std::string(BuildPartName)); } std::vector> AllBuildParts = ResolveBuildPartNames(BuildObject, BuildId, PartIds, PartNames, PreferredMultipartChunkSize); std::vector PartContents; std::vector BlockDescriptions; std::vector LooseChunkHashes; ChunkedFolderContent RemoteContent = GetRemoteContent(Log, Storage, BuildId, AllBuildParts, {}, {}, {}, ChunkController, PartContents, BlockDescriptions, LooseChunkHashes, /*IsQuiet*/ false, /*IsVerbose*/ false, /*DoExtraContentVerify*/ true); GetFolderContentStatistics LocalFolderScanStats; struct ContentVisitor : public GetDirectoryContentVisitor { virtual void AsyncVisitDirectory(const std::filesystem::path& RelativeRoot, DirectoryContent&& Content) { RwLock::ExclusiveLockScope _(ExistingPathsLock); for (const std::filesystem::path& FileName : Content.FileNames) { if (RelativeRoot.empty()) { ExistingPaths.push_back(FileName); } else { ExistingPaths.push_back(RelativeRoot / FileName); } } } RwLock ExistingPathsLock; std::vector ExistingPaths; } Visitor; Latch PendingWorkCount(1); GetDirectoryContent(TargetPath, DirectoryContentFlags::IncludeFiles | DirectoryContentFlags::Recursive, Visitor, WorkerPool, PendingWorkCount); PendingWorkCount.CountDown(); PendingWorkCount.Wait(); FolderContent CurrentLocalFolderState = GetValidFolderContent( WorkerPool, LocalFolderScanStats, TargetPath, Visitor.ExistingPaths, [](uint64_t PathCount, uint64_t CompletedPathCount) { ZEN_UNUSED(PathCount, CompletedPathCount); }, 1000, AbortFlag, PauseFlag); ChunkingStatistics LocalChunkingStats; ChunkedFolderContent LocalContent = ChunkFolderContent( LocalChunkingStats, WorkerPool, TargetPath, CurrentLocalFolderState, *ChunkController, *ChunkCache, 1000, [&](bool IsAborted, bool IsPaused, std::ptrdiff_t) { ZEN_UNUSED(IsAborted, IsPaused); }, AbortFlag, PauseFlag); if (Append) { RemoteContent = ApplyChunkedContentOverlay(LocalContent, RemoteContent, {}, {}); } const ChunkedContentLookup LocalLookup = BuildChunkedContentLookup(LocalContent); const ChunkedContentLookup RemoteLookup = BuildChunkedContentLookup(RemoteContent); BuildsOperationUpdateFolder Download(Log, *LogOutput, Storage, AbortFlag, PauseFlag, WorkerPool, NetworkPool, BuildId, TargetPath, LocalContent, LocalLookup, RemoteContent, RemoteLookup, BlockDescriptions, LooseChunkHashes, BuildsOperationUpdateFolder::Options{.SystemRootDir = SystemRootDir, .ZenFolderPath = ZenFolderPath, .ValidateCompletedSequences = true}); FolderContent ResultingState; Download.Execute(ResultingState); return ResultingState; } void ValidateDownload(std::span Paths, std::span Sizes, const std::filesystem::path& Source, const std::filesystem::path& Target, const FolderContent& DownloadContent) { const std::filesystem::path SourcePath = RootPath / Source; const std::filesystem::path TargetPath = RootPath / Target; CHECK_EQ(Paths.size(), DownloadContent.Paths.size()); tsl::robin_map ExpectedSizes; tsl::robin_map ExpectedHashes; for (size_t Index = 0; Index < Paths.size(); Index++) { const std::string LookupString = std::filesystem::path(Paths[Index]).generic_string(); ExpectedSizes.insert_or_assign(LookupString, Sizes[Index]); std::filesystem::path FilePath = SourcePath / Paths[Index]; const IoHash SourceHash = IoHash::HashBuffer(IoBufferBuilder::MakeFromFile(FilePath.make_preferred())); ExpectedHashes.insert_or_assign(LookupString, SourceHash); } for (size_t Index = 0; Index < DownloadContent.Paths.size(); Index++) { const std::string LookupString = std::filesystem::path(DownloadContent.Paths[Index]).generic_string(); auto SizeIt = ExpectedSizes.find(LookupString); CHECK_NE(SizeIt, ExpectedSizes.end()); CHECK_EQ(SizeIt->second, DownloadContent.RawSizes[Index]); std::filesystem::path FilePath = TargetPath / DownloadContent.Paths[Index]; const IoHash DownloadedHash = IoHash::HashBuffer(IoBufferBuilder::MakeFromFile(FilePath.make_preferred())); auto HashIt = ExpectedHashes.find(LookupString); CHECK_NE(HashIt, ExpectedHashes.end()); CHECK_EQ(HashIt->second, DownloadedHash); } } const std::filesystem::path RootPath; std::filesystem::path StoragePath; std::filesystem::path TempPath; std::filesystem::path SystemRootDir; std::filesystem::path ZenFolderPath; LoggerRef Log = ConsoleLog(); std::unique_ptr LogOutput; std::unique_ptr ChunkController; std::unique_ptr ChunkCache; StorageInstance Storage; BuildStorageBase::Statistics StorageStats; WorkerThreadPool WorkerPool; WorkerThreadPool NetworkPool; std::atomic AbortFlag; std::atomic PauseFlag; }; } // namespace buildstorageoperations_testutils TEST_SUITE_BEGIN("remotestore.buildstorageutil"); TEST_CASE("validatepartselection.empty_unchanged") { std::vector Ids; std::vector Names; ValidatePartSelection(Ids, Names, {}); CHECK(Ids.empty()); CHECK(Names.empty()); } TEST_CASE("validatepartselection.wildcard_alone_clears_names") { std::vector Ids; std::vector Names = {"*"}; ValidatePartSelection(Ids, Names, {}); CHECK(Ids.empty()); CHECK(Names.empty()); } TEST_CASE("validatepartselection.wildcard_with_other_name_throws") { std::vector Ids; std::vector Names = {"*", "foo"}; CHECK_THROWS_AS(ValidatePartSelection(Ids, Names, {}), OptionParseException); } TEST_CASE("validatepartselection.wildcard_with_ids_throws") { std::vector Ids = {Oid::NewOid()}; std::vector Names = {"*"}; CHECK_THROWS_AS(ValidatePartSelection(Ids, Names, {}), OptionParseException); } TEST_CASE("validatepartselection.explicit_name_unchanged") { std::vector Ids; std::vector Names = {"foo"}; ValidatePartSelection(Ids, Names, {}); CHECK(Ids.empty()); REQUIRE_EQ(Names.size(), 1u); CHECK_EQ(Names[0], "foo"); } TEST_CASE("validatepartselection.ids_only_unchanged") { const Oid Id = Oid::NewOid(); std::vector Ids = {Id}; std::vector Names; ValidatePartSelection(Ids, Names, {}); REQUIRE_EQ(Ids.size(), 1u); CHECK_EQ(Ids[0], Id); CHECK(Names.empty()); } TEST_CASE("buildstorageoperations.upload.folder") { using namespace buildstorageoperations_testutils; FastRandom BaseRandom; const size_t FileCount = 11; const std::string Paths[FileCount] = {{"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 Sizes[FileCount] = {6u * 1024u, 0, 798, 19u * 1024u, 7u * 1024u, 93, 31u * 1024u, 17u * 1024u, 13u * 1024u, 2u * 1024u, 3u * 1024u}; ScopedTemporaryDirectory SourceFolder; TestState State(SourceFolder.Path()); State.Initialize(); State.CreateSourceData("source", Paths, Sizes); const Oid BuildId = Oid::NewOid(); const Oid BuildPartId = Oid::NewOid(); const std::string BuildPartName = "default"; auto Result = State.Upload(BuildId, BuildPartId, BuildPartName, "source", {}); CHECK_EQ(Result.size(), 1u); CHECK_EQ(Result[0].first, BuildPartId); CHECK_EQ(Result[0].second, BuildPartName); State.ValidateUpload(BuildId, Result); FolderContent DownloadContent = State.Download(BuildId, Oid::Zero, {}, "download", /* Append */ false); CHECK_EQ(DownloadContent.Paths.size(), FileCount); State.ValidateDownload(Paths, Sizes, "source", "download", DownloadContent); } TEST_CASE("buildstorageoperations.upload.manifest") { using namespace buildstorageoperations_testutils; FastRandom BaseRandom; const size_t FileCount = 11; const std::string Paths[FileCount] = {{"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 Sizes[FileCount] = {6u * 1024u, 0, 798, 19u * 1024u, 7u * 1024u, 93, 31u * 1024u, 17u * 1024u, 13u * 1024u, 2u * 1024u, 3u * 1024u}; ScopedTemporaryDirectory SourceFolder; TestState State(SourceFolder.Path()); State.Initialize(); State.CreateSourceData("source", Paths, Sizes); std::span ManifestFiles(Paths); ManifestFiles = ManifestFiles.subspan(0, FileCount / 2); std::span ManifestSizes(Sizes); ManifestSizes = ManifestSizes.subspan(0, FileCount / 2); ExtendableStringBuilder<1024> Manifest; for (const std::string& FilePath : ManifestFiles) { Manifest << FilePath << "\n"; } WriteFile(State.RootPath / "manifest.txt", IoBuffer(IoBuffer::Wrap, Manifest.Data(), Manifest.Size())); const Oid BuildId = Oid::NewOid(); const Oid BuildPartId = Oid::NewOid(); const std::string BuildPartName = "default"; auto Result = State.Upload(BuildId, BuildPartId, BuildPartName, "source", State.RootPath / "manifest.txt"); CHECK_EQ(Result.size(), 1u); CHECK_EQ(Result[0].first, BuildPartId); CHECK_EQ(Result[0].second, BuildPartName); State.ValidateUpload(BuildId, Result); FolderContent DownloadContent = State.Download(BuildId, Oid::Zero, {}, "download", /* Append */ false); State.ValidateDownload(ManifestFiles, ManifestSizes, "source", "download", DownloadContent); } TEST_CASE("buildstorageoperations.memorychunkingcache") { using namespace buildstorageoperations_testutils; FastRandom BaseRandom; const size_t FileCount = 11; const std::string Paths[FileCount] = {{"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 Sizes[FileCount] = {6u * 1024u, 0, 798, 19u * 1024u, 7u * 1024u, 93, 31u * 1024u, 17u * 1024u, 13u * 1024u, 2u * 1024u, 3u * 1024u}; ScopedTemporaryDirectory SourceFolder; TestState State(SourceFolder.Path()); State.Initialize(); State.CreateSourceData("source", Paths, Sizes); const Oid BuildId = Oid::NewOid(); const Oid BuildPartId = Oid::NewOid(); const std::string BuildPartName = "default"; { const std::filesystem::path SourcePath = SourceFolder.Path() / "source"; CbObject MetaData; BuildsOperationUploadFolder Upload(State.Log, *State.LogOutput, State.Storage, State.AbortFlag, State.PauseFlag, State.WorkerPool, State.NetworkPool, BuildId, SourcePath, true, MetaData, BuildsOperationUploadFolder::Options{.TempDir = State.TempPath}); auto Result = Upload.Execute(BuildPartId, BuildPartName, {}, *State.ChunkController, *State.ChunkCache); CHECK_EQ(Upload.m_ChunkingStats.FilesStoredInCache.load(), FileCount - 1); // Zero size files are not stored in cache CHECK_EQ(Upload.m_ChunkingStats.BytesStoredInCache.load(), std::accumulate(&Sizes[0], &Sizes[FileCount], uint64_t(0))); CHECK(Upload.m_ChunkingStats.ChunksStoredInCache.load() >= FileCount - 1); // Zero size files are not stored in cache CHECK_EQ(Result.size(), 1u); CHECK_EQ(Result[0].first, BuildPartId); CHECK_EQ(Result[0].second, BuildPartName); } auto Result = State.Upload(BuildId, BuildPartId, BuildPartName, "source", {}); const Oid BuildId2 = Oid::NewOid(); const Oid BuildPartId2 = Oid::NewOid(); { const std::filesystem::path SourcePath = SourceFolder.Path() / "source"; CbObject MetaData; BuildsOperationUploadFolder Upload(State.Log, *State.LogOutput, State.Storage, State.AbortFlag, State.PauseFlag, State.WorkerPool, State.NetworkPool, BuildId2, SourcePath, true, MetaData, BuildsOperationUploadFolder::Options{.TempDir = State.TempPath}); Upload.Execute(BuildPartId2, BuildPartName, {}, *State.ChunkController, *State.ChunkCache); CHECK_EQ(Upload.m_ChunkingStats.FilesFoundInCache.load(), FileCount - 1); // Zero size files are not stored in cache CHECK_EQ(Upload.m_ChunkingStats.BytesFoundInCache.load(), std::accumulate(&Sizes[0], &Sizes[FileCount], uint64_t(0))); CHECK(Upload.m_ChunkingStats.ChunksFoundInCache.load() >= FileCount - 1); // Zero size files are not stored in cache } FolderContent DownloadContent = State.Download(BuildId2, BuildPartId2, {}, "download", /* Append */ false); State.ValidateDownload(Paths, Sizes, "source", "download", DownloadContent); } TEST_CASE("buildstorageoperations.upload.multipart") { // Disabled since it relies on authentication and specific block being present in cloud storage if (false) { using namespace buildstorageoperations_testutils; FastRandom BaseRandom; const size_t FileCount = 11; const std::string Paths[FileCount] = {{"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 Sizes[FileCount] = {6u * 1024u, 0, 798, 19u * 1024u, 7u * 1024u, 93, 31u * 1024u, 17u * 1024u, 13u * 1024u, 2u * 1024u, 3u * 1024u}; ScopedTemporaryDirectory SourceFolder; TestState State(SourceFolder.Path()); State.Initialize(); State.CreateSourceData("source", Paths, Sizes); std::span ManifestFiles1(Paths); ManifestFiles1 = ManifestFiles1.subspan(0, FileCount / 2); std::span ManifestSizes1(Sizes); ManifestSizes1 = ManifestSizes1.subspan(0, FileCount / 2); std::span ManifestFiles2(Paths); ManifestFiles2 = ManifestFiles2.subspan(FileCount / 2 - 1); std::span ManifestSizes2(Sizes); ManifestSizes2 = ManifestSizes2.subspan(FileCount / 2 - 1); const Oid BuildPart1Id = Oid::NewOid(); const std::string BuildPart1Name = "part1"; const Oid BuildPart2Id = Oid::NewOid(); const std::string BuildPart2Name = "part2"; { CbObjectWriter Writer; Writer.BeginObject("parts"sv); { Writer.BeginObject(BuildPart1Name); { Writer.AddObjectId("partId"sv, BuildPart1Id); Writer.BeginArray("files"sv); for (const std::string& ManifestFile : ManifestFiles1) { Writer.AddString(ManifestFile); } Writer.EndArray(); // files } Writer.EndObject(); // part1 Writer.BeginObject(BuildPart2Name); { Writer.AddObjectId("partId"sv, BuildPart2Id); Writer.BeginArray("files"sv); for (const std::string& ManifestFile : ManifestFiles2) { Writer.AddString(ManifestFile); } Writer.EndArray(); // files } Writer.EndObject(); // part2 } Writer.EndObject(); // parts ExtendableStringBuilder<1024> Manifest; CompactBinaryToJson(Writer.Save(), Manifest); WriteFile(State.RootPath / "manifest.json", IoBuffer(IoBuffer::Wrap, Manifest.Data(), Manifest.Size())); } const Oid BuildId = Oid::NewOid(); auto Result = State.Upload(BuildId, {}, {}, "source", State.RootPath / "manifest.json"); CHECK_EQ(Result.size(), 2u); CHECK_EQ(Result[0].first, BuildPart1Id); CHECK_EQ(Result[0].second, BuildPart1Name); CHECK_EQ(Result[1].first, BuildPart2Id); CHECK_EQ(Result[1].second, BuildPart2Name); State.ValidateUpload(BuildId, Result); FolderContent DownloadContent = State.Download(BuildId, Oid::Zero, {}, "download", /* Append */ false); State.ValidateDownload(Paths, Sizes, "source", "download", DownloadContent); FolderContent Part1DownloadContent = State.Download(BuildId, BuildPart1Id, {}, "download_part1", /* Append */ false); State.ValidateDownload(ManifestFiles1, ManifestSizes1, "source", "download_part1", Part1DownloadContent); FolderContent Part2DownloadContent = State.Download(BuildId, Oid::Zero, BuildPart2Name, "download_part2", /* Append */ false); State.ValidateDownload(ManifestFiles2, ManifestSizes2, "source", "download_part2", Part2DownloadContent); (void)State.Download(BuildId, BuildPart1Id, BuildPart1Name, "download_part1+2", /* Append */ false); FolderContent Part1And2DownloadContent = State.Download(BuildId, BuildPart2Id, {}, "download_part1+2", /* Append */ true); State.ValidateDownload(Paths, Sizes, "source", "download_part1+2", Part1And2DownloadContent); } } TEST_CASE("buildstorageoperations.partial.block.download" * doctest::skip(true)) { const std::string OidcExecutableName = "OidcToken" ZEN_EXE_SUFFIX_LITERAL; std::filesystem::path OidcTokenExePath = (GetRunningExecutablePath().parent_path() / OidcExecutableName).make_preferred(); HttpClientSettings ClientSettings{ .LogCategory = "httpbuildsclient", .AccessTokenProvider = httpclientauth::CreateFromOidcTokenExecutable(OidcTokenExePath, "https://jupiter.devtools.epicgames.com", true, false, false), .AssumeHttp2 = false, .AllowResume = true, .RetryCount = 0, .Verbose = false}; HttpClient HttpClient("https://euc.jupiter.devtools.epicgames.com", ClientSettings); const std::string_view Namespace = "fortnite.oplog"; const std::string_view Bucket = "fortnitegame.staged-build.fortnite-main.ps4-client"; const Oid BuildId = Oid::FromHexString("09a76ea92ad301d4724fafad"); { HttpClient::Response Response = HttpClient.Get(fmt::format("/api/v2/builds/{}/{}/{}", Namespace, Bucket, BuildId), HttpClient::Accept(ZenContentType::kCbObject)); CbValidateError ValidateResult = CbValidateError::None; CbObject Object = ValidateAndReadCompactBinaryObject(IoBuffer(Response.ResponsePayload), ValidateResult); REQUIRE(ValidateResult == CbValidateError::None); } std::vector BlockDescriptions; { CbObjectWriter Request; Request.BeginArray("blocks"sv); { Request.AddHash(IoHash::FromHexString("7c353ed782675a5e8f968e61e51fc797ecdc2882")); } Request.EndArray(); IoBuffer Payload = Request.Save().GetBuffer().AsIoBuffer(); Payload.SetContentType(ZenContentType::kCbObject); HttpClient::Response BlockDescriptionsResponse = HttpClient.Post(fmt::format("/api/v2/builds/{}/{}/{}/blocks/getBlockMetadata", Namespace, Bucket, BuildId), Payload, HttpClient::Accept(ZenContentType::kCbObject)); REQUIRE(BlockDescriptionsResponse.IsSuccess()); CbValidateError ValidateResult = CbValidateError::None; CbObject Object = ValidateAndReadCompactBinaryObject(IoBuffer(BlockDescriptionsResponse.ResponsePayload), ValidateResult); REQUIRE(ValidateResult == CbValidateError::None); { CbArrayView BlocksArray = Object["blocks"sv].AsArrayView(); for (CbFieldView Block : BlocksArray) { ChunkBlockDescription Description = ParseChunkBlockDescription(Block.AsObjectView()); BlockDescriptions.emplace_back(std::move(Description)); } } } REQUIRE(!BlockDescriptions.empty()); const IoHash BlockHash = BlockDescriptions.back().BlockHash; const ChunkBlockDescription& BlockDescription = BlockDescriptions.front(); REQUIRE(!BlockDescription.ChunkRawHashes.empty()); REQUIRE(!BlockDescription.ChunkCompressedLengths.empty()); std::vector> ChunkOffsetAndSizes; uint64_t Offset = gsl::narrow(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize); for (uint32_t ChunkCompressedSize : BlockDescription.ChunkCompressedLengths) { ChunkOffsetAndSizes.push_back(std::make_pair(Offset, ChunkCompressedSize)); Offset += ChunkCompressedSize; } ScopedTemporaryDirectory SourceFolder; auto Validate = [&](std::span ChunkIndexesToFetch) { std::vector> Ranges; for (uint32_t ChunkIndex : ChunkIndexesToFetch) { Ranges.push_back(ChunkOffsetAndSizes[ChunkIndex]); } HttpClient::KeyValueMap Headers; if (!Ranges.empty()) { ExtendableStringBuilder<512> SB; for (const std::pair& R : Ranges) { if (SB.Size() > 0) { SB << ", "; } SB << R.first << "-" << R.first + R.second - 1; } Headers.Entries.insert({"Range", fmt::format("bytes={}", SB.ToView())}); } HttpClient::Response GetBlobRangesResponse = HttpClient.Download( fmt::format("/api/v2/builds/{}/{}/{}/blobs/{}?supportsRedirect=false", Namespace, Bucket, BuildId, BlockHash), SourceFolder.Path(), Headers); REQUIRE(GetBlobRangesResponse.IsSuccess()); [[maybe_unused]] MemoryView RangesMemoryView = GetBlobRangesResponse.ResponsePayload.GetView(); std::vector> PayloadRanges = GetBlobRangesResponse.GetRanges(Ranges); if (PayloadRanges.empty()) { // We got the whole blob, use the ranges as is PayloadRanges = Ranges; } REQUIRE(PayloadRanges.size() == Ranges.size()); for (uint32_t RangeIndex = 0; RangeIndex < PayloadRanges.size(); RangeIndex++) { const std::pair& PayloadRange = PayloadRanges[RangeIndex]; CHECK_EQ(PayloadRange.second, Ranges[RangeIndex].second); IoBuffer ChunkPayload(GetBlobRangesResponse.ResponsePayload, PayloadRange.first, PayloadRange.second); IoHash RawHash; uint64_t RawSize; CompressedBuffer CompressedChunk = CompressedBuffer::FromCompressed(SharedBuffer(ChunkPayload), RawHash, RawSize); CHECK(CompressedChunk); CHECK_EQ(RawHash, BlockDescription.ChunkRawHashes[ChunkIndexesToFetch[RangeIndex]]); CHECK_EQ(RawSize, BlockDescription.ChunkRawLengths[ChunkIndexesToFetch[RangeIndex]]); } }; { // Single std::vector ChunkIndexesToFetch{uint32_t(BlockDescription.ChunkCompressedLengths.size() / 2)}; Validate(ChunkIndexesToFetch); } { // Many std::vector ChunkIndexesToFetch; for (uint32_t Index = 0; Index < BlockDescription.ChunkCompressedLengths.size() / 16; Index++) { ChunkIndexesToFetch.push_back(uint32_t(BlockDescription.ChunkCompressedLengths.size() / 6 + Index * 7)); ChunkIndexesToFetch.push_back(uint32_t(BlockDescription.ChunkCompressedLengths.size() / 6 + Index * 7 + 1)); ChunkIndexesToFetch.push_back(uint32_t(BlockDescription.ChunkCompressedLengths.size() / 6 + Index * 7 + 3)); } Validate(ChunkIndexesToFetch); } { // First and last std::vector ChunkIndexesToFetch{0, uint32_t(BlockDescription.ChunkCompressedLengths.size() - 1)}; Validate(ChunkIndexesToFetch); } } TEST_SUITE_END(); void buildstorageutil_forcelink() { } #endif // ZEN_WITH_TESTS } // namespace zen