// Copyright Epic Games, Inc. All Rights Reserved. #include "projectstore_cmd.h" #include #include #include #include #include #include #include #include #include #include #include #include #include ZEN_THIRD_PARTY_INCLUDES_START #include #include ZEN_THIRD_PARTY_INCLUDES_END #include namespace zen { namespace { using namespace std::literals; const std::string DefaultJupiterAccessTokenEnvVariableName( #if ZEN_PLATFORM_WINDOWS "UE-CloudDataCacheAccessToken"sv #endif #if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC "UE_CloudDataCacheAccessToken"sv #endif ); std::string ReadJupiterAccessTokenFromFile(const std::filesystem::path& Path) { if (!IsFile(Path)) { throw std::runtime_error(fmt::format("the file '{}' does not exist", Path)); } IoBuffer Body = IoBufferBuilder::MakeFromFile(Path); std::string JsonText(reinterpret_cast(Body.GetData()), Body.GetSize()); std::string JsonError; json11::Json TokenInfo = json11::Json::parse(JsonText, JsonError); if (!JsonError.empty()) { throw std::runtime_error(fmt::format("failed parsing json file '{}'. Reason: '{}'", Path, JsonError)); } const std::string AuthToken = TokenInfo["Token"].string_value(); if (AuthToken.empty()) { throw std::runtime_error(fmt::format("the json file '{}' does not contain a value for \"Token\"", Path)); } return AuthToken; } std::filesystem::path FindOidcTokenExePath(std::string_view OidcTokenAuthExecutablePath) { if (OidcTokenAuthExecutablePath.empty()) { const std::string OidcExecutableName = "OidcToken" ZEN_EXE_SUFFIX_LITERAL; std::filesystem::path OidcTokenPath = (GetRunningExecutablePath().parent_path() / OidcExecutableName).make_preferred(); if (IsFile(OidcTokenPath)) { return OidcTokenPath; } OidcTokenPath = (std::filesystem::current_path() / OidcExecutableName).make_preferred(); if (IsFile(OidcTokenPath)) { return OidcTokenPath; } } else { std::filesystem::path OidcTokenPath = std::filesystem::absolute(StringToPath(OidcTokenAuthExecutablePath)).make_preferred(); if (IsFile(OidcTokenPath)) { return OidcTokenPath; } } return {}; }; void WriteAuthOptions(CbObjectWriter& Writer, std::string_view JupiterOpenIdProvider, std::string_view JupiterAccessToken, std::string_view JupiterAccessTokenEnv, std::string_view JupiterAccessTokenPath, std::string_view OidcTokenAuthExecutablePath) { if (!JupiterOpenIdProvider.empty()) { Writer.AddString("openid-provider"sv, JupiterOpenIdProvider); } if (!JupiterAccessToken.empty()) { Writer.AddString("access-token"sv, JupiterAccessToken); } if (!JupiterAccessTokenPath.empty()) { std::string ResolvedCloudAccessToken = ReadJupiterAccessTokenFromFile(JupiterAccessTokenPath); if (!ResolvedCloudAccessToken.empty()) { Writer.AddString("access-token"sv, ResolvedCloudAccessToken); } } if (!JupiterAccessTokenEnv.empty()) { std::string ResolvedCloudAccessTokenEnv = GetEnvVariable(JupiterAccessTokenEnv); if (!ResolvedCloudAccessTokenEnv.empty()) { Writer.AddString("access-token"sv, ResolvedCloudAccessTokenEnv); } else { Writer.AddString("access-token-env"sv, JupiterAccessTokenEnv); } } if (std::filesystem::path OidcTokenExePath = FindOidcTokenExePath(OidcTokenAuthExecutablePath); !OidcTokenExePath.empty()) { Writer.AddString("oidc-exe-path"sv, OidcTokenExePath.generic_string()); } } IoBuffer MakeCbObjectPayload(std::function WriteCB) { CbObjectWriter Writer; WriteCB(Writer); IoBuffer Payload = Writer.Save().GetBuffer().AsIoBuffer(); Payload.SetContentType(ZenContentType::kCbObject); return Payload; }; static std::atomic_uint32_t SignalCounter[NSIG] = {0}; static void SignalCallbackHandler(int SigNum) { if (SigNum >= 0 && SigNum < NSIG) { SignalCounter[SigNum].fetch_add(1); } } class AsyncJobError : public std::runtime_error { public: using _Mybase = runtime_error; AsyncJobError(const std::string& Message, int ReturnCode) : _Mybase(Message), m_ReturnCode(ReturnCode) {} const int m_ReturnCode = 0; }; void ExecuteAsyncOperation(HttpClient& Http, std::string_view Url, IoBuffer&& Payload, bool PlainProgress) { signal(SIGINT, SignalCallbackHandler); #if ZEN_PLATFORM_WINDOWS signal(SIGBREAK, SignalCallbackHandler); #endif // ZEN_PLATFORM_WINDOWS if (HttpClient::Response Result = Http.Post(Url, Payload)) { if (Result.StatusCode == HttpResponseCode::Accepted) { bool Cancelled = false; std::string_view JobIdText = Result.AsText(); std::optional JobIdMaybe = ParseInt(JobIdText); if (!JobIdMaybe) { throw std::runtime_error(fmt::format("invalid job id returned, received '{}'", JobIdText)); } ProgressBar ProgressBar(PlainProgress ? ProgressBar::Mode::Plain : ProgressBar::Mode::Pretty, ""sv); auto OuputMessages = [&](CbObjectView StatusObject) { CbArrayView Messages = StatusObject["Messages"sv].AsArrayView(); if (Messages.Num() > 0) { ProgressBar.ForceLinebreak(); for (auto M : Messages) { std::string_view Message = M.AsString(); ZEN_CONSOLE("{}", Message); } } }; uint64_t JobId = JobIdMaybe.value(); while (true) { HttpClient::Response StatusResult = Http.Get(fmt::format("/admin/jobs/{}", JobId), HttpClient::Accept(ZenContentType::kCbObject)); if (!StatusResult) { StatusResult.ThrowError("failed to create project"sv); } CbObject StatusObject = StatusResult.AsObject(); std::string_view Status = StatusObject["Status"sv].AsString(); bool MessagesDone = false; if (Status == "Running") { std::string_view CurrentOp = StatusObject["Op"sv].AsString(); std::string_view CurrentOpDetails = StatusObject["Details"sv].AsString(); uint64_t TotalCount = StatusObject["TotalCount"sv].AsUInt64(); uint64_t RemainingCount = StatusObject["RemainingCount"sv].AsUInt64(); if (!ProgressBar.IsSameTask(CurrentOp)) { ProgressBar.Finish(); } if (!ProgressBar.HasActiveTask()) { OuputMessages(StatusObject); MessagesDone = true; } ProgressBar.UpdateState({.Task = std::string(CurrentOp), .Details = std::string(CurrentOpDetails), .TotalCount = TotalCount, .RemainingCount = RemainingCount}, false); } if ((Status == "Complete") || (Status == "Aborted")) { ProgressBar.Finish(); } if (!MessagesDone) { OuputMessages(StatusObject); } if (Status == "Complete") { if (Cancelled) { ZEN_CONSOLE("Cancelled"); } else { double QueueTimeS = StatusObject["QueueTimeS"].AsDouble(); double RuntimeS = StatusObject["RunTimeS"].AsDouble(); ZEN_CONSOLE("Completed: QueueTime: {}, RunTime: {}", NiceTimeSpanMs(static_cast(QueueTimeS * 1000.0)), NiceTimeSpanMs(static_cast(RuntimeS * 1000.0))); } break; } if (Status == "Aborted") { std::string_view AbortReason = StatusObject["AbortReason"].AsString(); int ReturnCode = StatusObject["ReturnCode"].AsInt32(-1); if (!AbortReason.empty()) { throw AsyncJobError(std::string(AbortReason), ReturnCode); } else { throw AsyncJobError("Aborted", ReturnCode); } break; } if (Status == "Queued") { double QueueTimeS = StatusObject["QueueTimeS"].AsDouble(); ZEN_CONSOLE("Queued, waited {}...", NiceTimeSpanMs(static_cast(QueueTimeS * 1000.0))); } uint32_t InterruptCounter = SignalCounter[SIGINT].load(); uint32_t BreakCounter = 0; #if ZEN_PLATFORM_WINDOWS BreakCounter = SignalCounter[SIGBREAK].load(); #endif // ZEN_PLATFORM_WINDOWS if (InterruptCounter > 0 || BreakCounter > 0) { SignalCounter[SIGINT].fetch_sub(InterruptCounter); #if ZEN_PLATFORM_WINDOWS SignalCounter[SIGBREAK].fetch_sub(BreakCounter); #endif // ZEN_PLATFORM_WINDOWS if (HttpClient::Response DeleteResult = Http.Delete(fmt::format("/admin/jobs/{}", JobId))) { ProgressBar.ForceLinebreak(); ZEN_CONSOLE("Requested cancel..."); Cancelled = true; } else { ProgressBar.ForceLinebreak(); ZEN_CONSOLE("Failed cancelling job {}", DeleteResult); } continue; } if (PlainProgress) { Sleep(5000); } else { Sleep(500); } } } else { ZEN_CONSOLE("{}", Result); } } else { Result.ThrowError("failed to start operation"sv); } } std::vector GetProjectIds(HttpClient& Http) { std::vector AvailableProjects; if (HttpClient::Response Result = Http.Get("/prj")) { CbObject CompactBinary = Result.AsObject(); for (CbFieldView Field : CompactBinary.AsFieldView().AsArrayView()) { if (auto Id = Field.AsObjectView()["Id"].AsString(); !Id.empty()) { AvailableProjects.push_back(std::string(Id)); } } } else { Result.ThrowError(fmt::format("failed to fetch available projects from '{}'", Http.GetBaseUri())); } return AvailableProjects; } std::vector GetOlogIds(HttpClient& Http, std::string_view ProjectId) { std::vector AvailableOplogs; if (HttpClient::Response Result = Http.Get(fmt::format("/prj/{}", ProjectId))) { CbObject CompactBinary = Result.AsObject(); for (CbFieldView Field : CompactBinary["oplogs"].AsArrayView()) { if (auto Id = Field.AsObjectView()["id"].AsString(); !Id.empty()) { AvailableOplogs.push_back(std::string(Id)); } } } else { Result.ThrowError(fmt::format("failed to fetch available oplogs from '{}' for project '{}'", Http.GetBaseUri(), ProjectId)); } return AvailableOplogs; } std::vector MatchId(const std::vector& Ids, std::string_view FindId) { bool FindHasDotDelimiter = FindId.find('.') != std::string_view::npos; std::vector PossibleIds; for (const std::string& Id : Ids) { if (Id == FindId) { return std::vector{Id}; } else if (Id.starts_with(FindId)) { if (FindHasDotDelimiter || Id[FindId.length()] == '.') { PossibleIds.push_back(Id); } } } return PossibleIds; } std::string FmtArray(const std::vector& Values, std::string_view Separator = ", "sv, std::string_view Quotes = "'"sv) { ExtendableStringBuilder<512> SB; for (const std::string& Value : Values) { if (SB.Size() > 0 && !Separator.empty()) { SB.Append(Separator); } if (!Quotes.empty()) { SB.Append(Quotes); } SB.Append(Value); if (!Quotes.empty()) { SB.Append(Quotes); } } return SB.ToString(); } std::string FmtProjectIdArray(const std::vector& ProjectIds, std::string_view Separator = ", "sv, std::string_view Quotes = "'"sv) { std::vector PrettyProjectIds; PrettyProjectIds.reserve(ProjectIds.size()); for (const std::string& ProjectId : ProjectIds) { auto PrettyProjectId = [Quotes, &ProjectIds](const std::string& ProjectId) -> std::string { if (std::string::size_type DotPos = ProjectId.find('.'); DotPos != std::string::npos) { const std::string ConflictProjectId = ProjectId.substr(0, DotPos + 1); auto It = std::find_if(ProjectIds.begin(), ProjectIds.end(), [&ProjectId, &ConflictProjectId](const std::string& CheckForConflictProjectId) { return (ProjectId != CheckForConflictProjectId) && CheckForConflictProjectId.starts_with(ConflictProjectId); }); if (It == ProjectIds.end()) { return fmt::format("{}{}{} ({})", Quotes, ProjectId.substr(0, DotPos), Quotes, ProjectId); } } return fmt::format("{}{}{}", Quotes, ProjectId, Quotes); }; PrettyProjectIds.push_back(PrettyProjectId(ProjectId)); } return FmtArray(PrettyProjectIds, Separator, ""); } std::string ResolveProject(HttpClient& Http, std::string_view OptionalProjectName) { std::vector AvailableProjects = GetProjectIds(Http); if (AvailableProjects.empty()) { if (OptionalProjectName.empty()) { ZEN_CONSOLE("No projects found at {}", Http.GetBaseUri()); } else { ZEN_CONSOLE("Unable to resolve project name '{}', no projects found at {}", OptionalProjectName, Http.GetBaseUri()); } return {}; } if (OptionalProjectName.empty()) { if (AvailableProjects.size() == 1) { return AvailableProjects.front(); } else { ZEN_CONSOLE("Available projects at {}: {}", Http.GetBaseUri(), FmtProjectIdArray(AvailableProjects)); return {}; } } std::vector MatchingProjectIds = MatchId(AvailableProjects, OptionalProjectName); if (MatchingProjectIds.empty()) { ZEN_CONSOLE("Unable to match project name '{}' at {}, available projects: {}", OptionalProjectName, Http.GetBaseUri(), FmtProjectIdArray(AvailableProjects)); return {}; } if (MatchingProjectIds.size() == 1) { return MatchingProjectIds.front(); } ZEN_CONSOLE("Project name is ambigous '{}' at {}: possible matches: {}", OptionalProjectName, Http.GetBaseUri(), FmtProjectIdArray(MatchingProjectIds)); return {}; } std::string ResolveOplog(HttpClient& Http, std::string_view ProjectName, std::string_view OptionalOplogName) { std::vector AvailableOplogs = GetOlogIds(Http, ProjectName); if (AvailableOplogs.empty()) { if (OptionalOplogName.empty()) { ZEN_CONSOLE("No oplogs for project '{}' found at {}", ProjectName, Http.GetBaseUri()); } else { ZEN_CONSOLE("Unable to resolve oplog name '{}' for project '{}', no oplogs found at {}", OptionalOplogName, ProjectName, Http.GetBaseUri()); } return {}; } if (OptionalOplogName.empty()) { if (AvailableOplogs.size() == 1) { return AvailableOplogs.front(); } else { ZEN_CONSOLE("Available oplogs for project '{}' at {}: {}", ProjectName, Http.GetBaseUri(), FmtArray(AvailableOplogs)); return {}; } } std::vector MatchingOplogIds = MatchId(AvailableOplogs, OptionalOplogName); if (MatchingOplogIds.empty()) { ZEN_CONSOLE("Unable to match oplog name '{}' for project '{}', available oplogs at {}: {}", OptionalOplogName, ProjectName, Http.GetBaseUri(), FmtArray(AvailableOplogs)); return {}; } if (MatchingOplogIds.size() == 1) { return MatchingOplogIds.front(); } ZEN_CONSOLE("Oplog name '{}' for project '{}' at {}, is ambigous, possible matches: {}", OptionalOplogName, ProjectName, Http.GetBaseUri(), FmtArray(MatchingOplogIds)); return {}; } } // namespace /////////////////////////////////////// DropProjectCommand::DropProjectCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectName), ""); m_Options.add_option("", "o", "oplog", "Oplog name", cxxopts::value(m_OplogName), ""); m_Options.add_option("", "", "dryrun", "Dry run - resolve arguments but do not drop", cxxopts::value(m_DryRun), ""); m_Options.parse_positional({"project", "oplog"}); m_Options.positional_help("[ []]"); } DropProjectCommand::~DropProjectCommand() { } int DropProjectCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return 0; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { throw OptionParseException("unable to resolve server specification"); } HttpClient Http(m_HostName); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { return 1; } if (m_OplogName.empty()) { if (m_DryRun) { ZEN_CONSOLE("Would drop project '{}' from '{}'. Use --dryrun=false to execute the drop operation.", m_ProjectName, m_HostName); } else { ZEN_CONSOLE("Dropping project '{}' from '{}'", m_ProjectName, m_HostName); if (HttpClient::Response Result = Http.Delete(fmt::format("/prj/{}", m_ProjectName))) { ZEN_CONSOLE("{}", Result); } else { Result.ThrowError("delete project failed"sv); return 1; } } } else { m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { return 1; } if (m_DryRun) { ZEN_CONSOLE("Would drop oplog '{}/{}' from '{}'. Add --dryrun=false to execute the drop operation.", m_ProjectName, m_OplogName, m_HostName); } else { ZEN_CONSOLE("Dropping oplog '{}/{}' from '{}'", m_ProjectName, m_OplogName, m_HostName); if (HttpClient::Response Result = Http.Delete(fmt::format("/prj/{}/oplog/{}", m_ProjectName, m_OplogName))) { ZEN_CONSOLE("{}", Result); } else { Result.ThrowError("delete oplog failed"sv); return 1; } } } return 0; } /////////////////////////////////////// ProjectInfoCommand::ProjectInfoCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectName), ""); m_Options.add_option("", "o", "oplog", "Oplog name", cxxopts::value(m_OplogName), ""); m_Options.parse_positional({"project", "oplog"}); m_Options.positional_help("[ []]"); } ProjectInfoCommand::~ProjectInfoCommand() { } int ProjectInfoCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return 0; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { throw OptionParseException("unable to resolve server specification"); } if (!m_OplogName.empty() && m_ProjectName.empty()) { throw OptionParseException("an oplog can't be specified without also specifying a project"); } HttpClient Http(m_HostName); std::string Url; if (m_ProjectName.empty()) { Url = "/prj"; ZEN_CONSOLE("Info from '{}'", Url); } else if (m_OplogName.empty()) { m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { return 1; } Url = fmt::format("/prj/{}", m_ProjectName); ZEN_CONSOLE("Info on project '{}' from '{}{}'", m_ProjectName, m_HostName, Url); } else { m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { return 1; } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { return 1; } Url = fmt::format("/prj/{}/oplog/{}", m_ProjectName, m_OplogName); ZEN_CONSOLE("Info on oplog '{}/{}' from '{}{}'", m_ProjectName, m_OplogName, m_HostName, Url); } if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result.ToText()); return 0; } else { Result.ThrowError("failed to fetch info"sv); return 1; } } /////////////////////////////////////// CreateProjectCommand::CreateProjectCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectId), ""); m_Options.add_option("", "", "rootdir", "Absolute path to root directory", cxxopts::value(m_RootDir), ""); m_Options.add_option("", "", "enginedir", "Absolute path to engine root directory", cxxopts::value(m_EngineRootDir), ""); m_Options.add_option("", "", "projectdir", "Absolute path to project directory", cxxopts::value(m_ProjectRootDir), ""); m_Options.add_option("", "", "projectfile", "Absolute path to .uproject file", cxxopts::value(m_ProjectFile), ""); m_Options.add_option("", "f", "force-update", "Force update of existing project", cxxopts::value(m_ForceUpdate), ""); m_Options.parse_positional({"project", "rootdir", "enginedir", "projectdir", "projectfile"}); } CreateProjectCommand::~CreateProjectCommand() = default; int CreateProjectCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); using namespace std::literals; if (!ParseOptions(argc, argv)) { return 0; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { throw OptionParseException("unable to resolve server specification"); } if (m_ProjectId.empty()) { ZEN_ERROR("Project name must be given"); return 1; } HttpClient Http(m_HostName); std::string Url = fmt::format("/prj/{}", m_ProjectId); if (!m_ForceUpdate) { if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("Project already exists.\n{}", Result.ToText()); return 1; } } IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("id"sv, m_ProjectId); Writer.AddString("root"sv, m_RootDir); Writer.AddString("engine"sv, m_EngineRootDir); Writer.AddString("project"sv, m_ProjectRootDir); Writer.AddString("projectfile"sv, m_ProjectFile); }); if (HttpClient::Response Result = m_ForceUpdate ? Http.Put(Url, Payload, HttpClient::Accept(ZenContentType::kText)) : Http.Post(Url, Payload, HttpClient::Accept(ZenContentType::kText))) { ZEN_CONSOLE("{}", Result); return 0; } else { Result.ThrowError("failed to create project"sv); return 1; } } /////////////////////////////////////// CreateOplogCommand::CreateOplogCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectId), ""); m_Options.add_option("", "o", "oplog", "Oplog name", cxxopts::value(m_OplogId), ""); m_Options.add_option("", "", "gcpath", "Absolute path to oplog lifetime marker file", cxxopts::value(m_GcPath), ""); m_Options.add_option("", "f", "force-update", "Force update of existing oplog", cxxopts::value(m_ForceUpdate), ""); m_Options.parse_positional({"project", "oplog", "gcpath"}); } CreateOplogCommand::~CreateOplogCommand() = default; int CreateOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); using namespace std::literals; if (!ParseOptions(argc, argv)) { return 0; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { throw OptionParseException("unable to resolve server specification"); } if (m_ProjectId.empty()) { throw OptionParseException("project name must be specified"); } HttpClient Http(m_HostName); m_ProjectId = ResolveProject(Http, m_ProjectId); if (m_ProjectId.empty()) { return 1; } if (m_OplogId.empty()) { throw OptionParseException("oplog name must be specified"); } std::string Url = fmt::format("/prj/{}/oplog/{}", m_ProjectId, m_OplogId); if (!m_ForceUpdate) { if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("Oplog already exists.\n{}", Result.ToText()); return 1; } } IoBuffer OplogPayload; if (!m_GcPath.empty()) { OplogPayload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("gcpath"sv, m_GcPath); }); } if (HttpClient::Response Result = m_ForceUpdate ? Http.Put(Url, OplogPayload, HttpClient::Accept(ZenContentType::kText)) : Http.Post(Url, OplogPayload, HttpClient::Accept(ZenContentType::kText))) { ZEN_CONSOLE("{}", Result); return 0; } else { Result.ThrowError("failed to create oplog"sv); return 1; } } /////////////////////////////////////// ExportOplogCommand::ExportOplogCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectName), ""); m_Options.add_option("", "o", "oplog", "Oplog name", cxxopts::value(m_OplogName), ""); m_Options.add_option("", "", "maxblocksize", "Max size for bundled attachments", cxxopts::value(m_MaxBlockSize), ""); m_Options.add_option("", "", "maxchunkembedsize", "Max size for attachment to be bundled", cxxopts::value(m_MaxChunkEmbedSize), ""); m_Options.add_option("", "", "embedloosefiles", "Export additional files referenced by path as attachments", cxxopts::value(m_EmbedLooseFiles), ""); m_Options.add_option("", "f", "force", "Force export of all attachments", cxxopts::value(m_Force), ""); m_Options.add_option("", "", "ignore-missing-attachments", "Continue exporting oplog even if attachments are missing", cxxopts::value(m_IgnoreMissingAttachments), ""); m_Options.add_option("", "", "disableblocks", "Disable block creation and save all attachments individually (applies to file and cloud target)", cxxopts::value(m_DisableBlocks), ""); m_Options.add_option("", "a", "async", "Trigger export but don't wait for completion", cxxopts::value(m_Async), ""); m_Options.add_option("", "", "namespace", "Cloud/Builds Storage namespace", cxxopts::value(m_JupiterNamespace), ""); m_Options.add_option("", "", "bucket", "Cloud/Builds Storage bucket", cxxopts::value(m_JupiterBucket), ""); m_Options.add_option("", "", "openid-provider", "Cloud/Builds Storage openid provider", cxxopts::value(m_JupiterOpenIdProvider), ""); m_Options .add_option("", "", "access-token", "Cloud/Builds Storage access token", cxxopts::value(m_JupiterAccessToken), ""); m_Options.add_option("", "", "access-token-env", "Name of environment variable that holds the cloud/builds Storage access token", cxxopts::value(m_JupiterAccessTokenEnv)->default_value(DefaultJupiterAccessTokenEnvVariableName), ""); m_Options.add_option("", "", "access-token-path", "Path to json file that holds the cloud/builds Storage access token", cxxopts::value(m_JupiterAccessTokenPath), ""); m_Options.add_option("", "", "oidctoken-exe-path", "Path to OidcToken executable", cxxopts::value(m_OidcTokenAuthExecutablePath)->default_value(""), ""); m_Options.add_option("", "", "assume-http2", "Assume that the cloud/builds endpoint is a HTTP/2 endpoint skipping HTTP/1.1 upgrade handshake", cxxopts::value(m_JupiterAssumeHttp2), ""); m_Options.add_option( "", "", "disabletempblocks", "Disable temp block creation and upload blocks without waiting for oplog container to be uploaded for cloud/builds storage", cxxopts::value(m_JupiterDisableTempBlocks), ""); m_Options.add_option("", "", "cloud", "Cloud Storage URL", cxxopts::value(m_CloudUrl), ""); m_Options.add_option("cloud", "", "key", "Cloud Storage key", cxxopts::value(m_CloudKey), ""); m_Options.add_option("cloud", "", "basekey", "Optional Base Cloud Storage key for incremental export", cxxopts::value(m_BaseCloudKey), ""); m_Options.add_option("", "", "builds", "Builds Storage API URL", cxxopts::value(m_BuildsUrl), ""); m_Options.add_option("builds", "", "builds-id", "Builds Id", cxxopts::value(m_BuildsId), ""); m_Options.add_option("builds", "", "builds-metadata-path", "Path to json file that holds the metadata for the build", cxxopts::value(m_BuildsMetadataPath), ""); m_Options.add_option("builds", "", "builds-metadata", "Key-value pairs separated by ';' with build meta data. (key1=value1;key2=value2)", cxxopts::value(m_BuildsMetadata), ""); m_Options.add_option("", "", "zen", "Zen service upload address", cxxopts::value(m_ZenUrl), ""); m_Options.add_option("zen", "", "target-project", "Zen target project name", cxxopts::value(m_ZenProjectName), ""); m_Options.add_option("zen", "", "target-oplog", "Zen target oplog name", cxxopts::value(m_ZenOplogName), ""); m_Options.add_option("zen", "", "clean", "Delete existing target Zen oplog", cxxopts::value(m_ZenClean), ""); m_Options.add_option("", "", "file", "Local folder path", cxxopts::value(m_FileDirectoryPath), ""); m_Options.add_option("file", "", "name", "Local file name", cxxopts::value(m_FileName), ""); m_Options.add_option("file", "", "basename", "Local base file name for incremental oplog export", cxxopts::value(m_BaseFileName), ""); m_Options.add_option("file", "", "forcetempblocks", "Force creation of temp attachment blocks", cxxopts::value(m_FileForceEnableTempBlocks), ""); m_Options.add_option("", "", "plainprogress", "Use (legacy) plain progress update", cxxopts::value(m_PlainProgress), ""); m_Options.parse_positional({"project", "oplog"}); } ExportOplogCommand::~ExportOplogCommand() { } int ExportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { using namespace std::literals; ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return 0; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { throw OptionParseException("unable to resolve server specification"); } if (m_ProjectName.empty()) { throw OptionParseException("project name must be specified"); } HttpClient Http(m_HostName); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { return 1; } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { return 1; } size_t TargetCount = 0; TargetCount += m_CloudUrl.empty() ? 0 : 1; TargetCount += m_BuildsUrl.empty() ? 0 : 1; TargetCount += m_ZenUrl.empty() ? 0 : 1; TargetCount += m_FileDirectoryPath.empty() ? 0 : 1; if (TargetCount != 1) { if (TargetCount == 0) { throw OptionParseException("an export target must be specified"); } else { throw OptionParseException("a single export target must be specified"); } } if (!m_CloudUrl.empty()) { if (m_JupiterNamespace.empty() || m_JupiterBucket.empty()) { ZEN_ERROR("Options for cloud target are missing"); ZEN_CONSOLE("{}", m_Options.help({"cloud"}).c_str()); return 1; } if (m_CloudKey.empty()) { std::string KeyString = fmt::format("{}/{}/{}/{}", m_ProjectName, m_OplogName, m_JupiterNamespace, m_JupiterBucket); IoHash Key = IoHash::HashBuffer(KeyString.data(), KeyString.size()); m_CloudKey = Key.ToHexString(); ZEN_WARN("Using auto generated cloud key '{}'", m_CloudKey); } } if (!m_BuildsUrl.empty()) { if (m_JupiterNamespace.empty() || m_JupiterBucket.empty()) { ZEN_ERROR("Options for builds target are missing"); ZEN_CONSOLE("{}", m_Options.help({"builds"}).c_str()); return 1; } if (m_BuildsMetadataPath.empty() && m_BuildsMetadata.empty()) { ZEN_ERROR("Options for builds target are missing"); ZEN_CONSOLE("{}", m_Options.help({"builds"}).c_str()); return 1; } if (!m_BuildsMetadataPath.empty() && !m_BuildsMetadata.empty()) { ZEN_ERROR("Conflicting options for builds target"); ZEN_CONSOLE("{}", m_Options.help({"builds"}).c_str()); return 1; } if (m_BuildsId.empty()) { m_BuildsId = Oid::NewOid().ToString(); ZEN_CONSOLE("Using generated builds id: {}", m_BuildsId); } } if (!m_ZenUrl.empty()) { if (m_ZenProjectName.empty()) { m_ZenProjectName = m_ProjectName; ZEN_WARN("Using default zen target project id '{}'", m_ZenProjectName); } if (m_ZenOplogName.empty()) { m_ZenOplogName = m_OplogName; ZEN_WARN("Using default zen target oplog id '{}'", m_ZenOplogName); } std::string TargetUrlBase = m_ZenUrl; if (TargetUrlBase.find("://") == std::string::npos) { // Assume https URL TargetUrlBase = fmt::format("http://{}", TargetUrlBase); } HttpClient TargetHttp(TargetUrlBase); std::string Url = fmt::format("/prj/{}/oplog/{}", m_ZenProjectName, m_ZenOplogName); bool CreateOplog = false; if (HttpClient::Response Result = TargetHttp.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { if (m_ZenClean) { ZEN_WARN("Deleting zen remote oplog '{}/{}'", m_ZenProjectName, m_ZenOplogName) Result = TargetHttp.Delete(Url, HttpClient::Accept(ZenContentType::kJSON)); if (!Result) { Result.ThrowError("failed deleting existing zen remote oplog"sv); return 1; } CreateOplog = true; } } else if (Result.StatusCode == HttpResponseCode::NotFound) { CreateOplog = true; } else { Result.ThrowError("failed checking zen remote oplog"sv); return 1; } if (CreateOplog) { ZEN_WARN("Creating zen remote oplog '{}/{}'", m_ZenProjectName, m_ZenOplogName); if (HttpClient::Response Result = TargetHttp.Post(Url); !Result) { Result.ThrowError("failed creating zen remote oplog"sv); return 1; } } } if (!m_FileDirectoryPath.empty()) { if (m_FileName.empty()) { m_FileName = m_OplogName; ZEN_WARN("Using default file name '{}'", m_FileName); } } std::string TargetDescription; IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "export"sv); Writer.BeginObject("params"sv); { if (m_MaxBlockSize != 0) { Writer.AddInteger("maxblocksize"sv, m_MaxBlockSize); } if (m_MaxChunkEmbedSize != 0) { Writer.AddInteger("maxchunkembedsize"sv, m_MaxChunkEmbedSize); } if (m_EmbedLooseFiles) { Writer.AddBool("embedloosefiles"sv, true); } if (m_Force) { Writer.AddBool("force"sv, true); } if (m_IgnoreMissingAttachments) { Writer.AddBool("ignoremissingattachments"sv, true); } Writer.AddBool("async"sv, true); if (!m_FileDirectoryPath.empty()) { Writer.BeginObject("file"sv); { Writer.AddString("path"sv, m_FileDirectoryPath); Writer.AddString("name"sv, m_FileName); if (!m_BaseFileName.empty()) { Writer.AddString("basename"sv, m_BaseFileName); } if (m_DisableBlocks) { Writer.AddBool("disableblocks"sv, true); } if (m_FileForceEnableTempBlocks) { Writer.AddBool("enabletempblocks"sv, true); } } Writer.EndObject(); // "file" TargetDescription = fmt::format("[file] {}/{}{}{}", m_FileDirectoryPath, m_FileName, m_BaseFileName.empty() ? "" : " Base: ", m_BaseFileName); } if (!m_CloudUrl.empty()) { Writer.BeginObject("cloud"sv); { Writer.AddString("url"sv, m_CloudUrl); Writer.AddString("namespace"sv, m_JupiterNamespace); Writer.AddString("bucket"sv, m_JupiterBucket); Writer.AddString("key"sv, m_CloudKey); if (!m_BaseCloudKey.empty()) { Writer.AddString("basekey"sv, m_BaseCloudKey); } WriteAuthOptions(Writer, m_JupiterOpenIdProvider, m_JupiterAccessToken, m_JupiterAccessTokenEnv, m_JupiterAccessTokenPath, m_OidcTokenAuthExecutablePath); if (m_JupiterAssumeHttp2) { Writer.AddBool("assumehttp2"sv, true); } if (m_DisableBlocks) { Writer.AddBool("disableblocks"sv, true); } if (m_JupiterDisableTempBlocks) { Writer.AddBool("disabletempblocks"sv, true); } } Writer.EndObject(); // "cloud" TargetDescription = fmt::format("[cloud] {}/{}/{}/{}{}{}", m_CloudUrl, m_JupiterNamespace, m_JupiterBucket, m_CloudKey, m_BaseCloudKey.empty() ? "" : " Base: ", m_BaseCloudKey); } if (!m_BuildsUrl.empty()) { Writer.BeginObject("builds"sv); { Writer.AddString("url"sv, m_BuildsUrl); Writer.AddString("namespace"sv, m_JupiterNamespace); Writer.AddString("bucket"sv, m_JupiterBucket); Writer.AddString("buildsid"sv, m_BuildsId); WriteAuthOptions(Writer, m_JupiterOpenIdProvider, m_JupiterAccessToken, m_JupiterAccessTokenEnv, m_JupiterAccessTokenPath, m_OidcTokenAuthExecutablePath); if (m_JupiterAssumeHttp2) { Writer.AddBool("assumehttp2"sv, true); } if (m_DisableBlocks) { Writer.AddBool("disableblocks"sv, true); } if (m_JupiterDisableTempBlocks) { Writer.AddBool("disabletempblocks"sv, true); } if (!m_BuildsMetadataPath.empty()) { std::filesystem::path MetadataPath(m_BuildsMetadataPath); IoBuffer MetaDataJson = ReadFile(MetadataPath).Flatten(); std::string_view Json(reinterpret_cast(MetaDataJson.GetData()), MetaDataJson.GetSize()); CbFieldIterator MetaData = LoadCompactBinaryFromJson(Json); Writer.AddBinary("metadata"sv, MetaData.GetBuffer()); } if (!m_BuildsMetadata.empty()) { CbObjectWriter MetaDataWriter(m_BuildsMetadata.length()); ForEachStrTok(m_BuildsMetadata, ';', [&](std::string_view Pair) { size_t SplitPos = Pair.find('='); if (SplitPos == std::string::npos || SplitPos == 0) { throw std::runtime_error(fmt::format("builds metadata key-value pair '{}' is malformed", Pair)); } MetaDataWriter.AddString(Pair.substr(0, SplitPos), Pair.substr(SplitPos + 1)); return true; }); CbObject MetaData = MetaDataWriter.Save(); Writer.AddBinary("metadata"sv, MetaData.GetBuffer()); } } Writer.EndObject(); // "builds" TargetDescription = fmt::format("[builds] {}/{}/{}/{}", m_CloudUrl, m_JupiterNamespace, m_JupiterBucket, m_BuildsId); } if (!m_ZenUrl.empty()) { Writer.BeginObject("zen"sv); { Writer.AddString("url"sv, m_ZenUrl); Writer.AddString("project"sv, m_ZenProjectName); Writer.AddString("oplog"sv, m_ZenOplogName); } Writer.EndObject(); // "zen" TargetDescription = fmt::format("[zen] {}/{}/{}", m_ZenUrl, m_ZenProjectName, m_ZenOplogName); } } Writer.EndObject(); // "params" }); ZEN_CONSOLE("Saving oplog '{}/{}' from '{}' to {}", m_ProjectName, m_OplogName, m_HostName, TargetDescription); try { if (m_Async) { if (HttpClient::Response Result = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), std::move(Payload), HttpClient::Accept(ZenContentType::kJSON)); Result) { ZEN_CONSOLE("{}", Result.ToText()); } else { Result.ThrowError("failed requesting loading oplog export"sv); } } else { ExecuteAsyncOperation(Http, fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), std::move(Payload), m_PlainProgress); } } catch (const HttpClientError& Ex) { ZEN_CONSOLE("Oplog export failed: '{}'", Ex.what()); return Ex.m_Error != 0 ? Ex.m_Error : (int)Ex.m_ResponseCode; } catch (const AsyncJobError& Ex) { ZEN_CONSOLE("Oplog export failed: '{}'", Ex.what()); return Ex.m_ReturnCode; } catch (const std::exception& Ex) { ZEN_CONSOLE("Oplog export failed: '{}'", Ex.what()); return 1; } return 0; } //////////////////////////// ImportOplogCommand::ImportOplogCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectName), ""); m_Options.add_option("", "o", "oplog", "Oplog name", cxxopts::value(m_OplogName), ""); m_Options.add_option("", "", "gcpath", "Absolute path to oplog lifetime marker file if we create the oplog", cxxopts::value(m_GcPath), ""); m_Options.add_option("", "", "maxblocksize", "Max size for bundled attachments", cxxopts::value(m_MaxBlockSize), ""); m_Options.add_option("", "", "maxchunkembedsize", "Max size for attachment to be bundled", cxxopts::value(m_MaxChunkEmbedSize), ""); m_Options.add_option("", "f", "force", "Force import of all attachments", cxxopts::value(m_Force), ""); m_Options.add_option("", "a", "async", "Trigger import but don't wait for completion", cxxopts::value(m_Async), ""); m_Options.add_option("", "", "clean", "Delete existing target oplog", cxxopts::value(m_Clean), ""); m_Options.add_option("", "", "ignore-missing-attachments", "Continue importing oplog even if attachments are missing", cxxopts::value(m_IgnoreMissingAttachments), ""); m_Options.add_option("", "", "namespace", "Cloud/Builds Storage namespace", cxxopts::value(m_JupiterNamespace), ""); m_Options.add_option("", "", "bucket", "Cloud/Builds Storage bucket", cxxopts::value(m_JupiterBucket), ""); m_Options.add_option("", "", "openid-provider", "Cloud/Builds Storage openid provider", cxxopts::value(m_JupiterOpenIdProvider), ""); m_Options .add_option("", "", "access-token", "Cloud/Builds Storage access token", cxxopts::value(m_JupiterAccessToken), ""); m_Options.add_option("", "", "access-token-env", "Name of environment variable that holds the cloud/builds Storage access token", cxxopts::value(m_JupiterAccessTokenEnv)->default_value(DefaultJupiterAccessTokenEnvVariableName), ""); m_Options.add_option("", "", "access-token-path", "Path to json file that holds the cloud/builds Storage access token", cxxopts::value(m_JupiterAccessTokenPath), ""); m_Options.add_option("", "", "oidctoken-exe-path", "Path to OidcToken executable", cxxopts::value(m_OidcTokenAuthExecutablePath)->default_value(""), ""); m_Options.add_option("", "", "assume-http2", "Assume that the cloud/builds endpoint is a HTTP/2 endpoint skipping HTTP/1.1 upgrade handshake", cxxopts::value(m_JupiterAssumeHttp2), ""); m_Options.add_option("", "", "cloud", "Cloud Storage URL", cxxopts::value(m_CloudUrl), ""); m_Options.add_option("cloud", "", "key", "Cloud Storage key", cxxopts::value(m_CloudKey), ""); m_Options.add_option("", "", "builds", "Builds Storage URL", cxxopts::value(m_BuildsUrl), ""); m_Options.add_option("builds", "", "builds-id", "Builds Id", cxxopts::value(m_BuildsId), ""); m_Options.add_option("", "", "zen", "Zen service upload address", cxxopts::value(m_ZenUrl), ""); m_Options.add_option("zen", "", "source-project", "Zen source project name", cxxopts::value(m_ZenProjectName), ""); m_Options.add_option("zen", "", "source-oplog", "Zen source oplog name", cxxopts::value(m_ZenOplogName), ""); m_Options.add_option("", "", "file", "Local folder path", cxxopts::value(m_FileDirectoryPath), ""); m_Options.add_option("file", "", "name", "Local file name", cxxopts::value(m_FileName), ""); m_Options.add_option("", "", "plainprogress", "Use (legacy) plain progress update", cxxopts::value(m_PlainProgress), ""); m_Options.parse_positional({"project", "oplog", "gcpath"}); m_Options.positional_help("[ []]"); } ImportOplogCommand::~ImportOplogCommand() { } int ImportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { using namespace std::literals; ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return 0; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { throw OptionParseException("unable to resolve server specification"); } if (m_ProjectName.empty()) { ZEN_ERROR("Project name must be given"); return 1; } if (m_OplogName.empty()) { ZEN_ERROR("Oplog name must be given"); return 1; } HttpClient Http(m_HostName); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { return 1; } size_t TargetCount = 0; TargetCount += m_CloudUrl.empty() ? 0 : 1; TargetCount += m_BuildsUrl.empty() ? 0 : 1; TargetCount += m_ZenUrl.empty() ? 0 : 1; TargetCount += m_FileDirectoryPath.empty() ? 0 : 1; if (TargetCount != 1) { ZEN_ERROR("Provide one source only"); ZEN_CONSOLE("{}", m_Options.help({""}).c_str()); return 1; } if (!m_CloudUrl.empty()) { if (m_JupiterNamespace.empty() || m_JupiterBucket.empty()) { ZEN_ERROR("Options for cloud source are missing"); ZEN_CONSOLE("{}", m_Options.help({"cloud"}).c_str()); return 1; } if (m_CloudKey.empty()) { std::string KeyString = fmt::format("{}/{}/{}/{}", m_ProjectName, m_OplogName, m_JupiterNamespace, m_JupiterBucket); IoHash Key = IoHash::HashBuffer(KeyString.data(), KeyString.size()); m_CloudKey = Key.ToHexString(); ZEN_WARN("Using auto generated cloud key '{}'", m_CloudKey); } } if (!m_BuildsUrl.empty()) { if (m_JupiterNamespace.empty() || m_JupiterBucket.empty() || m_BuildsId.empty()) { ZEN_ERROR("Options for builds source are missing"); ZEN_CONSOLE("{}", m_Options.help({"builds"}).c_str()); return 1; } } if (!m_ZenUrl.empty()) { if (m_ZenProjectName.empty()) { m_ZenProjectName = m_ProjectName; ZEN_WARN("Using default zen target project id '{}'", m_ZenProjectName); } if (m_ZenOplogName.empty()) { m_ZenOplogName = m_OplogName; ZEN_WARN("Using default zen target oplog id '{}'", m_ZenOplogName); } } if (!m_FileDirectoryPath.empty()) { if (m_FileName.empty()) { m_FileName = m_OplogName; ZEN_WARN("Using auto generated file name '{}'", m_FileName); } } std::string Url = fmt::format("/prj/{}/oplog/{}", m_ProjectName, m_OplogName); bool CreateOplog = false; if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON)); Result.StatusCode == HttpResponseCode::NotFound) { CreateOplog = true; } else if (!IsHttpSuccessCode(Result.StatusCode)) { Result.ThrowError("failed checking oplog"sv); return 1; } if (CreateOplog) { IoBuffer OplogPayload; if (!m_GcPath.empty()) { OplogPayload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("gcpath"sv, m_GcPath); }); } ZEN_WARN("Creating oplog '{}/{}'", m_ProjectName, m_OplogName); if (HttpClient::Response Result = Http.Post(Url, OplogPayload); !Result) { Result.ThrowError("failed creating oplog"sv); return 1; } } std::string SourceDescription; IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "import"sv); Writer.BeginObject("params"sv); { if (m_Force) { Writer.AddBool("force"sv, true); } if (m_IgnoreMissingAttachments) { Writer.AddBool("ignoremissingattachments"sv, true); } if (m_Clean) { Writer.AddBool("clean"sv, true); } if (m_Force) { Writer.AddBool("force"sv, true); } if (!m_FileDirectoryPath.empty()) { Writer.BeginObject("file"sv); { Writer.AddString("path"sv, m_FileDirectoryPath); Writer.AddString("name"sv, m_FileName); } Writer.EndObject(); // "file" SourceDescription = fmt::format("[file] {}/{}", m_FileDirectoryPath, m_FileName); } if (!m_CloudUrl.empty()) { Writer.BeginObject("cloud"sv); { Writer.AddString("url"sv, m_CloudUrl); Writer.AddString("namespace"sv, m_JupiterNamespace); Writer.AddString("bucket"sv, m_JupiterBucket); Writer.AddString("key"sv, m_CloudKey); WriteAuthOptions(Writer, m_JupiterOpenIdProvider, m_JupiterAccessToken, m_JupiterAccessTokenEnv, m_JupiterAccessTokenPath, m_OidcTokenAuthExecutablePath); if (m_JupiterAssumeHttp2) { Writer.AddBool("assumehttp2"sv, true); } } Writer.EndObject(); // "cloud" SourceDescription = fmt::format("[cloud] {}/{}/{}/{}", m_CloudUrl, m_JupiterNamespace, m_JupiterBucket, m_CloudKey); } if (!m_BuildsUrl.empty()) { Writer.BeginObject("builds"sv); { Writer.AddString("url"sv, m_BuildsUrl); Writer.AddString("namespace"sv, m_JupiterNamespace); Writer.AddString("bucket"sv, m_JupiterBucket); Writer.AddString("buildsid"sv, m_BuildsId); WriteAuthOptions(Writer, m_JupiterOpenIdProvider, m_JupiterAccessToken, m_JupiterAccessTokenEnv, m_JupiterAccessTokenPath, m_OidcTokenAuthExecutablePath); if (m_JupiterAssumeHttp2) { Writer.AddBool("assumehttp2"sv, true); } } Writer.EndObject(); // "builds" SourceDescription = fmt::format("[builds] {}/{}/{}/{}", m_CloudUrl, m_JupiterNamespace, m_JupiterBucket, m_BuildsId); } if (!m_ZenUrl.empty()) { Writer.BeginObject("zen"sv); { Writer.AddString("url"sv, m_ZenUrl); Writer.AddString("project"sv, m_ZenProjectName); Writer.AddString("oplog"sv, m_ZenOplogName); } Writer.EndObject(); // "zen" SourceDescription = fmt::format("[zen] {}", m_ZenUrl); } } Writer.EndObject(); // "params" }); ZEN_CONSOLE("Loading oplog '{}/{}' from '{}' to {}", m_ProjectName, m_OplogName, SourceDescription, m_HostName); try { if (m_Async) { if (HttpClient::Response Result = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), std::move(Payload), HttpClient::Accept(ZenContentType::kJSON)); Result) { ZEN_CONSOLE("{}", Result.ToText()); } else { Result.ThrowError("failed requesting loading oplog import"sv); } } else { ExecuteAsyncOperation(Http, fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), std::move(Payload), m_PlainProgress); } } catch (const HttpClientError& Ex) { ZEN_CONSOLE("Oplog import failed: '{}'", Ex.what()); return Ex.m_Error != 0 ? Ex.m_Error : (int)Ex.m_ResponseCode; } catch (const AsyncJobError& Ex) { ZEN_CONSOLE("Oplog export failed: '{}'", Ex.what()); return Ex.m_ReturnCode; } catch (const std::exception& Ex) { ZEN_CONSOLE("Oplog import failed: '{}'", Ex.what()); return 1; } return 0; } //////////////////////////// SnapshotOplogCommand::SnapshotOplogCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectName), ""); m_Options.add_option("", "o", "oplog", "Oplog name", cxxopts::value(m_OplogName), ""); m_Options.parse_positional({"project", "oplog"}); } SnapshotOplogCommand::~SnapshotOplogCommand() { } int SnapshotOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { using namespace std::literals; ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return 0; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { throw OptionParseException("unable to resolve server specification"); } HttpClient Http(m_HostName); if (m_ProjectName.empty()) { ZEN_ERROR("Project name must be given"); return 1; } m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { return 1; } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { return 1; } IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "snapshot"sv); }); ZEN_CONSOLE("Snapshotting oplog '{}/{}' to {}", m_ProjectName, m_OplogName, m_HostName); if (HttpClient::Response Result = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), Payload, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result); return 0; } else { Result.ThrowError("failed to create project"sv); return 1; } } //////////////////////////// ProjectStatsCommand::ProjectStatsCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), ""); } ProjectStatsCommand::~ProjectStatsCommand() { } int ProjectStatsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return 0; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { throw OptionParseException("unable to resolve server specification"); } HttpClient Http(m_HostName); if (HttpClient::Response Result = Http.Get("/stats/prj", HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result.ToText()); return 0; } else { Result.ThrowError("failed to get project stats"sv); return 1; } } //////////////////////////// ProjectOpDetailsCommand::ProjectOpDetailsCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "c", "csv", "Output in CSV format (default is JSon)", cxxopts::value(m_CSV), ""); m_Options.add_option("", "d", "details", "Detailed info on oplog", cxxopts::value(m_Details), "
"); m_Options.add_option("", "o", "opdetails", "Details info on oplog body", cxxopts::value(m_OpDetails), ""); m_Options.add_option("", "p", "project", "Project name to get info from", cxxopts::value(m_ProjectName), ""); m_Options.add_option("", "l", "oplog", "Oplog name to get info from", cxxopts::value(m_OplogName), ""); m_Options.add_option("", "i", "opid", "Oid of a specific op info for", cxxopts::value(m_OpId), ""); m_Options.add_option("", "a", "attachmentdetails", "Get detailed information about attachments", cxxopts::value(m_AttachmentDetails), ""); } ProjectOpDetailsCommand::~ProjectOpDetailsCommand() { } int ProjectOpDetailsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return 0; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { throw OptionParseException("unable to resolve server specification"); } HttpClient Http(m_HostName); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { return 1; } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { return 1; } ExtendableStringBuilder<128> Url; Url.Append("/prj/details$"); if (!m_ProjectName.empty()) { Url.Append("/"); Url.Append(m_ProjectName); } if (!m_OplogName.empty()) { Url.Append("/"); Url.Append(m_OplogName); } if (!m_OpId.empty()) { Url.Append("/"); Url.Append(m_OpId); } if (HttpClient::Response Result = Http.Get(Url, m_CSV ? HttpClient::Accept(ZenContentType::kText) : HttpClient::Accept(ZenContentType::kJSON), {{"opdetails", m_OpDetails ? "true" : "false"}, {"details", m_Details ? "true" : "false"}, {"attachmentdetails", m_AttachmentDetails ? "true" : "false"}, {"csv", m_CSV ? "true" : "false"}})) { ZEN_CONSOLE("{}", Result.ToText()); return 0; } else { Result.ThrowError("failed to get project details"sv); return 1; } } //////////////////////////// OplogMirrorCommand::OplogMirrorCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "p", "project", "Project name to get info from", cxxopts::value(m_ProjectName), ""); m_Options.add_option("", "l", "oplog", "Oplog name to get info from", cxxopts::value(m_OplogName), ""); m_Options.add_option("", "t", "target", "Target directory for mirror", cxxopts::value(m_MirrorRootPath), ""); m_Options.add_option("", "k", "key", "Oplog key string to limit output (substring match), defaults to no filtering", cxxopts::value(m_KeyFilter), ""); m_Options.add_option("", "f", "file", "Oplog file entry path string to limit output (substring match), defaults to no filtering", cxxopts::value(m_FilenameFilter), ""); m_Options.add_option("", "c", "chunk", "Oplog file entry chunk id to limit output, defaults to no filtering", cxxopts::value(m_ChunkIdFilter), ""); m_Options.add_option("", "d", "decompress", "Decompress data when applicable. Default = false", cxxopts::value(m_Decompress), ""); m_Options.add_option( "", "", "trim", "Restricts the mirrored ops to only include the ops in the ReferencedSet. The default (`--trim=true`) is ignored if the oplog's " "ReferencedSet is invalid.", cxxopts::value(m_TrimToReferencedSet), ""); m_Options.parse_positional({"project", "oplog", "target"}); m_Options.positional_help("[ ]"); } OplogMirrorCommand::~OplogMirrorCommand() { } int OplogMirrorCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return 0; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { throw OptionParseException("unable to resolve server specification"); } HttpClient Http(m_HostName); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { return 1; } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { return 1; } if (m_MirrorRootPath.empty()) { throw OptionParseException("a target path must be specified"); } Oid ChunkIdFilter = Oid::Zero; if (!m_ChunkIdFilter.empty()) { ChunkIdFilter = Oid::TryFromHexString(m_ChunkIdFilter); if (ChunkIdFilter == Oid::Zero) { throw OptionParseException("chunkid must be an Oid hex string"); } } if (!m_FilenameFilter.empty()) { std::replace(m_FilenameFilter.begin(), m_FilenameFilter.end(), '\\', '/'); m_FilenameFilter = ToLower(m_FilenameFilter); } if (!m_KeyFilter.empty()) { std::replace(m_KeyFilter.begin(), m_KeyFilter.end(), '\\', '/'); m_KeyFilter = ToLower(m_KeyFilter); } ZEN_CONSOLE("Emitting file data from oplog '{}/{}' to '{}'", m_ProjectName, m_OplogName, m_MirrorRootPath); // Emit file data to target directory std::filesystem::path RootPath{m_MirrorRootPath}; CreateDirectories(RootPath); std::filesystem::path TmpPath = RootPath / ".tmp"; CreateDirectories(TmpPath); auto _ = MakeGuard([&TmpPath]() { DeleteDirectories(TmpPath); }); std::atomic_int64_t FileCount = 0; int OplogEntryCount = 0; size_t WorkerCount = Min(std::thread::hardware_concurrency(), 16u); WorkerThreadPool WorkerPool(gsl::narrow(WorkerCount)); Latch WorkRemaining(1); size_t EmitCount = 0; std::unordered_set FileNames; std::atomic WrittenByteCount = 0; std::atomic AbortFlag(false); Stopwatch WriteStopWatch; auto EmitFilesForDataArray = [&](CbArrayView DataArray) { for (auto DataIter : DataArray) { if (CbObjectView Data = DataIter.AsObjectView()) { std::filesystem::path FileName(Data["filename"sv].AsU8String()); if (!m_FilenameFilter.empty()) { std::string FileNameLowerCase = ToLower(FileName.string()); if (FileNameLowerCase.find(m_FilenameFilter) == std::string::npos) { continue; } } Oid ChunkId = Data["id"sv].AsObjectId(); if (ChunkIdFilter != Oid::Zero && ChunkIdFilter != ChunkId) { continue; } if (!FileNames.insert(FileName.u8string()).second) { continue; } EmitCount++; WorkRemaining.AddCount(1); WorkerPool.ScheduleWork( [this, &RootPath, &AbortFlag, FileName, &FileCount, ChunkId, &Http, TmpPath, &WorkRemaining, &WrittenByteCount]() { auto _ = MakeGuard([&WorkRemaining]() { WorkRemaining.CountDown(); }); if (!AbortFlag) { std::filesystem::path TargetPath = RootPath / FileName; try { if (HttpClient::Response ChunkResponse = Http.Download(fmt::format("/prj/{}/oplog/{}/{}"sv, m_ProjectName, m_OplogName, ChunkId), TmpPath)) { auto TryDecompress = [](const IoBuffer& Buffer) -> IoBuffer { IoHash RawHash; uint64_t RawSize; if (CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Buffer), RawHash, RawSize)) { return Compressed.Decompress().AsIoBuffer(); }; return std::move(Buffer); }; IoBuffer ChunkData = m_Decompress ? TryDecompress(ChunkResponse.ResponsePayload) : ChunkResponse.ResponsePayload; if (!MoveToFile(TargetPath, ChunkData)) { WriteFile(TargetPath, ChunkData); } WrittenByteCount.fetch_add(ChunkData.GetSize()); ++FileCount; } else { throw std::runtime_error(fmt::format("Unable to fetch '{}' (chunk {}). Reason: '{}'", FileName, ChunkId, ChunkResponse.ErrorMessage(""sv))); } } catch (const std::exception& Ex) { AbortFlag.store(true); ZEN_CONSOLE("Failed writing file to '{}'. Reason: '{}'", TargetPath, Ex.what()); } } }); } } }; HttpClient::KeyValueMap Parameters{{"trim_by_referencedset", m_TrimToReferencedSet ? "true" : "false"}}; if (HttpClient::Response Response = Http.Get(fmt::format("/prj/{}/oplog/{}/entries"sv, m_ProjectName, m_OplogName), HttpClient::KeyValueMap(), Parameters)) { if (CbObject ResponseObject = Response.AsObject()) { std::unique_ptr EmitProgressBar; { ProgressBar ParseProgressBar(ProgressBar::Mode::Pretty, ""); CbArrayView Entries = ResponseObject["entries"sv].AsArrayView(); uint64_t Remaining = Entries.Num(); for (auto EntryIter : Entries) { if (!AbortFlag) { CbObjectView Entry = EntryIter.AsObjectView(); ParseProgressBar.UpdateState( {.Task = "Parsing oplog", .Details = "", .TotalCount = Entries.Num(), .RemainingCount = Remaining}, false); Remaining--; if (!m_KeyFilter.empty()) { if (Entry["key"].AsString().find(m_KeyFilter) == std::string_view::npos) { continue; } } if (!EmitProgressBar) { EmitProgressBar = std::make_unique(ProgressBar::Mode::Pretty, ""sv); WriteStopWatch.Reset(); } EmitFilesForDataArray(Entry["packagedata"sv].AsArrayView()); EmitFilesForDataArray(Entry["bulkdata"sv].AsArrayView()); ++OplogEntryCount; } } ParseProgressBar.Finish(); } WorkRemaining.CountDown(); if (EmitProgressBar) { while (!WorkRemaining.Wait(200)) { uint64_t EmitRemaining = gsl::narrow(WorkRemaining.Remaining()); uint64_t WrittenBytes = WrittenByteCount.load(); uint64_t ElapsedTimeMS = WriteStopWatch.GetElapsedTimeMs(); uint64_t BytesPerSecond = ElapsedTimeMS > 0 ? (1000 * WrittenBytes) / ElapsedTimeMS : WrittenBytes; EmitProgressBar->UpdateState({.Task = "Writing files", .Details = fmt::format("{}/{} files, {}/sec ({})", EmitCount - EmitRemaining, EmitCount, NiceBytes(BytesPerSecond), NiceBytes(WrittenByteCount.load())), .TotalCount = EmitCount, .RemainingCount = EmitRemaining}, false); } EmitProgressBar->Finish(); } else { WorkRemaining.Wait(); } if (AbortFlag) { // Error has already been reported by async code return 1; } } else { ZEN_ERROR("unknown format response to oplog entries request"); } } else { Response.ThrowError("oplog entries fetch failed"); return 1; } ZEN_CONSOLE("mirrored {} files from {} oplog entries successfully", FileCount.load(), OplogEntryCount); return 0; } //////////////////////////// OplogValidateCommand::OplogValidateCommand() { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("", "u", "hosturl", "Host URL", cxxopts::value(m_HostName)->default_value(""), ""); m_Options.add_option("", "p", "project", "Project name to get info from", cxxopts::value(m_ProjectName), ""); m_Options.add_option("", "l", "oplog", "Oplog name to get info from", cxxopts::value(m_OplogName), ""); m_Options.parse_positional({"project", "oplog"}); m_Options.positional_help("[ ]"); } OplogValidateCommand::~OplogValidateCommand() { } int OplogValidateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { return 0; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { throw OptionParseException("unable to resolve server specification"); } HttpClient Http(m_HostName); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { return 1; } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { return 1; } std::string Url = fmt::format("/prj/{}/oplog/{}/validate", m_ProjectName, m_OplogName); if (HttpClient::Response Result = Http.Post(Url, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result.ToText()); return 0; } else { Result.ThrowError("failed to get validate project oplog"sv); return 1; } return 0; } } // namespace zen