// Copyright Epic Games, Inc. All Rights Reserved. #include "projectstore_cmd.h" #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 DefaultCloudAccessTokenEnvVariableName( #if ZEN_PLATFORM_WINDOWS "UE-CloudDataCacheAccessToken"sv #endif #if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC "UE_CloudDataCacheAccessToken"sv #endif ); std::string ReadCloudAccessTokenFromFile(const std::filesystem::path& Path) { if (!std::filesystem::is_regular_file(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; } 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); } } void ExecuteAsyncOperation(HttpClient& Http, std::string_view Url, IoBuffer&& Payload) { 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)); } std::string LastCurrentOp; uint32_t LastCurrentOpPercentComplete = 0; 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(); if (Status == "Running") { std::string_view CurrentOp = StatusObject["CurrentOp"sv].AsString(); uint32_t CurrentOpPercentComplete = StatusObject["CurrentOpPercentComplete"sv].AsUInt32(); if (CurrentOp != LastCurrentOp || CurrentOpPercentComplete != LastCurrentOpPercentComplete) { LastCurrentOp = CurrentOp; LastCurrentOpPercentComplete = CurrentOpPercentComplete; ZEN_CONSOLE("{} {}%", CurrentOp, CurrentOpPercentComplete); } } CbArrayView Messages = StatusObject["Messages"sv].AsArrayView(); for (auto M : Messages) { std::string_view Message = M.AsString(); ZEN_CONSOLE("{}", Message); } 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(); if (!AbortReason.empty()) { throw std::runtime_error(std::string(AbortReason)); } else { throw std::runtime_error("Aborted"); } 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))) { ZEN_CONSOLE("Requested cancel..."); Cancelled = true; } else { ZEN_CONSOLE("Failed cancelling job {}", DeleteResult); } continue; } Sleep(100); } } else { ZEN_CONSOLE("{}", Result); } } else { Result.ThrowError("failed to start operation"sv); } } } // 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.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"); } if (m_ProjectName.empty()) { throw OptionParseException("Drop command requires a project"); } HttpClient Http(m_HostName); if (m_OplogName.empty()) { 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 { 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()) { Url = fmt::format("/prj/{}", m_ProjectName); ZEN_CONSOLE("Info on project '{}' from '{}{}'", m_ProjectName, m_HostName, Url); } else { 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()); } else { Result.ThrowError("failed to fetch info"sv); return 1; } 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; } } /////////////////////////////////////// DeleteProjectCommand::DeleteProjectCommand() { 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), ""); } DeleteProjectCommand::~DeleteProjectCommand() = default; int DeleteProjectCommand::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 (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON)); !Result) { Result.ThrowError("failed deleting project"sv); return 1; } if (HttpClient::Response Result = Http.Delete(Url, HttpClient::Accept(ZenContentType::kText))) { ZEN_CONSOLE("{}", Result); return 0; } else { Result.ThrowError("failed deleting 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"); } if (m_OplogId.empty()) { throw OptionParseException("oplog name must be specified"); } HttpClient Http(m_HostName); 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; } } /////////////////////////////////////// DeleteOplogCommand::DeleteOplogCommand() { 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.parse_positional({"project", "oplog", "gcpath"}); } DeleteOplogCommand::~DeleteOplogCommand() = default; int DeleteOplogCommand::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"); } if (m_OplogId.empty()) { throw OptionParseException("oplog name must be specified"); } HttpClient Http(m_HostName); std::string Url = fmt::format("/prj/{}/oplog/{}", m_ProjectId, m_OplogId); if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON)); !Result) { Result.ThrowError("failed deleting oplog"sv); return 1; } if (HttpClient::Response Result = Http.Delete(Url, HttpClient::Accept(ZenContentType::kText))) { ZEN_CONSOLE("{}", Result); return 0; } else { Result.ThrowError("failed deleting 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 importing 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("", "", "cloud", "Cloud Storage URL", cxxopts::value(m_CloudUrl), ""); m_Options.add_option("cloud", "", "namespace", "Cloud Storage namespace", cxxopts::value(m_CloudNamespace), ""); m_Options.add_option("cloud", "", "bucket", "Cloud Storage bucket", cxxopts::value(m_CloudBucket), ""); 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("cloud", "", "openid-provider", "Cloud Storage openid provider", cxxopts::value(m_CloudOpenIdProvider), ""); m_Options.add_option("cloud", "", "access-token", "Cloud Storage access token", cxxopts::value(m_CloudAccessToken), ""); m_Options.add_option("cloud", "", "access-token-env", "Name of environment variable that holds the cloud Storage access token", cxxopts::value(m_CloudAccessTokenEnv)->default_value(DefaultCloudAccessTokenEnvVariableName), ""); m_Options.add_option("cloud", "", "access-token-path", "Path to json file that holds the cloud Storage access token", cxxopts::value(m_CloudAccessTokenPath), ""); m_Options.add_option("cloud", "", "assume-http2", "Assume that the cloud endpoint is a HTTP/2 endpoint skipping HTTP/1.1 upgrade handshake", cxxopts::value(m_CloudAssumeHttp2), ""); m_Options.add_option("cloud", "", "disabletempblocks", "Disable temp block creation and upload blocks without waiting for oplog container to be uploaded", cxxopts::value(m_CloudDisableTempBlocks), ""); 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.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"); } if (m_OplogName.empty()) { throw OptionParseException("oplog identifier must be specified"); } size_t TargetCount = 0; TargetCount += m_CloudUrl.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_CloudNamespace.empty() || m_CloudBucket.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_CloudNamespace, m_CloudBucket); IoHash Key = IoHash::HashBuffer(KeyString.data(), KeyString.size()); m_CloudKey = Key.ToHexString(); ZEN_WARN("Using auto generated cloud key '{}'", m_CloudKey); } } 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 Http(TargetUrlBase); std::string Url = fmt::format("/prj/{}/oplog/{}", m_ZenProjectName, m_ZenOplogName); bool CreateOplog = false; if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { if (m_ZenClean) { ZEN_WARN("Deleting zen remote oplog '{}/{}'", m_ZenProjectName, m_ZenOplogName) Result = Http.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 = Http.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_CloudNamespace); Writer.AddString("bucket"sv, m_CloudBucket); Writer.AddString("key"sv, m_CloudKey); if (!m_BaseCloudKey.empty()) { Writer.AddString("basekey"sv, m_BaseCloudKey); } if (!m_CloudOpenIdProvider.empty()) { Writer.AddString("openid-provider"sv, m_CloudOpenIdProvider); } if (!m_CloudAccessToken.empty()) { Writer.AddString("access-token"sv, m_CloudAccessToken); } if (!m_CloudAccessTokenPath.empty()) { std::string ResolvedCloudAccessToken = ReadCloudAccessTokenFromFile(m_CloudAccessTokenPath); if (!ResolvedCloudAccessToken.empty()) { Writer.AddString("access-token"sv, ResolvedCloudAccessToken); } } if (!m_CloudAccessTokenEnv.empty()) { std::string ResolvedCloudAccessTokenEnv = GetEnvVariable(m_CloudAccessTokenEnv); if (!ResolvedCloudAccessTokenEnv.empty()) { Writer.AddString("access-token"sv, ResolvedCloudAccessTokenEnv); } else { Writer.AddString("access-token-env"sv, m_CloudAccessTokenEnv); } } if (m_CloudAssumeHttp2) { Writer.AddBool("assumehttp2"sv, true); } if (m_DisableBlocks) { Writer.AddBool("disableblocks"sv, true); } if (m_CloudDisableTempBlocks) { Writer.AddBool("disabletempblocks"sv, true); } } Writer.EndObject(); // "cloud" TargetDescription = fmt::format("[cloud] {}/{}/{}/{}{}{}", m_CloudUrl, m_CloudNamespace, m_CloudBucket, m_CloudKey, m_BaseCloudKey.empty() ? "" : " Base: ", m_BaseCloudKey); } 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); HttpClient Http(m_HostName); 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); return 1; } } else { ExecuteAsyncOperation(Http, fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), std::move(Payload)); } 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("", "", "cloud", "Cloud Storage URL", cxxopts::value(m_CloudUrl), ""); m_Options.add_option("cloud", "", "namespace", "Cloud Storage namespace", cxxopts::value(m_CloudNamespace), ""); m_Options.add_option("cloud", "", "bucket", "Cloud Storage bucket", cxxopts::value(m_CloudBucket), ""); m_Options.add_option("cloud", "", "key", "Cloud Storage key", cxxopts::value(m_CloudKey), ""); m_Options .add_option("cloud", "", "openid-provider", "Cloud Storage openid provider", cxxopts::value(m_CloudOpenIdProvider), ""); m_Options.add_option("cloud", "", "access-token", "Cloud Storage access token", cxxopts::value(m_CloudAccessToken), ""); m_Options.add_option("cloud", "", "access-token-env", "Name of environment variable that holds the cloud Storage access token", cxxopts::value(m_CloudAccessTokenEnv)->default_value(DefaultCloudAccessTokenEnvVariableName), ""); m_Options.add_option("cloud", "", "access-token-path", "Path to json file that holds the cloud Storage access token", cxxopts::value(m_CloudAccessTokenPath), ""); m_Options.add_option("cloud", "", "assume-http2", "Assume that the cloud endpoint is a HTTP/2 endpoint skipping HTTP/1.1 upgrade handshake", cxxopts::value(m_CloudAssumeHttp2), ""); 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.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; } size_t TargetCount = 0; TargetCount += m_CloudUrl.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_CloudNamespace.empty() || m_CloudBucket.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_CloudNamespace, m_CloudBucket); IoHash Key = IoHash::HashBuffer(KeyString.data(), KeyString.size()); m_CloudKey = Key.ToHexString(); ZEN_WARN("Using auto generated cloud key '{}'", m_CloudKey); } } 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); } } HttpClient Http(m_HostName); 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))) { if (m_Clean) { ZEN_WARN("Deleting oplog '{}/{}'", m_ProjectName, m_OplogName) Result = Http.Delete(Url, HttpClient::Accept(ZenContentType::kJSON)); if (!Result) { Result.ThrowError("failed deleting existing oplog"sv); return 1; } CreateOplog = true; } } else if (Result.StatusCode == HttpResponseCode::NotFound) { CreateOplog = true; } else { 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_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_CloudNamespace); Writer.AddString("bucket"sv, m_CloudBucket); Writer.AddString("key"sv, m_CloudKey); if (!m_CloudOpenIdProvider.empty()) { Writer.AddString("openid-provider"sv, m_CloudOpenIdProvider); } if (!m_CloudAccessToken.empty()) { Writer.AddString("access-token"sv, m_CloudAccessToken); } if (!m_CloudAccessTokenPath.empty()) { std::string ResolvedCloudAccessToken = ReadCloudAccessTokenFromFile(m_CloudAccessTokenPath); if (!ResolvedCloudAccessToken.empty()) { Writer.AddString("access-token"sv, ResolvedCloudAccessToken); } } if (!m_CloudAccessTokenEnv.empty()) { std::string ResolvedCloudAccessTokenEnv = GetEnvVariable(m_CloudAccessTokenEnv); if (!ResolvedCloudAccessTokenEnv.empty()) { Writer.AddString("access-token"sv, ResolvedCloudAccessTokenEnv); } else { Writer.AddString("access-token-env"sv, m_CloudAccessTokenEnv); } } if (m_CloudAssumeHttp2) { Writer.AddBool("assumehttp2"sv, true); } } Writer.EndObject(); // "cloud" SourceDescription = fmt::format("[cloud] {}/{}/{}/{}", m_CloudUrl, m_CloudNamespace, m_CloudBucket, m_CloudKey); } 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); 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); return 1; } } else { ExecuteAsyncOperation(Http, fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), std::move(Payload)); } 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"); } 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; } IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "snapshot"sv); }); HttpClient Http(m_HostName); 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; } } //////////////////////////// ProjectDetailsCommand::ProjectDetailsCommand() { 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), ""); } ProjectDetailsCommand::~ProjectDetailsCommand() { } int ProjectDetailsCommand::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_OpId.empty()) { if (m_ProjectName.empty() || m_OplogName.empty()) { ZEN_ERROR("Provide project and oplog name"); ZEN_CONSOLE("{}", m_Options.help({""}).c_str()); return 1; } } else if (!m_OplogName.empty()) { if (m_ProjectName.empty()) { ZEN_ERROR("Provide project name"); ZEN_CONSOLE("{}", m_Options.help({""}).c_str()); return 1; } } HttpClient Http(m_HostName); 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.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"); } if (m_ProjectName.empty()) { throw OptionParseException("a project must be specified"); } if (m_OplogName.empty()) { throw OptionParseException("an oplog must be specified"); } if (m_MirrorRootPath.empty()) { throw OptionParseException("a target path must be specified"); } ZEN_CONSOLE("Emitting file data from oplog '{}/{}' to '{}'", m_ProjectName, m_OplogName, m_MirrorRootPath); HttpClient Http(m_HostName); if (HttpClient::Response Result = Http.Get(fmt::format("/prj/{}/oplog/{}", m_ProjectName, m_OplogName))) { // The info requested is not really used at this moment, we just use the probe to be able to provide // better diagnostics up front } else { Result.ThrowError("oplog info fetch failed"sv); return 1; } // Emit file data to target directory std::filesystem::path RootPath{m_MirrorRootPath}; CreateDirectories(RootPath); std::filesystem::path TmpPath = RootPath / ".tmp"; CreateDirectories(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); std::unordered_set FileNames; auto EmitFilesForDataArray = [&](CbArrayView DataArray) { for (auto DataIter : DataArray) { if (CbObjectView Data = DataIter.AsObjectView()) { std::string FileName = std::string(Data["filename"sv].AsString()); Oid ChunkId = Data["id"sv].AsObjectId(); if (!FileNames.insert(FileName).second) { continue; } WorkRemaining.AddCount(1); WorkerPool.ScheduleWork([this, &RootPath, FileName, &FileCount, ChunkId, &Http, TmpPath, &WorkRemaining]() { auto _ = MakeGuard([&WorkRemaining]() { WorkRemaining.CountDown(); }); if (HttpClient::Response ChunkResponse = Http.Download(fmt::format("/prj/{}/oplog/{}/{}"sv, m_ProjectName, m_OplogName, ChunkId), TmpPath)) { IoBuffer ChunkData = ChunkResponse.ResponsePayload; std::filesystem::path TargetPath = RootPath / FileName; if (!MoveToFile(TargetPath, ChunkData)) { WriteFile(TargetPath, ChunkData); } ++FileCount; } else { ZEN_CONSOLE("Unable to fetch '{}' (chunk {}). Reason: '{}'", FileName, ChunkId, ChunkResponse.ErrorMessage(""sv)); } }); } } }; if (HttpClient::Response Response = Http.Get(fmt::format("/prj/{}/oplog/{}/entries"sv, m_ProjectName, m_OplogName))) { if (CbObject ResponseObject = Response.AsObject()) { for (auto EntryIter : ResponseObject["entries"sv]) { CbObjectView Entry = EntryIter.AsObjectView(); EmitFilesForDataArray(Entry["packagedata"sv].AsArrayView()); EmitFilesForDataArray(Entry["bulkdata"sv].AsArrayView()); ++OplogEntryCount; } } else { ZEN_ERROR("unknown format response to oplog entries request"); } } else { Response.ThrowError("oplog entries fetch failed"); return 1; } WorkRemaining.CountDown(); WorkRemaining.Wait(); std::filesystem::remove_all(TmpPath); ZEN_CONSOLE("mirrored {} files from {} oplog entries successfully", FileCount.load(), OplogEntryCount); return 0; } } // namespace zen