// Copyright Epic Games, Inc. All Rights Reserved. #include "httpprojectstore.h" #include "oplogreferencedset.h" #include "projectstore.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace zen { const FLLMTag& GetProjectHttpTag() { static FLLMTag _("http", FLLMTag("project")); return _; } void CSVHeader(bool Details, bool AttachmentDetails, StringBuilderBase& CSVWriter) { if (AttachmentDetails) { CSVWriter << "Project, Oplog, LSN, Key, Cid, Size"; } else if (Details) { CSVWriter << "Project, Oplog, LSN, Key, Size, AttachmentCount, AttachmentsSize"; } else { CSVWriter << "Project, Oplog, Key"; } } void CSVWriteOp(CidStore& CidStore, std::string_view ProjectId, std::string_view OplogId, bool Details, bool AttachmentDetails, uint32_t LSN, const Oid& Key, CbObjectView Op, StringBuilderBase& CSVWriter) { StringBuilder<32> KeyStringBuilder; Key.ToString(KeyStringBuilder); const std::string_view KeyString = KeyStringBuilder.ToView(); if (AttachmentDetails) { Op.IterateAttachments([&CidStore, &CSVWriter, &ProjectId, &OplogId, LSN, &KeyString](CbFieldView FieldView) { const IoHash AttachmentHash = FieldView.AsAttachment(); IoBuffer Attachment = CidStore.FindChunkByCid(AttachmentHash); CSVWriter << "\r\n" << ProjectId << ", " << OplogId << ", " << LSN << ", " << KeyString << ", " << AttachmentHash.ToHexString() << ", " << gsl::narrow(Attachment.GetSize()); }); } else if (Details) { uint64_t AttachmentCount = 0; size_t AttachmentsSize = 0; Op.IterateAttachments([&CidStore, &AttachmentCount, &AttachmentsSize](CbFieldView FieldView) { const IoHash AttachmentHash = FieldView.AsAttachment(); AttachmentCount++; IoBuffer Attachment = CidStore.FindChunkByCid(AttachmentHash); AttachmentsSize += Attachment.GetSize(); }); CSVWriter << "\r\n" << ProjectId << ", " << OplogId << ", " << LSN << ", " << KeyString << ", " << gsl::narrow(Op.GetSize()) << ", " << AttachmentCount << ", " << gsl::narrow(AttachmentsSize); } else { CSVWriter << "\r\n" << ProjectId << ", " << OplogId << ", " << KeyString; } }; ////////////////////////////////////////////////////////////////////////// namespace { void CbWriteOp(CidStore& CidStore, bool Details, bool OpDetails, bool AttachmentDetails, uint32_t LSN, const Oid& Key, CbObjectView Op, CbObjectWriter& CbWriter) { CbWriter.BeginObject(); { CbWriter.AddObjectId("key", Key); if (Details) { CbWriter.AddInteger("lsn", LSN); CbWriter.AddInteger("size", gsl::narrow(Op.GetSize())); } if (AttachmentDetails) { CbWriter.BeginArray("attachments"); Op.IterateAttachments([&CidStore, &CbWriter](CbFieldView FieldView) { const IoHash AttachmentHash = FieldView.AsAttachment(); CbWriter.BeginObject(); { IoBuffer Attachment = CidStore.FindChunkByCid(AttachmentHash); CbWriter.AddString("cid", AttachmentHash.ToHexString()); CbWriter.AddInteger("size", gsl::narrow(Attachment.GetSize())); } CbWriter.EndObject(); }); CbWriter.EndArray(); } else if (Details) { uint64_t AttachmentCount = 0; size_t AttachmentsSize = 0; Op.IterateAttachments([&CidStore, &AttachmentCount, &AttachmentsSize](CbFieldView FieldView) { const IoHash AttachmentHash = FieldView.AsAttachment(); AttachmentCount++; IoBuffer Attachment = CidStore.FindChunkByCid(AttachmentHash); AttachmentsSize += Attachment.GetSize(); }); if (AttachmentCount > 0) { CbWriter.AddInteger("attachments", AttachmentCount); CbWriter.AddInteger("attachmentssize", gsl::narrow(AttachmentsSize)); } } if (OpDetails) { CbWriter.BeginObject("op"); for (const CbFieldView& Field : Op) { if (!Field.HasName()) { CbWriter.AddField(Field); continue; } std::string_view FieldName = Field.GetName(); CbWriter.AddField(FieldName, Field); } CbWriter.EndObject(); } } CbWriter.EndObject(); }; void CbWriteOplogOps(CidStore& CidStore, ProjectStore::Oplog& Oplog, bool Details, bool OpDetails, bool AttachmentDetails, CbObjectWriter& Cbo) { Cbo.BeginArray("ops"); { Oplog.IterateOplogWithKey( [&Cbo, &CidStore, Details, OpDetails, AttachmentDetails](uint32_t LSN, const Oid& Key, CbObjectView Op) { CbWriteOp(CidStore, Details, OpDetails, AttachmentDetails, LSN, Key, Op, Cbo); }); } Cbo.EndArray(); } void CbWriteOplog(CidStore& CidStore, ProjectStore::Oplog& Oplog, bool Details, bool OpDetails, bool AttachmentDetails, CbObjectWriter& Cbo) { Cbo.BeginObject(); { Cbo.AddString("name", Oplog.OplogId()); CbWriteOplogOps(CidStore, Oplog, Details, OpDetails, AttachmentDetails, Cbo); } Cbo.EndObject(); } void CbWriteOplogs(CidStore& CidStore, ProjectStore::Project& Project, std::vector OpLogs, bool Details, bool OpDetails, bool AttachmentDetails, CbObjectWriter& Cbo) { Cbo.BeginArray("oplogs"); { for (const std::string& OpLogId : OpLogs) { ProjectStore::Oplog* Oplog = Project.OpenOplog(OpLogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); if (Oplog != nullptr) { CbWriteOplog(CidStore, *Oplog, Details, OpDetails, AttachmentDetails, Cbo); } } } Cbo.EndArray(); } void CbWriteProject(CidStore& CidStore, ProjectStore::Project& Project, std::vector OpLogs, bool Details, bool OpDetails, bool AttachmentDetails, CbObjectWriter& Cbo) { Cbo.BeginObject(); { Cbo.AddString("name", Project.Identifier); CbWriteOplogs(CidStore, Project, OpLogs, Details, OpDetails, AttachmentDetails, Cbo); } Cbo.EndObject(); } } // namespace ////////////////////////////////////////////////////////////////////////// HttpProjectService::HttpProjectService(ProjectStore* Projects, HttpStatusService& StatusService, HttpStatsService& StatsService, AuthMgr& AuthMgr) : m_Log(logging::Get("project")) , m_ProjectStore(Projects) , m_StatusService(StatusService) , m_StatsService(StatsService) , m_AuthMgr(AuthMgr) { ZEN_MEMSCOPE(GetProjectHttpTag()); using namespace std::literals; m_Router.AddPattern("project", "([[:alnum:]_.]+)"); m_Router.AddPattern("log", "([[:alnum:]_.]+)"); m_Router.AddPattern("op", "([[:digit:]]+?)"); m_Router.AddPattern("chunk", "([[:xdigit:]]{24})"); m_Router.AddPattern("hash", "([[:xdigit:]]{40})"); m_Router.RegisterRoute( "", [this](HttpRouterRequest& Req) { HandleProjectListRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "list", [this](HttpRouterRequest& Req) { HandleProjectListRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "{project}/oplog/{log}/batch", [this](HttpRouterRequest& Req) { HandleChunkBatchRequest(Req); }, HttpVerb::kPost); m_Router.RegisterRoute( "{project}/oplog/{log}/files", [this](HttpRouterRequest& Req) { HandleFilesRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "{project}/oplog/{log}/chunkinfos", [this](HttpRouterRequest& Req) { HandleChunkInfosRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "{project}/oplog/{log}/{chunk}/info", [this](HttpRouterRequest& Req) { HandleChunkInfoRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "{project}/oplog/{log}/{chunk}", [this](HttpRouterRequest& Req) { HandleChunkByIdRequest(Req); }, HttpVerb::kGet | HttpVerb::kHead); m_Router.RegisterRoute( "{project}/oplog/{log}/{hash}", [this](HttpRouterRequest& Req) { HandleChunkByCidRequest(Req); }, HttpVerb::kGet | HttpVerb::kPost); m_Router.RegisterRoute( "{project}/oplog/{log}/prep", [this](HttpRouterRequest& Req) { HandleOplogOpPrepRequest(Req); }, HttpVerb::kPost); m_Router.RegisterRoute( "{project}/oplog/{log}/new", [this](HttpRouterRequest& Req) { HandleOplogOpNewRequest(Req); }, HttpVerb::kPost); m_Router.RegisterRoute( "{project}/oplog/{log}/validate", [this](HttpRouterRequest& Req) { HandleOplogValidateRequest(Req); }, HttpVerb::kPost); m_Router.RegisterRoute( "{project}/oplog/{log}/{op}", [this](HttpRouterRequest& Req) { HandleOpLogOpRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "{project}/oplog/{log}", [this](HttpRouterRequest& Req) { HandleOpLogRequest(Req); }, HttpVerb::kGet | HttpVerb::kPut | HttpVerb::kPost | HttpVerb::kDelete); m_Router.RegisterRoute( "{project}/oplog/{log}/entries", [this](HttpRouterRequest& Req) { HandleOpLogEntriesRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "{project}", [this](HttpRouterRequest& Req) { HandleProjectRequest(Req); }, HttpVerb::kGet | HttpVerb::kPut | HttpVerb::kPost | HttpVerb::kDelete); // Push a oplog container m_Router.RegisterRoute( "{project}/oplog/{log}/save", [this](HttpRouterRequest& Req) { HandleOplogSaveRequest(Req); }, HttpVerb::kPost); // Pull a oplog container m_Router.RegisterRoute( "{project}/oplog/{log}/load", [this](HttpRouterRequest& Req) { HandleOplogLoadRequest(Req); }, HttpVerb::kGet); // Do an rpc style operation on project/oplog m_Router.RegisterRoute( "{project}/oplog/{log}/rpc", [this](HttpRouterRequest& Req) { HandleRpcRequest(Req); }, HttpVerb::kPost); m_Router.RegisterRoute( "details\\$", [this](HttpRouterRequest& Req) { HandleDetailsRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "details\\$/{project}", [this](HttpRouterRequest& Req) { HandleProjectDetailsRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "details\\$/{project}/{log}", [this](HttpRouterRequest& Req) { HandleOplogDetailsRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "details\\$/{project}/{log}/{chunk}", [this](HttpRouterRequest& Req) { HandleOplogOpDetailsRequest(Req); }, HttpVerb::kGet); m_StatusService.RegisterHandler("prj", *this); m_StatsService.RegisterHandler("prj", *this); } HttpProjectService::~HttpProjectService() { m_StatsService.UnregisterHandler("prj", *this); m_StatusService.UnregisterHandler("prj", *this); } const char* HttpProjectService::BaseUri() const { return "/prj/"; } void HttpProjectService::HandleRequest(HttpServerRequest& Request) { m_ProjectStats.RequestCount++; ZEN_MEMSCOPE(GetProjectHttpTag()); metrics::OperationTiming::Scope $(m_HttpRequests); if (m_Router.HandleRequest(Request) == false) { m_ProjectStats.BadRequestCount++; ZEN_WARN("No route found for {0}", Request.RelativeUri()); } } void HttpProjectService::HandleStatsRequest(HttpServerRequest& HttpReq) { ZEN_TRACE_CPU("ProjectService::Stats"); bool ShowCidStoreStats = HttpReq.GetQueryParams().GetValue("cidstorestats") == "true"; const GcStorageSize StoreSize = m_ProjectStore->StorageSize(); uint64_t TotalChunkHitCount = 0; uint64_t TotalChunkMissCount = 0; uint64_t TotalChunkWriteCount = 0; CidStoreSize TotalCidSize; tsl::robin_map UniqueStores; { m_ProjectStore->IterateProjects([&UniqueStores](ProjectStore::Project& Project) { CidStore* Store = &Project.GetCidStore(); if (auto It = UniqueStores.find(Store); It == UniqueStores.end()) { UniqueStores.insert_or_assign(Store, Project.Identifier); } else { UniqueStores.insert_or_assign(Store, std::string{}); } }); for (auto It : UniqueStores) { CidStore* ChunkStore = It.first; CidStoreStats ChunkStoreStats = ChunkStore->Stats(); CidStoreSize ChunkStoreSize = ChunkStore->TotalSize(); TotalChunkHitCount += ChunkStoreStats.HitCount; TotalChunkMissCount += ChunkStoreStats.MissCount; TotalChunkWriteCount += ChunkStoreStats.WriteCount; TotalCidSize.TinySize += ChunkStoreSize.TinySize; TotalCidSize.SmallSize += ChunkStoreSize.SmallSize; TotalCidSize.LargeSize += ChunkStoreSize.LargeSize; TotalCidSize.TotalSize += ChunkStoreSize.TotalSize; } } CbObjectWriter Cbo; EmitSnapshot("requests", m_HttpRequests, Cbo); Cbo.BeginObject("store"); { Cbo.BeginObject("size"); { Cbo << "disk" << StoreSize.DiskSize; Cbo << "memory" << StoreSize.MemorySize; } Cbo.EndObject(); Cbo.BeginObject("project"); { Cbo << "readcount" << m_ProjectStats.ProjectReadCount << "writecount" << m_ProjectStats.ProjectWriteCount << "deletecount" << m_ProjectStats.ProjectDeleteCount; } Cbo.EndObject(); Cbo.BeginObject("oplog"); { Cbo << "readcount" << m_ProjectStats.OpLogReadCount << "writecount" << m_ProjectStats.OpLogWriteCount << "deletecount" << m_ProjectStats.OpLogDeleteCount; } Cbo.EndObject(); Cbo.BeginObject("op"); { Cbo << "hitcount" << m_ProjectStats.OpHitCount << "misscount" << m_ProjectStats.OpMissCount << "writecount" << m_ProjectStats.OpWriteCount; } Cbo.EndObject(); Cbo.BeginObject("chunk"); { Cbo << "hitcount" << m_ProjectStats.ChunkHitCount << "misscount" << m_ProjectStats.ChunkMissCount << "writecount" << m_ProjectStats.ChunkWriteCount; } Cbo.EndObject(); Cbo << "requestcount" << m_ProjectStats.RequestCount; Cbo << "badrequestcount" << m_ProjectStats.BadRequestCount; } Cbo.EndObject(); Cbo.BeginObject("cid"); { Cbo.BeginObject("size"); { Cbo << "tiny" << TotalCidSize.TinySize; Cbo << "small" << TotalCidSize.SmallSize; Cbo << "large" << TotalCidSize.LargeSize; Cbo << "total" << TotalCidSize.TotalSize; } Cbo.EndObject(); if (ShowCidStoreStats) { Cbo << "cidhits" << TotalChunkHitCount << "cidmisses" << TotalChunkMissCount << "cidwrites" << TotalChunkWriteCount; const uint64_t TotalChunkCount = TotalChunkHitCount + TotalChunkMissCount; Cbo << "cidhit_ratio" << (TotalChunkHitCount ? (double(TotalChunkCount) / double(TotalChunkHitCount)) : 0.0); Cbo.BeginObject("store"); auto OutputStats = [&](CidStore& ChunkStore) { CidStoreStats StoreStats = ChunkStore.Stats(); Cbo << "hits" << StoreStats.HitCount << "misses" << StoreStats.MissCount << "writes" << StoreStats.WriteCount; const uint64_t Count = StoreStats.HitCount + StoreStats.MissCount; Cbo << "hit_ratio" << (Count ? (double(StoreStats.HitCount) / double(Count)) : 0.0); EmitSnapshot("read", StoreStats.FindChunkOps, Cbo); EmitSnapshot("write", StoreStats.AddChunkOps, Cbo); }; if (UniqueStores.size() > 1) { Cbo.BeginArray("projects"); for (auto It : UniqueStores) { CidStore* ChunkStore = It.first; const std::string& ProjectId = It.second; CidStoreSize ChunkStoreSize = ChunkStore->TotalSize(); Cbo.BeginObject(); { Cbo << "project" << ProjectId; Cbo.BeginObject("stats"); OutputStats(*ChunkStore); Cbo.EndObject(); Cbo.BeginObject("size"); { Cbo << "tiny" << ChunkStoreSize.TinySize; Cbo << "small" << ChunkStoreSize.SmallSize; Cbo << "large" << ChunkStoreSize.LargeSize; Cbo << "total" << ChunkStoreSize.TotalSize; } Cbo.EndObject(); } Cbo.EndObject(); } Cbo.EndArray(); // projects } else if (UniqueStores.size() != 0) { CidStore& ChunkStore = *UniqueStores.begin()->first; OutputStats(ChunkStore); } Cbo.EndObject(); } } Cbo.EndObject(); return HttpReq.WriteResponse(HttpResponseCode::OK, Cbo.Save()); } void HttpProjectService::HandleStatusRequest(HttpServerRequest& Request) { ZEN_TRACE_CPU("HttpProjectService::Status"); CbObjectWriter Cbo; Cbo << "ok" << true; Request.WriteResponse(HttpResponseCode::OK, Cbo.Save()); } void HttpProjectService::HandleProjectListRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::ProjectList"); HttpServerRequest& HttpReq = Req.ServerRequest(); CbArray ProjectsList = m_ProjectStore->GetProjectsList(); HttpReq.WriteResponse(HttpResponseCode::OK, ProjectsList); } void HttpProjectService::HandleChunkBatchRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::ChunkBatch"); HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); Ref Project = m_ProjectStore->OpenProject(ProjectId); if (!Project) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } Project->TouchOplog(OplogId); // Parse Request IoBuffer Payload = HttpReq.ReadPayload(); BinaryReader Reader(Payload); struct RequestHeader { enum { kMagic = 0xAAAA'77AC }; uint32_t Magic; uint32_t ChunkCount; uint32_t Reserved1; uint32_t Reserved2; }; struct RequestChunkEntry { Oid ChunkId; uint32_t CorrelationId; uint64_t Offset; uint64_t RequestBytes; }; if (Payload.Size() <= sizeof(RequestHeader)) { m_ProjectStats.BadRequestCount++; HttpReq.WriteResponse(HttpResponseCode::BadRequest); } RequestHeader RequestHdr; Reader.Read(&RequestHdr, sizeof RequestHdr); if (RequestHdr.Magic != RequestHeader::kMagic) { m_ProjectStats.BadRequestCount++; HttpReq.WriteResponse(HttpResponseCode::BadRequest); } std::vector RequestedChunks; RequestedChunks.resize(RequestHdr.ChunkCount); Reader.Read(RequestedChunks.data(), sizeof(RequestChunkEntry) * RequestHdr.ChunkCount); // Make Response struct ResponseHeader { uint32_t Magic = 0xbada'b00f; uint32_t ChunkCount; uint32_t Reserved1 = 0; uint32_t Reserved2 = 0; }; struct ResponseChunkEntry { uint32_t CorrelationId; uint32_t Flags = 0; uint64_t ChunkSize; }; std::vector OutBlobs; OutBlobs.emplace_back(sizeof(ResponseHeader) + RequestHdr.ChunkCount * sizeof(ResponseChunkEntry)); for (uint32_t ChunkIndex = 0; ChunkIndex < RequestHdr.ChunkCount; ++ChunkIndex) { const RequestChunkEntry& RequestedChunk = RequestedChunks[ChunkIndex]; IoBuffer FoundChunk = FoundLog->FindChunk(RequestedChunk.ChunkId, nullptr); if (FoundChunk) { if (RequestedChunk.Offset > 0 || RequestedChunk.RequestBytes < uint64_t(-1)) { uint64_t Offset = RequestedChunk.Offset; if (Offset > FoundChunk.Size()) { Offset = FoundChunk.Size(); } uint64_t Size = RequestedChunk.RequestBytes; if ((Offset + Size) > FoundChunk.Size()) { Size = FoundChunk.Size() - Offset; } FoundChunk = IoBuffer(FoundChunk, Offset, Size); } } OutBlobs.emplace_back(std::move(FoundChunk)); } uint8_t* ResponsePtr = reinterpret_cast(OutBlobs[0].MutableData()); ResponseHeader ResponseHdr; ResponseHdr.ChunkCount = RequestHdr.ChunkCount; memcpy(ResponsePtr, &ResponseHdr, sizeof(ResponseHdr)); ResponsePtr += sizeof(ResponseHdr); for (uint32_t ChunkIndex = 0; ChunkIndex < RequestHdr.ChunkCount; ++ChunkIndex) { const RequestChunkEntry& RequestedChunk = RequestedChunks[ChunkIndex]; const IoBuffer& FoundChunk(OutBlobs[ChunkIndex + 1]); ResponseChunkEntry ResponseChunk; ResponseChunk.CorrelationId = RequestedChunk.CorrelationId; if (FoundChunk) { ResponseChunk.ChunkSize = FoundChunk.Size(); m_ProjectStats.ChunkHitCount++; } else { ResponseChunk.ChunkSize = uint64_t(-1); m_ProjectStats.ChunkMissCount++; } memcpy(ResponsePtr, &ResponseChunk, sizeof(ResponseChunk)); ResponsePtr += sizeof(ResponseChunk); } std::erase_if(OutBlobs, [](IoBuffer Buffer) -> bool { return !Buffer; }); return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kBinary, OutBlobs); } void HttpProjectService::HandleFilesRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::Files"); using namespace std::literals; HttpServerRequest& HttpReq = Req.ServerRequest(); // File manifest fetch, returns the client file list const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams(); std::unordered_set WantedFieldNames; if (auto FieldFilter = HttpServerRequest::Decode(Params.GetValue("fieldnames")); !FieldFilter.empty()) { if (FieldFilter != "*") // Get all - empty FieldFilter equal getting all fields { ForEachStrTok(FieldFilter, ',', [&](std::string_view FieldName) { WantedFieldNames.insert(std::string(FieldName)); return true; }); } } else { const bool FilterClient = Params.GetValue("filter"sv) == "client"sv; WantedFieldNames.insert("id"); WantedFieldNames.insert("clientpath"); if (!FilterClient) { WantedFieldNames.insert("serverpath"); } } CbObject ResponsePayload; std::pair Result = m_ProjectStore->GetProjectFiles(ProjectId, OplogId, WantedFieldNames, ResponsePayload); if (Result.first == HttpResponseCode::OK) { if (HttpReq.AcceptContentType() == HttpContentType::kCompressedBinary) { CompositeBuffer Payload = CompressedBuffer::Compress(ResponsePayload.GetBuffer()).GetCompressed(); return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCompressedBinary, Payload); } else { return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload); } } else { if (Result.first == HttpResponseCode::BadRequest) { m_ProjectStats.BadRequestCount++; } ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", ToString(HttpReq.RequestVerb()), HttpReq.QueryString(), static_cast(Result.first), Result.second); } if (Result.second.empty()) { return HttpReq.WriteResponse(Result.first); } return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } void HttpProjectService::HandleChunkInfosRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::ChunkInfos"); HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams(); std::unordered_set WantedFieldNames; if (auto FieldFilter = HttpServerRequest::Decode(Params.GetValue("fieldnames")); !FieldFilter.empty()) { if (FieldFilter != "*") // Get all - empty FieldFilter equal getting all fields { ForEachStrTok(FieldFilter, ',', [&](std::string_view FieldName) { WantedFieldNames.insert(std::string(FieldName)); return true; }); } } else { WantedFieldNames.insert("id"); WantedFieldNames.insert("rawhash"); WantedFieldNames.insert("rawsize"); } CbObject ResponsePayload; std::pair Result = m_ProjectStore->GetProjectChunkInfos(ProjectId, OplogId, WantedFieldNames, ResponsePayload); if (Result.first == HttpResponseCode::OK) { if (HttpReq.AcceptContentType() == HttpContentType::kCompressedBinary) { CompositeBuffer Payload = CompressedBuffer::Compress(ResponsePayload.GetBuffer()).GetCompressed(); return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCompressedBinary, Payload); } else { return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload); } } else { if (Result.first == HttpResponseCode::BadRequest) { m_ProjectStats.BadRequestCount++; } ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", ToString(HttpReq.RequestVerb()), HttpReq.QueryString(), static_cast(Result.first), Result.second); } if (Result.second.empty()) { return HttpReq.WriteResponse(Result.first); } return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } void HttpProjectService::HandleChunkInfoRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::ChunkInfo"); HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); const auto& ChunkId = Req.GetCapture(3); CbObject ResponsePayload; std::pair Result = m_ProjectStore->GetChunkInfo(ProjectId, OplogId, ChunkId, ResponsePayload); if (Result.first == HttpResponseCode::OK) { m_ProjectStats.ChunkHitCount++; return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload); } else if (Result.first == HttpResponseCode::NotFound) { m_ProjectStats.ChunkMissCount++; ZEN_DEBUG("chunk - '{}/{}/{}' MISSING", ProjectId, OplogId, ChunkId); } else { if (Result.first == HttpResponseCode::BadRequest) { m_ProjectStats.BadRequestCount++; } ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", ToString(HttpReq.RequestVerb()), HttpReq.QueryString(), static_cast(Result.first), Result.second); } if (Result.second.empty()) { return HttpReq.WriteResponse(Result.first); } return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } void HttpProjectService::HandleChunkByIdRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::ChunkById"); HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); const auto& ChunkId = Req.GetCapture(3); uint64_t Offset = 0; uint64_t Size = ~(0ull); auto QueryParms = HttpReq.GetQueryParams(); if (auto OffsetParm = QueryParms.GetValue("offset"); OffsetParm.empty() == false) { if (auto OffsetVal = ParseInt(OffsetParm)) { Offset = OffsetVal.value(); } else { m_ProjectStats.BadRequestCount++; return HttpReq.WriteResponse(HttpResponseCode::BadRequest); } } if (auto SizeParm = QueryParms.GetValue("size"); SizeParm.empty() == false) { if (auto SizeVal = ParseInt(SizeParm)) { Size = SizeVal.value(); } else { m_ProjectStats.BadRequestCount++; return HttpReq.WriteResponse(HttpResponseCode::BadRequest); } } HttpContentType AcceptType = HttpReq.AcceptContentType(); CompositeBuffer Chunk; HttpContentType ContentType; std::pair Result = m_ProjectStore->GetChunkRange(ProjectId, OplogId, ChunkId, Offset, Size, AcceptType, Chunk, ContentType, nullptr); if (Result.first == HttpResponseCode::OK) { m_ProjectStats.ChunkHitCount++; ZEN_DEBUG("chunk - '{}/{}/{}' '{}'", ProjectId, OplogId, ChunkId, ToString(ContentType)); return HttpReq.WriteResponse(HttpResponseCode::OK, ContentType, Chunk); } else if (Result.first == HttpResponseCode::NotFound) { m_ProjectStats.ChunkMissCount++; ZEN_DEBUG("chunk - '{}/{}/{}' MISSING", ProjectId, OplogId, ChunkId); } else { if (Result.first == HttpResponseCode::BadRequest) { m_ProjectStats.BadRequestCount++; } ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", ToString(HttpReq.RequestVerb()), HttpReq.QueryString(), static_cast(Result.first), Result.second); } if (Result.second.empty()) { return HttpReq.WriteResponse(Result.first); } return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } void HttpProjectService::HandleChunkByCidRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::ChunkByCid"); HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); const auto& Cid = Req.GetCapture(3); HttpContentType AcceptType = HttpReq.AcceptContentType(); HttpContentType RequestType = HttpReq.RequestContentType(); switch (HttpReq.RequestVerb()) { case HttpVerb::kGet: { IoBuffer Value; std::pair Result = m_ProjectStore->GetChunk(ProjectId, OplogId, Cid, Value, nullptr); if (Result.first == HttpResponseCode::OK) { if (AcceptType == ZenContentType::kUnknownContentType || AcceptType == ZenContentType::kBinary || AcceptType == ZenContentType::kJSON || AcceptType == ZenContentType::kYAML || AcceptType == ZenContentType::kCbObject) { CompressedBuffer Compressed = CompressedBuffer::FromCompressedNoValidate(std::move(Value)); IoBuffer DecompressedBuffer = Compressed.Decompress().AsIoBuffer(); if (DecompressedBuffer) { if (AcceptType == ZenContentType::kJSON || AcceptType == ZenContentType::kYAML || AcceptType == ZenContentType::kCbObject) { CbValidateError CbErr = ValidateCompactBinary(DecompressedBuffer.GetView(), CbValidateMode::Default); if (!!CbErr) { m_ProjectStats.BadRequestCount++; ZEN_DEBUG( "chunk - '{}/{}/{}' WRONGTYPE. Reason: `Requested {} format, but could not convert to object`", ProjectId, OplogId, Cid, ToString(AcceptType)); return HttpReq.WriteResponse( HttpResponseCode::NotAcceptable, HttpContentType::kText, fmt::format("Content format not supported, requested {} format, but could not convert to object", ToString(AcceptType))); } m_ProjectStats.ChunkHitCount++; CbObject ContainerObject = LoadCompactBinaryObject(DecompressedBuffer); return HttpReq.WriteResponse(HttpResponseCode::OK, ContainerObject); } else { Value = DecompressedBuffer; Value.SetContentType(ZenContentType::kBinary); } } else { m_ProjectStats.BadRequestCount++; ZEN_DEBUG("chunk - '{}/{}/{}' WRONGTYPE. Reason: `Requested {} format, but could not decompress stored data`", ProjectId, OplogId, Cid, ToString(AcceptType)); return HttpReq.WriteResponse( HttpResponseCode::NotAcceptable, HttpContentType::kText, fmt::format("Content format not supported, requested {} format, but could not decompress stored data", ToString(AcceptType))); } } m_ProjectStats.ChunkHitCount++; return HttpReq.WriteResponse(HttpResponseCode::OK, Value.GetContentType(), Value); } else if (Result.first == HttpResponseCode::NotFound) { m_ProjectStats.ChunkMissCount++; ZEN_DEBUG("chunk - '{}/{}/{}' MISSING", ProjectId, OplogId, Cid); } else { if (Result.first == HttpResponseCode::BadRequest) { m_ProjectStats.BadRequestCount++; } ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", ToString(HttpReq.RequestVerb()), HttpReq.QueryString(), static_cast(Result.first), Result.second); } if (Result.second.empty()) { return HttpReq.WriteResponse(Result.first); } return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } case HttpVerb::kPost: { if (!m_ProjectStore->AreDiskWritesAllowed()) { return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); } std::pair Result = m_ProjectStore->PutChunk(ProjectId, OplogId, Cid, RequestType, HttpReq.ReadPayload()); if (Result.first == HttpResponseCode::OK || Result.first == HttpResponseCode::Created) { m_ProjectStats.ChunkWriteCount++; return HttpReq.WriteResponse(Result.first); } else { if (Result.first == HttpResponseCode::BadRequest) { m_ProjectStats.BadRequestCount++; } ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", ToString(HttpReq.RequestVerb()), HttpReq.QueryString(), static_cast(Result.first), Result.second); } if (Result.second.empty()) { return HttpReq.WriteResponse(Result.first); } return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } break; } } void HttpProjectService::HandleOplogOpPrepRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::OplogOpPrep"); using namespace std::literals; HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); Ref Project = m_ProjectStore->OpenProject(ProjectId); if (!Project) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } Project->TouchOplog(OplogId); // This operation takes a list of referenced hashes and decides which // chunks are not present on this server. This list is then returned in // the "need" list in the response IoBuffer Payload = HttpReq.ReadPayload(); CbObject RequestObject = LoadCompactBinaryObject(Payload); std::vector NeedList; { eastl::fixed_vector ChunkList; CbArrayView HaveList = RequestObject["have"sv].AsArrayView(); ChunkList.reserve(HaveList.Num()); for (auto& Entry : HaveList) { ChunkList.push_back(Entry.AsHash()); } NeedList = FoundLog->CheckPendingChunkReferences(std::span(begin(ChunkList), end(ChunkList)), std::chrono::minutes(2)); } CbObjectWriter Cbo(1 + 1 + 5 + NeedList.size() * (1 + sizeof(IoHash::Hash)) + 1); Cbo.BeginArray("need"); { for (const IoHash& Hash : NeedList) { ZEN_DEBUG("prep - NEED: {}", Hash); Cbo << Hash; } } Cbo.EndArray(); CbObject Response = Cbo.Save(); return HttpReq.WriteResponse(HttpResponseCode::OK, Response); } void HttpProjectService::HandleOplogOpNewRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::OplogOpNew"); using namespace std::literals; HttpServerRequest& HttpReq = Req.ServerRequest(); if (!m_ProjectStore->AreDiskWritesAllowed()) { return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); } const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams(); bool IsUsingSalt = false; IoHash SaltHash = IoHash::Zero; if (std::string_view SaltParam = Params.GetValue("salt"sv); SaltParam.empty() == false) { const uint32_t Salt = std::stoi(std::string(SaltParam)); SaltHash = IoHash::HashBuffer(&Salt, sizeof Salt); IsUsingSalt = true; } Ref Project = m_ProjectStore->OpenProject(ProjectId); if (!Project) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } Project->TouchOplog(OplogId); CidStore& ChunkStore = Project->GetCidStore(); ProjectStore::Oplog& Oplog = *FoundLog; IoBuffer Payload = HttpReq.ReadPayload(); // This will attempt to open files which may not exist for the case where // the prep step rejected the chunk. This should be fixed since there's // a performance cost associated with any file system activity bool IsValid = true; std::vector MissingChunks; CbPackage::AttachmentResolver Resolver = [&](const IoHash& Hash) -> SharedBuffer { if (ChunkStore.ContainsChunk(Hash)) { // Return null attachment as we already have it, no point in reading it and storing it again return {}; } IoHash AttachmentId; if (IsUsingSalt) { IoHash AttachmentSpec[]{SaltHash, Hash}; AttachmentId = IoHash::HashBuffer(MakeMemoryView(AttachmentSpec)); } else { AttachmentId = Hash; } std::filesystem::path AttachmentPath = Oplog.TempPath() / AttachmentId.ToHexString(); if (IoBuffer Data = IoBufferBuilder::MakeFromTemporaryFile(AttachmentPath)) { Data.SetDeleteOnClose(true); return SharedBuffer(std::move(Data)); } else { IsValid = false; MissingChunks.push_back(Hash); return {}; } }; CbPackage Package; if (!legacy::TryLoadCbPackage(Package, Payload, &UniqueBuffer::Alloc, &Resolver)) { if (CbObject Core = LoadCompactBinaryObject(Payload)) { Package.SetObject(Core); } else { std::filesystem::path BadPackagePath = Oplog.TempPath() / "bad_packages"sv / fmt::format("session{}_request{}"sv, HttpReq.SessionId(), HttpReq.RequestId()); ZEN_WARN("Received malformed package! Saving payload to '{}'", BadPackagePath); WriteFile(BadPackagePath, Payload); m_ProjectStats.BadRequestCount++; return HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, u8"request body must be a compact binary object or package in legacy format"); } } m_ProjectStats.ChunkMissCount += MissingChunks.size(); if (!IsValid) { ExtendableStringBuilder<256> ResponseText; ResponseText.Append("Missing chunk references: "); bool IsFirst = true; for (const auto& Hash : MissingChunks) { if (IsFirst) { IsFirst = false; } else { ResponseText.Append(", "); } Hash.ToHexString(ResponseText); } return HttpReq.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, ResponseText); } CbObject Core = Package.GetObject(); if (!Core["key"sv]) { m_ProjectStats.BadRequestCount++; return HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "No oplog entry key specified"); } eastl::fixed_vector ReferencedChunks; Core.IterateAttachments([&ReferencedChunks](CbFieldView View) { ReferencedChunks.push_back(View.AsAttachment()); }); // Write core to oplog size_t AttachmentCount = Package.GetAttachments().size(); const uint32_t OpLsn = Oplog.AppendNewOplogEntry(Package); if (OpLsn == ProjectStore::Oplog::kInvalidOp) { m_ProjectStats.BadRequestCount++; return HttpReq.WriteResponse(HttpResponseCode::BadRequest); } m_ProjectStats.ChunkWriteCount += AttachmentCount; // Once we stored the op, we no longer need to retain any chunks this op references if (!ReferencedChunks.empty()) { FoundLog->RemovePendingChunkReferences(std::span(begin(ReferencedChunks), end(ReferencedChunks))); } m_ProjectStats.OpWriteCount++; ZEN_DEBUG("'{}/{}' op #{} ({}) - '{}'", ProjectId, OplogId, OpLsn, NiceBytes(Payload.Size()), Core["key"sv].AsString()); HttpReq.WriteResponse(HttpResponseCode::Created); } void HttpProjectService::HandleOplogValidateRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::OplogOpValidate"); using namespace std::literals; HttpServerRequest& HttpReq = Req.ServerRequest(); if (!m_ProjectStore->AreDiskWritesAllowed()) { return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); } const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); Ref Project = m_ProjectStore->OpenProject(ProjectId); if (!Project) { return HttpReq.WriteResponse(HttpResponseCode::NotFound, ZenContentType::kText, fmt::format("Project '{}' not found", ProjectId)); } Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound, ZenContentType::kText, fmt::format("Oplog '{}' not found in project '{}'", OplogId, ProjectId)); } Project->TouchOplog(OplogId); ProjectStore::Oplog& Oplog = *FoundLog; std::atomic_bool CancelFlag = false; ProjectStore::Oplog::ValidationResult Result = Oplog.Validate(CancelFlag, &GetSmallWorkerPool(EWorkloadType::Burst)); tsl::robin_map KeyNameLookup; KeyNameLookup.reserve(Result.OpKeys.size()); for (const auto& It : Result.OpKeys) { KeyNameLookup.insert_or_assign(It.first, It.second); } CbObjectWriter Writer; Writer << "HasMissingData" << !Result.IsEmpty(); Writer << "OpCount" << Result.OpCount; Writer << "LSNLow" << Result.LSNLow; Writer << "LSNHigh" << Result.LSNHigh; if (!Result.MissingFiles.empty()) { Writer.BeginArray("MissingFiles"); for (const auto& MissingFile : Result.MissingFiles) { Writer.BeginObject(); { Writer << "Key" << MissingFile.first; Writer << "KeyName" << KeyNameLookup[MissingFile.first]; Writer << "Id" << MissingFile.second.Id; Writer << "Hash" << MissingFile.second.Hash; Writer << "ServerPath" << MissingFile.second.ServerPath; Writer << "ClientPath" << MissingFile.second.ClientPath; } Writer.EndObject(); } Writer.EndArray(); } if (!Result.MissingChunks.empty()) { Writer.BeginArray("MissingChunks"); for (const auto& MissingChunk : Result.MissingChunks) { Writer.BeginObject(); { Writer << "Key" << MissingChunk.first; Writer << "KeyName" << KeyNameLookup[MissingChunk.first]; Writer << "Id" << MissingChunk.second.Id; Writer << "Hash" << MissingChunk.second.Hash; } Writer.EndObject(); } Writer.EndArray(); } if (!Result.MissingMetas.empty()) { Writer.BeginArray("MissingMetas"); for (const auto& MissingMeta : Result.MissingMetas) { Writer.BeginObject(); { Writer << "Key" << MissingMeta.first; Writer << "KeyName" << KeyNameLookup[MissingMeta.first]; Writer << "Id" << MissingMeta.second.Id; Writer << "Hash" << MissingMeta.second.Hash; } Writer.EndObject(); } Writer.EndArray(); } if (!Result.MissingAttachments.empty()) { Writer.BeginArray("MissingAttachments"); for (const auto& MissingMeta : Result.MissingAttachments) { Writer.BeginObject(); { Writer << "Key" << MissingMeta.first; Writer << "KeyName" << KeyNameLookup[MissingMeta.first]; Writer << "Hash" << MissingMeta.second; } Writer.EndObject(); } Writer.EndArray(); } CbObject Response = Writer.Save(); HttpReq.WriteResponse(HttpResponseCode::OK, Response); } void HttpProjectService::HandleOpLogOpRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::OplogOp"); HttpServerRequest& HttpReq = Req.ServerRequest(); const std::string_view ProjectId = Req.GetCapture(1); const std::string_view OplogId = Req.GetCapture(2); const std::string_view OpIdString = Req.GetCapture(3); Ref Project = m_ProjectStore->OpenProject(ProjectId); if (!Project) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } Project->TouchOplog(OplogId); CidStore& ChunkStore = Project->GetCidStore(); ProjectStore::Oplog& Oplog = *FoundLog; if (const std::optional OpId = ParseInt(OpIdString)) { if (std::optional MaybeOp = Oplog.GetOpByIndex(OpId.value())) { CbObject& Op = MaybeOp.value(); if (HttpReq.AcceptContentType() == ZenContentType::kCbPackage) { CbPackage Package; Package.SetObject(Op); Op.IterateAttachments([&](CbFieldView FieldView) { const IoHash AttachmentHash = FieldView.AsAttachment(); IoBuffer Payload = ChunkStore.FindChunkByCid(AttachmentHash); if (Payload) { switch (Payload.GetContentType()) { case ZenContentType::kCbObject: if (CbObject Object = LoadCompactBinaryObject(Payload)) { Package.AddAttachment(CbAttachment(Object)); } else { // Error - malformed object ZEN_WARN("malformed object returned for {}", AttachmentHash); } break; case ZenContentType::kCompressedBinary: if (CompressedBuffer Compressed = CompressedBuffer::FromCompressedNoValidate(std::move(Payload))) { Package.AddAttachment(CbAttachment(Compressed, AttachmentHash)); } else { // Error - not compressed! ZEN_WARN("invalid compressed binary returned for {}", AttachmentHash); } break; default: Package.AddAttachment(CbAttachment(SharedBuffer(Payload))); break; } } }); m_ProjectStats.OpHitCount++; return HttpReq.WriteResponse(HttpResponseCode::Accepted, Package); } else { // Client cannot accept a package, so we only send the core object m_ProjectStats.OpHitCount++; return HttpReq.WriteResponse(HttpResponseCode::Accepted, Op); } } } m_ProjectStats.OpMissCount++; return HttpReq.WriteResponse(HttpResponseCode::NotFound); } void HttpProjectService::HandleOpLogRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::Oplog"); HttpServerRequest& HttpReq = Req.ServerRequest(); using namespace std::literals; const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); Ref Project = m_ProjectStore->OpenProject(ProjectId); if (!Project) { return HttpReq.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("project {} not found", ProjectId)); } Project->TouchProject(); switch (HttpReq.RequestVerb()) { case HttpVerb::kGet: { ProjectStore::Oplog* OplogIt = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ true); if (!OplogIt) { return HttpReq.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("oplog {} not found in project {}", OplogId, ProjectId)); } Project->TouchOplog(OplogId); ProjectStore::Oplog& Log = *OplogIt; CbObjectWriter Cb; Cb << "id"sv << Log.OplogId() << "project"sv << Project->Identifier << "tempdir"sv << Log.TempPath().c_str() << "markerpath"sv << Log.MarkerPath().c_str() << "totalsize"sv << Log.TotalSize() << "opcount" << Log.OplogCount() << "expired"sv << Project->IsExpired(GcClock::TimePoint::min(), Log); HttpReq.WriteResponse(HttpResponseCode::OK, Cb.Save()); m_ProjectStats.OpLogReadCount++; } break; case HttpVerb::kPost: { if (!m_ProjectStore->AreDiskWritesAllowed()) { return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); } std::filesystem::path OplogMarkerPath; if (CbObject Params = HttpReq.ReadPayloadObject()) { OplogMarkerPath = Params["gcpath"sv].AsString(); } ProjectStore::Oplog* OplogIt = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); if (!OplogIt) { if (!Project->NewOplog(OplogId, OplogMarkerPath)) { // TODO: indicate why the operation failed! return HttpReq.WriteResponse(HttpResponseCode::InternalServerError); } Project->TouchOplog(OplogId); m_ProjectStats.OpLogWriteCount++; ZEN_INFO("established oplog '{}/{}', gc marker file at '{}'", ProjectId, OplogId, OplogMarkerPath); return HttpReq.WriteResponse(HttpResponseCode::Created); } // I guess this should ultimately be used to execute RPCs but for now, it // does absolutely nothing m_ProjectStats.BadRequestCount++; return HttpReq.WriteResponse(HttpResponseCode::BadRequest); } break; case HttpVerb::kPut: { if (!m_ProjectStore->AreDiskWritesAllowed()) { return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); } std::filesystem::path OplogMarkerPath; if (CbObject Params = HttpReq.ReadPayloadObject()) { OplogMarkerPath = Params["gcpath"sv].AsString(); } ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); if (!FoundLog) { if (!Project->NewOplog(OplogId, OplogMarkerPath)) { // TODO: indicate why the operation failed! return HttpReq.WriteResponse(HttpResponseCode::InternalServerError); } Project->TouchOplog(OplogId); m_ProjectStats.OpLogWriteCount++; ZEN_INFO("established oplog '{}/{}', gc marker file at '{}'", ProjectId, OplogId, OplogMarkerPath); return HttpReq.WriteResponse(HttpResponseCode::Created); } Project->TouchOplog(OplogId); FoundLog->Update(OplogMarkerPath); m_ProjectStats.OpLogWriteCount++; ZEN_INFO("updated oplog '{}/{}', gc marker file at '{}'", ProjectId, OplogId, OplogMarkerPath); return HttpReq.WriteResponse(HttpResponseCode::OK); } break; case HttpVerb::kDelete: { ZEN_INFO("deleting oplog '{}/{}'", ProjectId, OplogId); if (Project->DeleteOplog(OplogId)) { m_ProjectStats.OpLogDeleteCount++; return HttpReq.WriteResponse(HttpResponseCode::OK); } else { return HttpReq.WriteResponse(HttpResponseCode::Locked, HttpContentType::kText, fmt::format("oplog {}/{} is in use", ProjectId, OplogId)); } } break; default: break; } } std::optional LoadReferencedSet(ProjectStore::Oplog& Log) { using namespace std::literals; Oid ReferencedSetOplogId = OpKeyStringAsOid(OplogReferencedSet::ReferencedSetOplogKey); std::optional ReferencedSetOp = Log.GetOpByKey(ReferencedSetOplogId); if (!ReferencedSetOp) { return std::optional(); } // We expect only a single file in the "files" array; get the chunk for the first file CbFieldView FileField = *(*ReferencedSetOp)["files"sv].AsArrayView().CreateViewIterator(); Oid ChunkId = FileField.AsObjectView()["id"sv].AsObjectId(); if (ChunkId == Oid::Zero) { return std::optional(); } return OplogReferencedSet::LoadFromChunk(Log.FindChunk(ChunkId, nullptr)); } void HttpProjectService::HandleOpLogEntriesRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::OplogEntries"); using namespace std::literals; HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); Ref Project = m_ProjectStore->OpenProject(ProjectId); if (!Project) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ true); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } Project->TouchOplog(OplogId); CbObjectWriter Response; if (FoundLog->OplogCount() > 0) { std::unordered_set FieldNamesFilter; auto FilterObject = [&FieldNamesFilter](CbObjectView& Object) -> CbObject { CbObject RewrittenOp = RewriteCbObject(Object, [&FieldNamesFilter](CbObjectWriter&, CbFieldView Field) -> bool { if (FieldNamesFilter.contains(std::string(Field.GetName()))) { return false; } return true; }); return RewrittenOp; }; HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams(); if (auto FieldFilter = HttpServerRequest::Decode(Params.GetValue("fieldfilter")); !FieldFilter.empty()) { ForEachStrTok(FieldFilter, ',', [&](std::string_view FieldName) { FieldNamesFilter.insert(std::string(FieldName)); return true; }); } if (auto OpKey = Params.GetValue("opkey"); !OpKey.empty()) { Oid OpKeyId = OpKeyStringAsOid(OpKey); std::optional Op = FoundLog->GetOpByKey(OpKeyId); if (Op.has_value()) { if (FieldNamesFilter.empty()) { Response << "entry"sv << Op.value(); } else { Response << "entry"sv << FilterObject(Op.value()); } } else { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } } else { std::optional ReferencedSet; if (auto TrimString = Params.GetValue("trim_by_referencedset"); TrimString == "true") { ReferencedSet = LoadReferencedSet(*FoundLog); } Response.BeginArray("entries"sv); ProjectStore::Oplog::Paging EntryPaging; if (std::string_view Param = Params.GetValue("start"); !Param.empty()) { if (auto Value = ParseInt(Param)) { EntryPaging.Start = *Value; } } if (std::string_view Param = Params.GetValue("count"); !Param.empty()) { if (auto Value = ParseInt(Param)) { EntryPaging.Count = *Value; } } bool ShouldFilterFields = !FieldNamesFilter.empty(); FoundLog->IterateOplogWithKey( [this, &Response, &FilterObject, ShouldFilterFields, &ReferencedSet](uint32_t /* LSN */, const Oid& Key, CbObjectView Op) { if (ReferencedSet && !ReferencedSet->Contains(Key, Op["key"].AsString())) { return; } if (ShouldFilterFields) { Response << FilterObject(Op); } else { Response << Op; } }, EntryPaging); Response.EndArray(); } } if (HttpReq.AcceptContentType() == HttpContentType::kCompressedBinary) { CompositeBuffer Payload = CompressedBuffer::Compress(Response.Save().GetBuffer()).GetCompressed(); return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCompressedBinary, Payload); } else { return HttpReq.WriteResponse(HttpResponseCode::OK, Response.Save()); } } void HttpProjectService::HandleProjectRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::Project"); using namespace std::literals; HttpServerRequest& HttpReq = Req.ServerRequest(); const std::string_view ProjectId = Req.GetCapture(1); switch (HttpReq.RequestVerb()) { case HttpVerb::kPost: { if (!m_ProjectStore->AreDiskWritesAllowed()) { return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); } IoBuffer Payload = HttpReq.ReadPayload(); CbObject Params = LoadCompactBinaryObject(Payload); std::filesystem::path Root = Params["root"sv].AsU8String(); // Workspace root (i.e `D:/UE5/`) std::filesystem::path EngineRoot = Params["engine"sv].AsU8String(); // Engine root (i.e `D:/UE5/Engine`) std::filesystem::path ProjectRoot = Params["project"sv].AsU8String(); // Project root directory (i.e `D:/UE5/Samples/Games/Lyra`) std::filesystem::path ProjectFilePath = Params["projectfile"sv].AsU8String(); // Project file path (i.e `D:/UE5/Samples/Games/Lyra/Lyra.uproject`) const std::filesystem::path BasePath = m_ProjectStore->BasePath() / ProjectId; m_ProjectStore->NewProject(BasePath, ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath); ZEN_INFO("established project (id: '{}', roots: '{}', '{}', '{}', '{}'{})", ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath, ProjectFilePath.empty() ? ", project will not be GCd due to empty project file path" : ""); m_ProjectStats.ProjectWriteCount++; HttpReq.WriteResponse(HttpResponseCode::Created); } break; case HttpVerb::kPut: { if (!m_ProjectStore->AreDiskWritesAllowed()) { return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); } IoBuffer Payload = HttpReq.ReadPayload(); CbObject Params = LoadCompactBinaryObject(Payload); std::filesystem::path Root = Params["root"sv].AsU8String(); // Workspace root (i.e `D:/UE5/`) std::filesystem::path EngineRoot = Params["engine"sv].AsU8String(); // Engine root (i.e `D:/UE5/Engine`) std::filesystem::path ProjectRoot = Params["project"sv].AsU8String(); // Project root directory (i.e `D:/UE5/Samples/Games/Lyra`) std::filesystem::path ProjectFilePath = Params["projectfile"sv].AsU8String(); // Project file path (i.e `D:/UE5/Samples/Games/Lyra/Lyra.uproject`) if (m_ProjectStore->UpdateProject(ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath)) { m_ProjectStats.ProjectWriteCount++; ZEN_INFO("updated project (id: '{}', roots: '{}', '{}', '{}', '{}'{})", ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath, ProjectFilePath.empty() ? ", project will not be GCd due to empty project file path" : ""); HttpReq.WriteResponse(HttpResponseCode::OK); } else { const std::filesystem::path BasePath = m_ProjectStore->BasePath() / ProjectId; m_ProjectStore->NewProject(BasePath, ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath); m_ProjectStats.ProjectWriteCount++; ZEN_INFO("established project (id: '{}', roots: '{}', '{}', '{}', '{}'{})", ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath, ProjectFilePath.empty() ? ", project will not be GCd due to empty project file path" : ""); HttpReq.WriteResponse(HttpResponseCode::Created); } } break; case HttpVerb::kGet: { Ref Project = m_ProjectStore->OpenProject(ProjectId); if (!Project) { return HttpReq.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("project {} not found", ProjectId)); } Project->TouchProject(); std::vector OpLogs = Project->ScanForOplogs(); CbObjectWriter Response; Response << "id"sv << Project->Identifier; Response << "root"sv << PathToUtf8(Project->RootDir); Response << "engine"sv << PathToUtf8(Project->EngineRootDir); Response << "project"sv << PathToUtf8(Project->ProjectRootDir); Response << "projectfile"sv << PathToUtf8(Project->ProjectFilePath); Response.BeginArray("oplogs"sv); for (const std::string& OplogId : OpLogs) { Response.BeginObject(); Response << "id"sv << OplogId; Response.EndObject(); } Response.EndArray(); // oplogs HttpReq.WriteResponse(HttpResponseCode::OK, Response.Save()); m_ProjectStats.ProjectReadCount++; } break; case HttpVerb::kDelete: { Ref Project = m_ProjectStore->OpenProject(ProjectId); if (!Project) { return HttpReq.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("project {} not found", ProjectId)); } ZEN_INFO("deleting project '{}'", ProjectId); if (!m_ProjectStore->DeleteProject(ProjectId)) { return HttpReq.WriteResponse(HttpResponseCode::Locked, HttpContentType::kText, fmt::format("project {} is in use", ProjectId)); } m_ProjectStats.ProjectDeleteCount++; return HttpReq.WriteResponse(HttpResponseCode::NoContent); } break; default: break; } } void HttpProjectService::HandleOplogSaveRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::OplogSave"); HttpServerRequest& HttpReq = Req.ServerRequest(); if (!m_ProjectStore->AreDiskWritesAllowed()) { return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); } const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); if (HttpReq.RequestContentType() != HttpContentType::kCbObject) { m_ProjectStats.BadRequestCount++; return HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid content type"); } IoBuffer Payload = HttpReq.ReadPayload(); CbObject Response; std::pair Result = m_ProjectStore->WriteOplog(ProjectId, OplogId, std::move(Payload), Response); if (Result.first == HttpResponseCode::OK) { return HttpReq.WriteResponse(HttpResponseCode::OK, Response); } else { if (Result.first == HttpResponseCode::BadRequest) { m_ProjectStats.BadRequestCount++; } ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", ToString(HttpReq.RequestVerb()), HttpReq.QueryString(), static_cast(Result.first), Result.second); } if (Result.second.empty()) { return HttpReq.WriteResponse(Result.first); } return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } void HttpProjectService::HandleOplogLoadRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::OplogLoad"); HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); if (HttpReq.AcceptContentType() != HttpContentType::kCbObject) { m_ProjectStats.BadRequestCount++; return HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid accept content type"); } IoBuffer Payload = HttpReq.ReadPayload(); CbObject Response; std::pair Result = m_ProjectStore->ReadOplog(ProjectId, OplogId, HttpReq.GetQueryParams(), Response); if (Result.first == HttpResponseCode::OK) { return HttpReq.WriteResponse(HttpResponseCode::OK, Response); } else { if (Result.first == HttpResponseCode::BadRequest) { m_ProjectStats.BadRequestCount++; } ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", ToString(HttpReq.RequestVerb()), HttpReq.QueryString(), static_cast(Result.first), Result.second); } if (Result.second.empty()) { return HttpReq.WriteResponse(Result.first); } return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } void HttpProjectService::HandleRpcRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::Rpc"); HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); IoBuffer Payload = HttpReq.ReadPayload(); bool OkRequest = m_ProjectStore->Rpc(HttpReq, ProjectId, OplogId, std::move(Payload), m_AuthMgr); if (!OkRequest) { m_ProjectStats.BadRequestCount++; } } void HttpProjectService::HandleDetailsRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::Details"); using namespace std::literals; HttpServerRequest& HttpReq = Req.ServerRequest(); HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams(); bool CSV = Params.GetValue("csv"sv) == "true"sv; bool Details = Params.GetValue("details"sv) == "true"sv; bool OpDetails = Params.GetValue("opdetails"sv) == "true"sv; bool AttachmentDetails = Params.GetValue("attachmentdetails"sv) == "true"sv; if (CSV) { ExtendableStringBuilder<4096> CSVWriter; CSVHeader(Details, AttachmentDetails, CSVWriter); m_ProjectStore->IterateProjects([&](ProjectStore::Project& Project) { CidStore& ChunkStore = Project.GetCidStore(); Project.IterateOplogs([&](const RwLock::SharedLockScope&, ProjectStore::Oplog& Oplog) { Oplog.IterateOplogWithKey([this, &Project, &Oplog, &ChunkStore, &CSVWriter, Details, AttachmentDetails](uint32_t LSN, const Oid& Key, CbObjectView Op) { CSVWriteOp(ChunkStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, Key, Op, CSVWriter); }); }); }); HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, CSVWriter.ToView()); } else { CbObjectWriter Cbo; Cbo.BeginArray("projects"); { m_ProjectStore->DiscoverProjects(); m_ProjectStore->IterateProjects([&](ProjectStore::Project& Project) { CidStore& ChunkStore = Project.GetCidStore(); std::vector OpLogs = Project.ScanForOplogs(); CbWriteProject(ChunkStore, Project, OpLogs, Details, OpDetails, AttachmentDetails, Cbo); }); } Cbo.EndArray(); HttpReq.WriteResponse(HttpResponseCode::OK, Cbo.Save()); } } void HttpProjectService::HandleProjectDetailsRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::ProjectDetails"); using namespace std::literals; HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams(); bool CSV = Params.GetValue("csv"sv) == "true"sv; bool Details = Params.GetValue("details"sv) == "true"sv; bool OpDetails = Params.GetValue("opdetails"sv) == "true"sv; bool AttachmentDetails = Params.GetValue("attachmentdetails"sv) == "true"sv; Ref FoundProject = m_ProjectStore->OpenProject(ProjectId); if (!FoundProject) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } ProjectStore::Project& Project = *FoundProject.Get(); CidStore& ChunkStore = Project.GetCidStore(); if (CSV) { ExtendableStringBuilder<4096> CSVWriter; CSVHeader(Details, AttachmentDetails, CSVWriter); FoundProject->IterateOplogs([&](const RwLock::SharedLockScope&, ProjectStore::Oplog& Oplog) { Oplog.IterateOplogWithKey([this, &Project, &Oplog, &ChunkStore, &CSVWriter, Details, AttachmentDetails](uint32_t LSN, const Oid& Key, CbObjectView Op) { CSVWriteOp(ChunkStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, Key, Op, CSVWriter); }); }); HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, CSVWriter.ToView()); } else { CbObjectWriter Cbo; std::vector OpLogs = FoundProject->ScanForOplogs(); Cbo.BeginArray("projects"); { CbWriteProject(ChunkStore, Project, OpLogs, Details, OpDetails, AttachmentDetails, Cbo); } Cbo.EndArray(); HttpReq.WriteResponse(HttpResponseCode::OK, Cbo.Save()); } } void HttpProjectService::HandleOplogDetailsRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::OplogDetails"); using namespace std::literals; HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams(); bool CSV = Params.GetValue("csv"sv) == "true"sv; bool Details = Params.GetValue("details"sv) == "true"sv; bool OpDetails = Params.GetValue("opdetails"sv) == "true"sv; bool AttachmentDetails = Params.GetValue("attachmentdetails"sv) == "true"sv; Ref FoundProject = m_ProjectStore->OpenProject(ProjectId); if (!FoundProject) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } ProjectStore::Oplog* FoundLog = FoundProject->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } ProjectStore::Project& Project = *FoundProject.Get(); CidStore& ChunkStore = Project.GetCidStore(); ProjectStore::Oplog& Oplog = *FoundLog; if (CSV) { ExtendableStringBuilder<4096> CSVWriter; CSVHeader(Details, AttachmentDetails, CSVWriter); Oplog.IterateOplogWithKey( [this, &Project, &Oplog, &ChunkStore, &CSVWriter, Details, AttachmentDetails](uint32_t LSN, const Oid& Key, CbObjectView Op) { CSVWriteOp(ChunkStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, Key, Op, CSVWriter); }); HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, CSVWriter.ToView()); } else { CbObjectWriter Cbo; Cbo.BeginArray("oplogs"); { CbWriteOplog(ChunkStore, Oplog, Details, OpDetails, AttachmentDetails, Cbo); } Cbo.EndArray(); HttpReq.WriteResponse(HttpResponseCode::OK, Cbo.Save()); } } void HttpProjectService::HandleOplogOpDetailsRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::OplogOpDetails"); using namespace std::literals; HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); const auto& OpId = Req.GetCapture(3); HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams(); bool CSV = Params.GetValue("csv"sv) == "true"sv; bool Details = Params.GetValue("details"sv) == "true"sv; bool OpDetails = Params.GetValue("opdetails"sv) == "true"sv; bool AttachmentDetails = Params.GetValue("attachmentdetails"sv) == "true"sv; Ref FoundProject = m_ProjectStore->OpenProject(ProjectId); if (!FoundProject) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } ProjectStore::Oplog* FoundLog = FoundProject->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } if (OpId.size() != 2 * sizeof(Oid::OidBits)) { m_ProjectStats.BadRequestCount++; return HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Chunk info request for invalid chunk id '{}/{}'/'{}'", ProjectId, OplogId, OpId)); } const Oid ObjId = Oid::FromHexString(OpId); ProjectStore::Project& Project = *FoundProject.Get(); CidStore& ChunkStore = Project.GetCidStore(); ProjectStore::Oplog& Oplog = *FoundLog; std::optional Op = Oplog.GetOpByKey(ObjId); if (!Op.has_value()) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } std::optional LSN = Oplog.GetOpIndexByKey(ObjId); if (!LSN.has_value()) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } if (CSV) { ExtendableStringBuilder<4096> CSVWriter; CSVHeader(Details, AttachmentDetails, CSVWriter); CSVWriteOp(ChunkStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN.value(), ObjId, Op.value(), CSVWriter); HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, CSVWriter.ToView()); } else { CbObjectWriter Cbo; Cbo.BeginArray("ops"); { CbWriteOp(ChunkStore, Details, OpDetails, AttachmentDetails, LSN.value(), ObjId, Op.value(), Cbo); } Cbo.EndArray(); HttpReq.WriteResponse(HttpResponseCode::OK, Cbo.Save()); } } } // namespace zen