#include "resources/config.h" #include "../librepomgr/json.h" #include "../librepomgr/webapi/params.h" #include "../librepomgr/webclient/session.h" #include "../libpkg/data/database.h" #include "../libpkg/data/package.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace CppUtilities; using namespace CppUtilities::EscapeCodes; using namespace std; struct ClientConfig { void parse(const Argument &configFileArg, const Argument &instanceArg); const char *path = nullptr; std::string instance; std::string url; std::string userName; std::string password; }; void ClientConfig::parse(const Argument &configFileArg, const Argument &instanceArg) { // parse connfig file path = configFileArg.firstValue(); if (!path || !*path) { path = "/etc/buildservice" PROJECT_CONFIG_SUFFIX "/client.conf"; } auto configFile = NativeFileStream(); configFile.exceptions(std::ios_base::badbit | std::ios_base::failbit); configFile.open(path, std::ios_base::in); auto configIni = AdvancedIniFile(); configIni.parse(configFile); configFile.close(); // read innstance if (instanceArg.isPresent()) { instance = instanceArg.values().front(); } for (const auto §ion : configIni.sections) { if (!section.name.starts_with("instance/")) { continue; } if (!instance.empty() && instance != std::string_view(section.name.data() + 9, section.name.size() - 9)) { continue; } instance = section.name; if (const auto url = section.findField("url"); url != section.fieldEnd()) { this->url = std::move(url->value); } else { throw std::runtime_error("Config is invalid: No \"url\" specified within \"" % section.name + "\"."); } if (const auto user = section.findField("user"); user != section.fieldEnd()) { this->userName = std::move(user->value); } break; } if (url.empty()) { throw std::runtime_error("Config is invalid: Instance configuration insufficient."); } // read user data if (userName.empty()) { return; } if (const auto userSection = configIni.findSection("user/" + userName); userSection != configIni.sectionEnd()) { if (const auto password = userSection->findField("password"); password != userSection->fieldEnd()) { this->password = std::move(password->value); } else { throw std::runtime_error("Config is invalid: No \"password\" specified within \"" % userSection->name + "\"."); } } else { throw std::runtime_error("Config is invalid: User \"" % userName + "\" referenced in instance configuration not found."); } } void configureColumnWidths(tabulate::Table &table) { const auto terminalSize = determineTerminalSize(); if (!terminalSize.columns) { return; } struct ColumnStats { std::size_t maxSize = 0; std::size_t totalSize = 0; std::size_t rows = 0; double averageSize = 0.0; double averagePercentage = 0.0; std::size_t width = 0; }; auto columnStats = std::vector(); for (const auto &row : table) { const auto columnCount = row.size(); if (columnStats.size() < columnCount) { columnStats.resize(columnCount); } auto column = columnStats.begin(); for (const auto &cell : row.cells()) { const auto size = cell->size(); column->maxSize = std::max(column->maxSize, size); column->totalSize += std::max(10, size); column->rows += 1; ++column; } } auto totalAverageSize = 0.0; for (auto &column : columnStats) { totalAverageSize += (column.averageSize = static_cast(column.totalSize) / column.rows); } for (auto &column : columnStats) { column.averagePercentage = column.averageSize / totalAverageSize; column.width = std::max(terminalSize.columns * column.averagePercentage, std::min(column.maxSize, 10)); } for (std::size_t columnIndex = 0; columnIndex != columnStats.size(); ++columnIndex) { table.column(columnIndex).format().width(columnStats[columnIndex].width); } } void printPackageSearchResults(const LibRepoMgr::WebClient::Response::body_type::value_type &jsonData) { const auto packages = ReflectiveRapidJSON::JsonReflector::fromJson>(jsonData.data(), jsonData.size()); tabulate::Table t; t.format().hide_border(); t.add_row({ "Arch", "Repo", "Name", "Version", "Description", "Build date" }); for (const auto &[db, package] : packages) { const auto &dbInfo = std::get(db); t.add_row( { package->packageInfo ? package->packageInfo->arch : dbInfo.arch, dbInfo.name, package->name, package->version, package->description, package->packageInfo && !package->packageInfo->buildDate.isNull() ? package->packageInfo->buildDate.toString() : "?" }); } t.row(0).format().font_align(tabulate::FontAlign::center).font_style({ tabulate::FontStyle::bold }); configureColumnWidths(t); std::cout << t << std::endl; } template inline std::string formatList(const List &list) { return joinStrings(list, ", "); } std::string formatDependencies(const std::vector &deps) { auto asStrings = std::vector(); asStrings.reserve(deps.size()); for (const auto &dep : deps) { asStrings.emplace_back(dep.toString()); } return formatList(asStrings); } void printPackageDetails(const LibRepoMgr::WebClient::Response::body_type::value_type &jsonData) { const auto packages = ReflectiveRapidJSON::JsonReflector::fromJson>(jsonData.data(), jsonData.size()); for (const auto &package : packages) { const auto *const pkg = &package; std::cout << TextAttribute::Bold << pkg->name << ' ' << pkg->version << TextAttribute::Reset << '\n'; tabulate::Table t; t.format().hide_border(); if (pkg->packageInfo) { t.add_row({ "Arch", pkg->packageInfo->arch }); } else if (pkg->sourceInfo) { t.add_row({ "Archs", formatList(pkg->sourceInfo->archs) }); } t.add_row({ "Description", pkg->description }); t.add_row({ "Upstream URL", pkg->upstreamUrl }); t.add_row({ "License(s)", formatList(pkg->licenses) }); t.add_row({ "Groups", formatList(pkg->groups) }); if (pkg->packageInfo && pkg->packageInfo->size) { t.add_row({ "Package size", dataSizeToString(pkg->packageInfo->size, true) }); } if (pkg->installInfo) { t.add_row({ "Installed size", dataSizeToString(pkg->installInfo->installedSize, true) }); } if (pkg->packageInfo) { if (!pkg->packageInfo->packager.empty()) { t.add_row({ "Packager", pkg->packageInfo->packager }); } if (!pkg->packageInfo->buildDate.isNull()) { t.add_row({ "Packager", pkg->packageInfo->buildDate.toString() }); } } t.add_row({ "Dependencies", formatDependencies(pkg->dependencies) }); t.add_row({ "Optional dependencies", formatDependencies(pkg->optionalDependencies) }); if (pkg->sourceInfo) { t.add_row({ "Make dependencies", formatDependencies(pkg->sourceInfo->makeDependencies) }); t.add_row({ "Check dependencies", formatDependencies(pkg->sourceInfo->checkDependencies) }); } t.add_row({ "Provides", formatDependencies(pkg->provides) }); t.add_row({ "Replaces", formatDependencies(pkg->replaces) }); t.add_row({ "Conflicts", formatDependencies(pkg->conflicts) }); t.add_row({ "Contained libraries", formatList(pkg->libprovides) }); t.add_row({ "Needed libraries", formatList(pkg->libdepends) }); t.column(0).format().font_align(tabulate::FontAlign::right); configureColumnWidths(t); std::cout << t << '\n'; } std::cout.flush(); } void printRawData(const LibRepoMgr::WebClient::Response::body_type::value_type &rawData) { if (!rawData.empty()) { std::cerr << Phrases::InfoMessage << "Server replied:" << Phrases::End << rawData << '\n'; } } void handleResponse(const std::string &url, const LibRepoMgr::WebClient::SessionData &data, const LibRepoMgr::WebClient::HttpClientError &error, void (*printer)(const LibRepoMgr::WebClient::Response::body_type::value_type &jsonData), int &returnCode) { const auto &response = std::get(data.response); const auto &body = response.body(); if (error.errorCode != boost::beast::errc::success && error.errorCode != boost::asio::ssl::error::stream_truncated) { std::cerr << Phrases::ErrorMessage << "Unable to connect: " << error.what() << Phrases::End; std::cerr << Phrases::InfoMessage << "URL was: " << url << Phrases::End; printRawData(body); return; } if (response.result() != boost::beast::http::status::ok) { std::cerr << Phrases::ErrorMessage << "HTTP request not successful: " << error.what() << Phrases::End; std::cerr << Phrases::InfoMessage << "URL was: " << url << Phrases::End; printRawData(body); return; } try { std::invoke(printer, body); } catch (const RAPIDJSON_NAMESPACE::ParseResult &e) { std::cerr << Phrases::ErrorMessage << "Unable to parse responnse: " << tupleToString(LibRepoMgr::serializeParseError(e)) << Phrases::End; std::cerr << Phrases::InfoMessage << "URL was: " << url << std::endl; returnCode = 11; } catch (const std::runtime_error &e) { std::cerr << Phrases::ErrorMessage << "Unable to display response: " << e.what() << Phrases::End; std::cerr << Phrases::InfoMessage << "URL was: " << url << std::endl; returnCode = 12; } } int main(int argc, const char *argv[]) { // define command-specific parameters std::string path; void (*printer)(const LibRepoMgr::WebClient::Response::body_type::value_type &jsonData) = nullptr; // read CLI args ArgumentParser parser; ConfigValueArgument configFileArg("config-file", 'c', "specifies the path of the config file", { "path" }); configFileArg.setEnvironmentVariable(PROJECT_VARNAME_UPPER "_CONFIG_FILE"); ConfigValueArgument instanceArg("instance", 'i', "specifies the instance to connect to", { "instance" }); OperationArgument searchArg("search", 's', "searches packages"); ConfigValueArgument searchTermArg("term", 't', "specifies the search term", { "term" }); searchTermArg.setImplicit(true); searchTermArg.setRequired(true); ConfigValueArgument searchModeArg("mode", 'm', "specifies the mode", { "name/name-contains/regex/provides/depends/libprovides/libdepends" }); searchModeArg.setPreDefinedCompletionValues("name name-contains regex provides depends libprovides libdepends"); searchArg.setSubArguments({ &searchTermArg, &searchModeArg }); searchArg.setCallback([&path, &printer, &searchTermArg, &searchModeArg](const ArgumentOccurrence &) { path = "/api/v0/packages?mode=" % LibRepoMgr::WebAPI::Url::encodeValue(searchModeArg.firstValueOr("name-contains")) % "&name=" + LibRepoMgr::WebAPI::Url::encodeValue(searchTermArg.values().front()); printer = printPackageSearchResults; }); OperationArgument packageArg("package", 'p', "shows detauls about a package"); ConfigValueArgument packageNameArg("name", 'n', "specifies the package name", { "name" }); packageNameArg.setImplicit(true); packageNameArg.setRequired(true); packageArg.setSubArguments({ &packageNameArg }); packageArg.setCallback([&path, &printer, &packageNameArg](const ArgumentOccurrence &) { path = "/api/v0/packages?mode=name&details=1&name=" + LibRepoMgr::WebAPI::Url::encodeValue(packageNameArg.values().front()); printer = printPackageDetails; }); HelpArgument helpArg(parser); NoColorArgument noColorArg; parser.setMainArguments({ &searchArg, &packageArg, &instanceArg, &configFileArg, &noColorArg, &helpArg }); parser.parseArgs(argc, argv); // return early if no operation specified if (!printer) { if (!helpArg.isPresent()) { std::cerr << "No command specified; use --help to list available commands.\n"; } return 0; } // parse config auto config = ClientConfig(); try { config.parse(configFileArg, instanceArg); } catch (const std::runtime_error &e) { std::cerr << Phrases::ErrorMessage << "Unable to parse config: " << e.what() << Phrases::End; std::cerr << Phrases::InfoMessage << "Path of config file was: " << (config.path ? config.path : "[none]") << Phrases::End; return 10; } // make HTTP request and show response const auto url = config.url + path; auto ioContext = boost::asio::io_context(); auto sslContext = boost::asio::ssl::context{ boost::asio::ssl::context::sslv23_client }; auto returnCode = 0; sslContext.set_verify_mode(boost::asio::ssl::verify_peer); sslContext.set_default_verify_paths(); LibRepoMgr::WebClient::runSessionFromUrl(ioContext, sslContext, url, std::bind(&handleResponse, std::ref(url), std::placeholders::_1, std::placeholders::_2, printer, std::ref(returnCode)), std::string(), config.userName, config.password); ioContext.run(); return 0; }