// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #include #include #include #include #include namespace zen { using namespace std::literals; ZEN_DEFINE_LOG_CATEGORY_STATIC(LogFs, "fs"sv); namespace { std::filesystem::path GetPathParameter(HttpServerRequest& ServerRequest, std::string_view Name) { if (std::string_view Value = ServerRequest.GetQueryParams().GetValue(Name); !Value.empty()) { return std::filesystem::path(HttpServerRequest::Decode(Value)); } return {}; } void WriteWorkspaceConfig(CbWriter& Writer, const Workspaces::WorkspaceConfiguration& Config) { Writer << "id" << Config.Id; Writer << "root_path" << Config.RootPath.string(); // utf8? Writer << "allow_share_creation_from_http" << Config.AllowShareCreationFromHttp; }; void WriteWorkspaceShareConfig(CbWriter& Writer, const Workspaces::WorkspaceShareConfiguration& Config) { Writer << "id" << Config.Id; Writer << "share_path" << Config.SharePath.string(); // utf8? if (!Config.Alias.empty()) { Writer << "alias" << Config.Alias; } }; void WriteWorkspaceAndSharesConfig(CbWriter& Writer, Workspaces& Workspaces, const Workspaces::WorkspaceConfiguration& WorkspaceConfig) { WriteWorkspaceConfig(Writer, WorkspaceConfig); if (std::optional> ShareIds = Workspaces.GetWorkspaceShares(WorkspaceConfig.Id); ShareIds) { Writer.BeginArray("shares"); { for (const Oid& ShareId : *ShareIds) { if (std::optional WorkspaceShareConfig = Workspaces.GetWorkspaceShareConfiguration(WorkspaceConfig.Id, ShareId); WorkspaceShareConfig) { Writer.BeginObject(); { WriteWorkspaceShareConfig(Writer, *WorkspaceShareConfig); } Writer.EndObject(); } } } Writer.EndArray(); } } } // namespace HttpWorkspacesService::HttpWorkspacesService(HttpStatusService& StatusService, HttpStatsService& StatsService, const WorkspacesServeConfig& Cfg, Workspaces& Workspaces) : m_Log(logging::Get("workspaces")) , m_StatusService(StatusService) , m_StatsService(StatsService) , m_Config(Cfg) , m_Workspaces(Workspaces) { Initialize(); } HttpWorkspacesService::~HttpWorkspacesService() { m_StatsService.UnregisterHandler("ws", *this); m_StatusService.UnregisterHandler("ws", *this); } const char* HttpWorkspacesService::BaseUri() const { return "/ws/"; } void HttpWorkspacesService::HandleRequest(HttpServerRequest& Request) { metrics::OperationTiming::Scope $(m_HttpRequests); if (m_Router.HandleRequest(Request) == false) { ZEN_LOG_WARN(LogFs, "No route found for {0}", Request.RelativeUri()); return Request.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, "Not found"sv); } } void HttpWorkspacesService::HandleStatsRequest(HttpServerRequest& HttpReq) { ZEN_TRACE_CPU("WorkspacesService::Stats"); CbObjectWriter Cbo; EmitSnapshot("requests", m_HttpRequests, Cbo); Cbo.BeginObject("workspaces"); { Cbo.BeginObject("workspace"); { Cbo << "readcount" << m_WorkspacesStats.WorkspaceReadCount << "writecount" << m_WorkspacesStats.WorkspaceWriteCount << "deletecount" << m_WorkspacesStats.WorkspaceDeleteCount; } Cbo.EndObject(); Cbo.BeginObject("workspaceshare"); { Cbo << "readcount" << m_WorkspacesStats.WorkspaceShareReadCount << "writecount" << m_WorkspacesStats.WorkspaceShareWriteCount << "deletecount" << m_WorkspacesStats.WorkspaceShareDeleteCount; } Cbo.EndObject(); Cbo.BeginObject("chunk"); { Cbo << "hitcount" << m_WorkspacesStats.WorkspaceShareChunkHitCount << "misscount" << m_WorkspacesStats.WorkspaceShareChunkMissCount; } Cbo.EndObject(); Cbo << "filescount" << m_WorkspacesStats.WorkspaceShareFilesReadCount; Cbo << "entriescount" << m_WorkspacesStats.WorkspaceShareEntriesReadCount; Cbo << "batchcount" << m_WorkspacesStats.WorkspaceShareBatchReadCount; Cbo << "requestcount" << m_WorkspacesStats.RequestCount; Cbo << "badrequestcount" << m_WorkspacesStats.BadRequestCount; } Cbo.EndObject(); return HttpReq.WriteResponse(HttpResponseCode::OK, Cbo.Save()); } void HttpWorkspacesService::HandleStatusRequest(HttpServerRequest& Request) { ZEN_TRACE_CPU("HttpWorkspacesService::Status"); CbObjectWriter Cbo; Cbo << "ok" << true; Request.WriteResponse(HttpResponseCode::OK, Cbo.Save()); } void HttpWorkspacesService::Initialize() { using namespace std::literals; ZEN_LOG_INFO(LogFs, "Initializing Workspaces Service"); m_Router.AddPattern("workspace_id", "([[:xdigit:]]{24})"); m_Router.AddPattern("share_id", "([[:xdigit:]]{24})"); m_Router.AddPattern("chunk", "([[:xdigit:]]{24})"); m_Router.AddPattern("share_alias", "([[:alnum:]_.\\+\\-\\[\\]]+)"); m_Router.RegisterRoute( "{workspace_id}/{share_id}/files", [this](HttpRouterRequest& Req) { FilesRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "{workspace_id}/{share_id}/{chunk}/info", [this](HttpRouterRequest& Req) { ChunkInfoRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "{workspace_id}/{share_id}/batch", [this](HttpRouterRequest& Req) { BatchRequest(Req); }, HttpVerb::kPost); m_Router.RegisterRoute( "{workspace_id}/{share_id}/entries", [this](HttpRouterRequest& Req) { EntriesRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "{workspace_id}/{share_id}/{chunk}", [this](HttpRouterRequest& Req) { ChunkRequest(Req); }, HttpVerb::kGet | HttpVerb::kHead); m_Router.RegisterRoute( "share/{share_alias}/files", [this](HttpRouterRequest& Req) { ShareAliasFilesRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "share/{share_alias}/{chunk}/info", [this](HttpRouterRequest& Req) { ShareAliasChunkInfoRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "share/{share_alias}/batch", [this](HttpRouterRequest& Req) { ShareAliasBatchRequest(Req); }, HttpVerb::kPost); m_Router.RegisterRoute( "share/{share_alias}/entries", [this](HttpRouterRequest& Req) { ShareAliasEntriesRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "share/{share_alias}/{chunk}", [this](HttpRouterRequest& Req) { ShareAliasChunkRequest(Req); }, HttpVerb::kGet | HttpVerb::kHead); m_Router.RegisterRoute( "share/{share_alias}", [this](HttpRouterRequest& Req) { ShareAliasRequest(Req); }, HttpVerb::kPut | HttpVerb::kGet | HttpVerb::kDelete); m_Router.RegisterRoute( "{workspace_id}/{share_id}", [this](HttpRouterRequest& Req) { ShareRequest(Req); }, HttpVerb::kPut | HttpVerb::kGet | HttpVerb::kDelete); m_Router.RegisterRoute( "{workspace_id}", [this](HttpRouterRequest& Req) { WorkspaceRequest(Req); }, HttpVerb::kPut | HttpVerb::kGet | HttpVerb::kDelete); m_Router.RegisterRoute( "refresh", [this](HttpRouterRequest& Req) { RefreshRequest(Req); }, HttpVerb::kGet); m_Router.RegisterRoute( "", [this](HttpRouterRequest& Req) { WorkspacesRequest(Req); }, HttpVerb::kGet); RefreshState(); m_StatsService.RegisterHandler("ws", *this); m_StatusService.RegisterHandler("ws", *this); } std::filesystem::path HttpWorkspacesService::GetStatePath() const { return m_Config.SystemRootDir / "workspaces"; } void HttpWorkspacesService::RefreshState() { if (!m_Config.SystemRootDir.empty()) { m_Workspaces.RefreshState(GetStatePath()); } } bool HttpWorkspacesService::MayChangeConfiguration(const HttpServerRequest& Req) const { ZEN_UNUSED(Req); return m_Config.AllowConfigurationChanges; } void HttpWorkspacesService::RefreshRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); RefreshState(); return ServerRequest.WriteResponse(HttpResponseCode::OK); } void HttpWorkspacesService::WorkspacesRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); std::vector WorkspaceIds = m_Workspaces.GetWorkspaces(); CbObjectWriter Response; Response.BeginArray("workspaces"); for (const Oid& WorkspaceId : WorkspaceIds) { if (std::optional WorkspaceConfig = m_Workspaces.GetWorkspaceConfiguration(WorkspaceId); WorkspaceConfig) { Response.BeginObject(); { WriteWorkspaceAndSharesConfig(Response, m_Workspaces, *WorkspaceConfig); } Response.EndObject(); } } Response.EndArray(); return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); } void HttpWorkspacesService::FilesRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); const Oid WorkspaceId = Oid::TryFromHexString(Req.GetCapture(1)); if (WorkspaceId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } const Oid ShareId = Oid::TryFromHexString(Req.GetCapture(2)); if (ShareId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid share id '{}'", Req.GetCapture(2))); } FilesRequest(Req, WorkspaceId, ShareId); } void HttpWorkspacesService::ChunkInfoRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); const Oid WorkspaceId = Oid::TryFromHexString(Req.GetCapture(1)); if (WorkspaceId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } const Oid ShareId = Oid::TryFromHexString(Req.GetCapture(2)); if (ShareId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid share id '{}'", Req.GetCapture(2))); } const Oid ChunkId = Oid::TryFromHexString(Req.GetCapture(3)); if (ChunkId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid chunk id '{}'", Req.GetCapture(3))); } ChunkInfoRequest(Req, WorkspaceId, ShareId, ChunkId); } void HttpWorkspacesService::BatchRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); const Oid WorkspaceId = Oid::TryFromHexString(Req.GetCapture(1)); if (WorkspaceId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } const Oid ShareId = Oid::TryFromHexString(Req.GetCapture(2)); if (ShareId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid share id '{}'", Req.GetCapture(2))); } BatchRequest(Req, WorkspaceId, ShareId); } void HttpWorkspacesService::EntriesRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); const Oid WorkspaceId = Oid::TryFromHexString(Req.GetCapture(1)); if (WorkspaceId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } const Oid ShareId = Oid::TryFromHexString(Req.GetCapture(2)); if (ShareId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid share id '{}'", Req.GetCapture(2))); } EntriesRequest(Req, WorkspaceId, ShareId); } void HttpWorkspacesService::ChunkRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); const Oid WorkspaceId = Oid::TryFromHexString(Req.GetCapture(1)); if (WorkspaceId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } const Oid ShareId = Oid::TryFromHexString(Req.GetCapture(2)); if (ShareId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid share id '{}'", Req.GetCapture(2))); } const Oid ChunkId = Oid::TryFromHexString(Req.GetCapture(3)); if (ChunkId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid chunk id '{}'", Req.GetCapture(3))); } ChunkRequest(Req, WorkspaceId, ShareId, ChunkId); } void HttpWorkspacesService::ShareRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); const Oid WorkspaceId = Oid::TryFromHexString(Req.GetCapture(1)); if (WorkspaceId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } Oid ShareId = Oid::Zero; if (Req.GetCapture(2) != Oid::Zero.ToString()) { ShareId = Oid::TryFromHexString(Req.GetCapture(2)); if (ShareId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid share id '{}'", Req.GetCapture(2))); } } ShareRequest(Req, WorkspaceId, ShareId); } void HttpWorkspacesService::WorkspaceRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); Oid WorkspaceId = Oid::TryFromHexString(Req.GetCapture(1)); switch (ServerRequest.RequestVerb()) { case HttpVerb::kPut: { std::filesystem::path WorkspacePath = GetPathParameter(ServerRequest, "root_path"sv); if (WorkspacePath.empty()) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid 'root_path' parameter"); } if (Req.GetCapture(1) == Oid::Zero.ToString()) { // Synthesize Id WorkspaceId = Workspaces::PathToId(WorkspacePath); ZEN_INFO("Generated workspace id from path '{}': {}", WorkspacePath, WorkspaceId); } else if (WorkspaceId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } if (!MayChangeConfiguration(ServerRequest)) { return ServerRequest.WriteResponse(HttpResponseCode::Unauthorized, HttpContentType::kText, fmt::format("Adding workspace {} is not allowed", WorkspaceId)); } bool AllowShareCreationFromHttp = false; if (std::string_view Value = ServerRequest.GetQueryParams().GetValue("allow_share_creation_from_http"); Value == "true"sv) { AllowShareCreationFromHttp = true; } m_WorkspacesStats.WorkspaceWriteCount++; Workspaces::WorkspaceConfiguration OldConfig = Workspaces::FindWorkspace(Log(), GetStatePath(), WorkspaceId); Workspaces::WorkspaceConfiguration NewConfig = {.Id = WorkspaceId, .RootPath = WorkspacePath, .AllowShareCreationFromHttp = AllowShareCreationFromHttp}; if (OldConfig.Id == WorkspaceId && (OldConfig != NewConfig)) { return ServerRequest.WriteResponse( HttpResponseCode::Conflict, HttpContentType::kText, fmt::format("Workspace {} already exists with root path '{}'", WorkspaceId, OldConfig.RootPath)); } else if (OldConfig.Id == Oid::Zero) { if (Workspaces::WorkspaceConfiguration ConfigWithSameRoot = Workspaces::FindWorkspace(Log(), GetStatePath(), WorkspacePath); ConfigWithSameRoot.Id != Oid::Zero) { return ServerRequest.WriteResponse( HttpResponseCode::Conflict, HttpContentType::kText, fmt::format("Workspace {} already exists with same root path '{}'", ConfigWithSameRoot.Id, WorkspacePath)); } } bool Created = Workspaces::AddWorkspace(Log(), GetStatePath(), NewConfig); if (Created) { ZEN_ASSERT(OldConfig.Id == Oid::Zero); RefreshState(); return ServerRequest.WriteResponse(HttpResponseCode::Created, HttpContentType::kText, fmt::format("{}", WorkspaceId)); } else { return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("{}", WorkspaceId)); } } case HttpVerb::kGet: { if (WorkspaceId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } m_WorkspacesStats.WorkspaceReadCount++; std::optional Workspace = m_Workspaces.GetWorkspaceConfiguration(WorkspaceId); if (Workspace) { CbObjectWriter Response; WriteWorkspaceAndSharesConfig(Response, m_Workspaces, *Workspace); return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); } else { return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } } case HttpVerb::kDelete: { if (WorkspaceId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } if (!MayChangeConfiguration(ServerRequest)) { return ServerRequest.WriteResponse(HttpResponseCode::Unauthorized, HttpContentType::kText, fmt::format("Removing workspace {} is not allowed", WorkspaceId)); } m_WorkspacesStats.WorkspaceDeleteCount++; bool Deleted = Workspaces::RemoveWorkspace(Log(), GetStatePath(), WorkspaceId); if (Deleted) { RefreshState(); return ServerRequest.WriteResponse(HttpResponseCode::OK); } return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } } } void HttpWorkspacesService::ShareAliasFilesRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); std::string_view Alias = Req.GetCapture(1); if (Alias.empty()) { return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid alias '{}'", Req.GetCapture(1))); } std::optional WorkspaceAndShareId = m_Workspaces.GetShareAlias(Alias); if (!WorkspaceAndShareId.has_value()) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } FilesRequest(Req, WorkspaceAndShareId.value().WorkspaceId, WorkspaceAndShareId.value().ShareId); } void HttpWorkspacesService::ShareAliasChunkInfoRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); std::string_view Alias = Req.GetCapture(1); if (Alias.empty()) { return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid alias '{}'", Req.GetCapture(1))); } std::optional WorkspaceAndShareId = m_Workspaces.GetShareAlias(Alias); if (!WorkspaceAndShareId.has_value()) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } const Oid ChunkId = Oid::TryFromHexString(Req.GetCapture(2)); if (ChunkId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid chunk id '{}'", Req.GetCapture(2))); } ChunkInfoRequest(Req, WorkspaceAndShareId.value().WorkspaceId, WorkspaceAndShareId.value().ShareId, ChunkId); } void HttpWorkspacesService::ShareAliasBatchRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); std::string_view Alias = Req.GetCapture(1); if (Alias.empty()) { return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid alias '{}'", Req.GetCapture(1))); } std::optional WorkspaceAndShareId = m_Workspaces.GetShareAlias(Alias); if (!WorkspaceAndShareId.has_value()) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } BatchRequest(Req, WorkspaceAndShareId.value().WorkspaceId, WorkspaceAndShareId.value().ShareId); } void HttpWorkspacesService::ShareAliasEntriesRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); std::string_view Alias = Req.GetCapture(1); if (Alias.empty()) { return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid alias '{}'", Req.GetCapture(1))); } std::optional WorkspaceAndShareId = m_Workspaces.GetShareAlias(Alias); if (!WorkspaceAndShareId.has_value()) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } EntriesRequest(Req, WorkspaceAndShareId.value().WorkspaceId, WorkspaceAndShareId.value().ShareId); } void HttpWorkspacesService::ShareAliasChunkRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); std::string_view Alias = Req.GetCapture(1); if (Alias.empty()) { return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid alias '{}'", Req.GetCapture(1))); } std::optional WorkspaceAndShareId = m_Workspaces.GetShareAlias(Alias); if (!WorkspaceAndShareId.has_value()) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } const Oid ChunkId = Oid::TryFromHexString(Req.GetCapture(2)); if (ChunkId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid chunk id '{}'", Req.GetCapture(2))); } ChunkRequest(Req, WorkspaceAndShareId.value().WorkspaceId, WorkspaceAndShareId.value().ShareId, ChunkId); } void HttpWorkspacesService::ShareAliasRequest(HttpRouterRequest& Req) { HttpServerRequest& ServerRequest = Req.ServerRequest(); std::string_view Alias = Req.GetCapture(1); if (Alias.empty()) { return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid alias '{}'", Req.GetCapture(1))); } std::optional WorkspaceAndShareId = m_Workspaces.GetShareAlias(Alias); if (!WorkspaceAndShareId.has_value()) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } ShareRequest(Req, WorkspaceAndShareId.value().WorkspaceId, WorkspaceAndShareId.value().ShareId); } void HttpWorkspacesService::FilesRequest(HttpRouterRequest& Req, const Oid& WorkspaceId, const Oid& ShareId) { HttpServerRequest& ServerRequest = Req.ServerRequest(); m_WorkspacesStats.WorkspaceShareFilesReadCount++; std::unordered_set WantedFieldNames; if (auto FieldFilter = HttpServerRequest::Decode(ServerRequest.GetQueryParams().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 = ServerRequest.GetQueryParams().GetValue("filter"sv) == "client"sv; WantedFieldNames.insert("id"); WantedFieldNames.insert("clientpath"); if (!FilterClient) { WantedFieldNames.insert("serverpath"); } } bool Refresh = false; if (auto RefreshStr = ServerRequest.GetQueryParams().GetValue("refresh"); !RefreshStr.empty()) { Refresh = StrCaseCompare(std::string(RefreshStr).c_str(), "true") == 0; } const bool WantsAllFields = WantedFieldNames.empty(); const bool WantsIdField = WantsAllFields || WantedFieldNames.contains("id"); const bool WantsClientPathField = WantsAllFields || WantedFieldNames.contains("clientpath"); const bool WantsServerPathField = WantsAllFields || WantedFieldNames.contains("serverpath"); const bool WantsRawSizeField = WantsAllFields || WantedFieldNames.contains("rawsize"); const bool WantsSizeField = WantsAllFields || WantedFieldNames.contains("size"); std::optional> Files = m_Workspaces.GetWorkspaceShareFiles(WorkspaceId, ShareId, Refresh, GetSmallWorkerPool(EWorkloadType::Burst)); if (!Files.has_value()) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } CbObjectWriter Response; Response.BeginArray("files"sv); { for (const Workspaces::ShareFile& Entry : Files.value()) { Response.BeginObject(); if (WantsIdField) { Response << "id"sv << Entry.Id; } if (WantsServerPathField) { Response << "serverpath"sv << Entry.RelativePath; } if (WantsClientPathField) { Response << "clientpath"sv << Entry.RelativePath; } if (WantsSizeField) { Response << "size"sv << Entry.Size; } if (WantsRawSizeField) { Response << "rawsize"sv << Entry.Size; } Response.EndObject(); } } Response.EndArray(); return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); } void HttpWorkspacesService::ChunkInfoRequest(HttpRouterRequest& Req, const Oid& WorkspaceId, const Oid& ShareId, const Oid& ChunkId) { HttpServerRequest& ServerRequest = Req.ServerRequest(); Workspaces::ShareFile File = m_Workspaces.GetWorkspaceShareChunkInfo(WorkspaceId, ShareId, ChunkId, GetSmallWorkerPool(EWorkloadType::Burst)); if (File.Id != Oid::Zero) { CbObjectWriter Response; Response << "size"sv << File.Size; m_WorkspacesStats.WorkspaceShareChunkHitCount++; return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); } m_WorkspacesStats.WorkspaceShareChunkMissCount++; return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } void HttpWorkspacesService::BatchRequest(HttpRouterRequest& Req, const Oid& WorkspaceId, const Oid& ShareId) { HttpServerRequest& ServerRequest = Req.ServerRequest(); IoBuffer Payload = ServerRequest.ReadPayload(); std::optional> ChunkRequests = ParseChunkBatchRequest(Payload); if (!ChunkRequests.has_value()) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "batch payload malformed"); } m_WorkspacesStats.WorkspaceShareBatchReadCount++; std::vector Requests; Requests.reserve(ChunkRequests.value().size()); std::transform(ChunkRequests.value().begin(), ChunkRequests.value().end(), std::back_inserter(Requests), [](const RequestChunkEntry& Entry) { return Workspaces::ChunkRequest{.ChunkId = Entry.ChunkId, .Offset = Entry.Offset, .Size = Entry.RequestBytes}; }); std::vector Chunks = m_Workspaces.GetWorkspaceShareChunks(WorkspaceId, ShareId, Requests, GetSmallWorkerPool(EWorkloadType::Burst)); if (Chunks.empty()) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } for (const IoBuffer& Buffer : Chunks) { if (Buffer) { m_WorkspacesStats.WorkspaceShareChunkHitCount++; } else { m_WorkspacesStats.WorkspaceShareChunkMissCount++; } } std::vector Response = BuildChunkBatchResponse(ChunkRequests.value(), Chunks); if (!Response.empty()) { return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kBinary, Response); } return ServerRequest.WriteResponse(HttpResponseCode::InternalServerError, HttpContentType::kText, fmt::format("failed formatting response for batch of {} chunks", Chunks.size())); } void HttpWorkspacesService::EntriesRequest(HttpRouterRequest& Req, const Oid& WorkspaceId, const Oid& ShareId) { HttpServerRequest& ServerRequest = Req.ServerRequest(); std::string_view OpKey = ServerRequest.GetQueryParams().GetValue("opkey"sv); if (!OpKey.empty() && OpKey != "file_manifest") { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } std::unordered_set WantedFieldNames; if (auto FieldFilter = HttpServerRequest::Decode(ServerRequest.GetQueryParams().GetValue("fieldfilter")); !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; }); } } bool Refresh = false; if (auto RefreshStr = ServerRequest.GetQueryParams().GetValue("refresh"); !RefreshStr.empty()) { Refresh = StrCaseCompare(std::string(RefreshStr).c_str(), "true") == 0; } m_WorkspacesStats.WorkspaceShareEntriesReadCount++; std::optional> Files = m_Workspaces.GetWorkspaceShareFiles(WorkspaceId, ShareId, Refresh, GetSmallWorkerPool(EWorkloadType::Burst)); if (!Files.has_value()) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } const bool WantsAllFields = WantedFieldNames.empty(); const bool WantsIdField = WantsAllFields || WantedFieldNames.contains("id"); const bool WantsClientPathField = WantsAllFields || WantedFieldNames.contains("clientpath"); const bool WantsServerPathField = WantsAllFields || WantedFieldNames.contains("serverpath"); CbObjectWriter Response; if (OpKey.empty()) { Response.BeginArray("entries"sv); Response.BeginObject(); } else { Response.BeginObject("entry"sv); } { // Synthesize a fake op Response << "key" << "file_manifest"; Response.BeginArray("files"); { for (const Workspaces::ShareFile& Entry : Files.value()) { Response.BeginObject(); { if (WantsIdField) { Response << "id"sv << Entry.Id; } if (WantsServerPathField) { Response << "serverpath"sv << Entry.RelativePath; } if (WantsClientPathField) { Response << "clientpath"sv << Entry.RelativePath; } } Response.EndObject(); } } Response.EndArray(); } if (OpKey.empty()) { Response.EndObject(); Response.EndArray(); } else { Response.EndObject(); } return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); } void HttpWorkspacesService::ChunkRequest(HttpRouterRequest& Req, const Oid& WorkspaceId, const Oid& ShareId, const Oid& ChunkId) { HttpServerRequest& ServerRequest = Req.ServerRequest(); uint64_t Offset = 0; uint64_t Size = ~(0ull); if (auto OffsetParm = ServerRequest.GetQueryParams().GetValue("offset"); OffsetParm.empty() == false) { if (auto OffsetVal = ParseInt(OffsetParm)) { Offset = OffsetVal.value(); } else { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid offset parameter '{}'", OffsetParm)); } } if (auto SizeParm = ServerRequest.GetQueryParams().GetValue("size"); SizeParm.empty() == false) { if (auto SizeVal = ParseInt(SizeParm)) { Size = SizeVal.value(); } else { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid size parameter '{}'", SizeParm)); } } std::vector Response = m_Workspaces.GetWorkspaceShareChunks( WorkspaceId, ShareId, std::vector{Workspaces::ChunkRequest{.ChunkId = ChunkId, .Offset = Offset, .Size = Size}}, GetSmallWorkerPool(EWorkloadType::Burst)); if (!Response.empty() && Response[0]) { m_WorkspacesStats.WorkspaceShareChunkHitCount++; if (Response[0].GetSize() == 0) { return ServerRequest.WriteResponse(HttpResponseCode::OK); } return ServerRequest.WriteResponse(HttpResponseCode::OK, Response[0].GetContentType(), Response); } m_WorkspacesStats.WorkspaceShareChunkMissCount++; return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } void HttpWorkspacesService::ShareRequest(HttpRouterRequest& Req, const Oid& WorkspaceId, const Oid& InShareId) { Oid ShareId = InShareId; HttpServerRequest& ServerRequest = Req.ServerRequest(); switch (ServerRequest.RequestVerb()) { case HttpVerb::kPut: { std::filesystem::path SharePath = GetPathParameter(ServerRequest, "share_path"sv); if (SharePath.empty()) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid 'share_path' parameter"); } if (ShareId == Oid::Zero) { // Synthesize Id ShareId = Workspaces::PathToId(SharePath); ZEN_INFO("Generated workspace id from path '{}': {}", SharePath, ShareId); } std::string Alias = HttpServerRequest::Decode(ServerRequest.GetQueryParams().GetValue("alias"sv)); if (!AsciiSet::HasOnly(Alias, Workspaces::ValidAliasCharactersSet)) { return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid 'alias' parameter"); } Workspaces::WorkspaceConfiguration Workspace = Workspaces::FindWorkspace(Log(), GetStatePath(), WorkspaceId); if (Workspace.Id == Oid::Zero) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("Workspace '{}' does not exist", WorkspaceId)); } if (!Workspace.AllowShareCreationFromHttp) { if (!MayChangeConfiguration(ServerRequest)) { return ServerRequest.WriteResponse( HttpResponseCode::Unauthorized, HttpContentType::kText, fmt::format("Adding workspace share {} in workspace {} is not allowed", WorkspaceId, ShareId)); } } m_WorkspacesStats.WorkspaceShareWriteCount++; const Workspaces::WorkspaceShareConfiguration OldConfig = Workspaces::FindWorkspaceShare(Log(), Workspace.RootPath, ShareId); const Workspaces::WorkspaceShareConfiguration NewConfig = {.Id = ShareId, .SharePath = SharePath, .Alias = std::string(Alias)}; if (OldConfig.Id == ShareId && (OldConfig != NewConfig)) { return ServerRequest.WriteResponse( HttpResponseCode::Conflict, HttpContentType::kText, fmt::format("Workspace share '{}' already exist in workspace '{}' with share path '{}' and alias '{}'", ShareId, WorkspaceId, OldConfig.SharePath, OldConfig.Alias)); } else if (OldConfig.Id == Oid::Zero) { if (Workspaces::WorkspaceShareConfiguration ConfigWithSamePath = Workspaces::FindWorkspaceShare(Log(), Workspace.RootPath, SharePath); ConfigWithSamePath.Id != Oid::Zero) { return ServerRequest.WriteResponse( HttpResponseCode::Conflict, HttpContentType::kText, fmt::format("Workspace share '{}' already exist in workspace '{}' with same share path '{}' and alias '{}'", ShareId, WorkspaceId, OldConfig.SharePath, OldConfig.Alias)); } } if (!IsDir(Workspace.RootPath / NewConfig.SharePath)) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("directory {} does not exist in workspace {} root '{}'", NewConfig.SharePath, WorkspaceId, Workspace.RootPath)); } bool Created = Workspaces::AddWorkspaceShare(Log(), Workspace.RootPath, NewConfig); if (Created) { RefreshState(); return ServerRequest.WriteResponse(HttpResponseCode::Created, HttpContentType::kText, fmt::format("{}", ShareId)); } return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("{}", ShareId)); } case HttpVerb::kGet: { if (WorkspaceId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } if (ShareId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid share id '{}'", ShareId)); } m_WorkspacesStats.WorkspaceShareReadCount++; std::optional Config = m_Workspaces.GetWorkspaceShareConfiguration(WorkspaceId, ShareId); if (!Config) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } CbObjectWriter Response; WriteWorkspaceShareConfig(Response, *Config); return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save()); } case HttpVerb::kDelete: { if (WorkspaceId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid workspace id '{}'", Req.GetCapture(1))); } if (ShareId == Oid::Zero) { m_WorkspacesStats.BadRequestCount++; return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, fmt::format("Invalid share id '{}'", ShareId)); } Workspaces::WorkspaceConfiguration Workspace = Workspaces::FindWorkspace(Log(), GetStatePath(), WorkspaceId); if (Workspace.Id == Oid::Zero) { return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } if (!Workspace.AllowShareCreationFromHttp) { if (!MayChangeConfiguration(ServerRequest)) { return ServerRequest.WriteResponse( HttpResponseCode::Unauthorized, HttpContentType::kText, fmt::format("Removing workspace share {} in workspace {} is not allowed", WorkspaceId, ShareId)); } } m_WorkspacesStats.WorkspaceShareDeleteCount++; bool Deleted = Workspaces::RemoveWorkspaceShare(Log(), Workspace.RootPath, ShareId); if (Deleted) { RefreshState(); return ServerRequest.WriteResponse(HttpResponseCode::OK); } return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } } } } // namespace zen