diff --git a/dnf5-plugins/CMakeLists.txt b/dnf5-plugins/CMakeLists.txt index 503bfe4bc..f28a3e58d 100644 --- a/dnf5-plugins/CMakeLists.txt +++ b/dnf5-plugins/CMakeLists.txt @@ -17,3 +17,4 @@ add_subdirectory("config-manager_plugin") add_subdirectory("copr_plugin") add_subdirectory("needs_restarting_plugin") add_subdirectory("repoclosure_plugin") +add_subdirectory("downloadurl_plugin") diff --git a/dnf5-plugins/downloadurl_plugin/CMakeLists.txt b/dnf5-plugins/downloadurl_plugin/CMakeLists.txt new file mode 100644 index 000000000..40699a833 --- /dev/null +++ b/dnf5-plugins/downloadurl_plugin/CMakeLists.txt @@ -0,0 +1,15 @@ +# set gettext domain for translations +set(GETTEXT_DOMAIN dnf5-plugin-downloadurl) +add_definitions(-DGETTEXT_DOMAIN=\"${GETTEXT_DOMAIN}\") + +add_library(downloadurl_cmd_plugin MODULE downloadurl.cpp downloadurl_cmd_plugin.cpp) + +# disable the 'lib' prefix in order to create downloadurl_cmd_plugin.so +set_target_properties(downloadurl_cmd_plugin PROPERTIES PREFIX "") + +pkg_check_modules(SDBUS_CPP REQUIRED sdbus-c++) + +target_link_libraries(downloadurl_cmd_plugin PRIVATE dnf5 libdnf5 libdnf5-cli ${SDBUS_CPP_LIBRARIES}) + +install(TARGETS downloadurl_cmd_plugin LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}/dnf5/plugins/) + diff --git a/dnf5-plugins/downloadurl_plugin/downloadurl.cpp b/dnf5-plugins/downloadurl_plugin/downloadurl.cpp new file mode 100644 index 000000000..be79a5f94 --- /dev/null +++ b/dnf5-plugins/downloadurl_plugin/downloadurl.cpp @@ -0,0 +1,311 @@ +/* +Copyright Contributors to the libdnf project. + +This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + +Libdnf is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +Libdnf is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with libdnf. If not, see . +*/ + +#include "downloadurl.hpp" + +#include "dnf5/shared_options.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +namespace dnf5 { + +using namespace libdnf5::cli; + +void DownloadURLCommand::set_parent_command() { + auto * arg_parser_parent_cmd = get_session().get_argument_parser().get_root_command(); + auto * arg_parser_this_cmd = get_argument_parser_command(); + arg_parser_parent_cmd->register_command(arg_parser_this_cmd); +} + +void DownloadURLCommand::set_argument_parser() { + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + + auto & cmd = *get_argument_parser_command(); + cmd.set_description("Download software to the current directory"); + + patterns_to_download_options = parser.add_new_values(); + auto keys = parser.add_new_positional_arg( + "keys_to_match", + ArgumentParser::PositionalArg::UNLIMITED, + parser.add_init_value(std::unique_ptr(new libdnf5::OptionString(nullptr))), + patterns_to_download_options); + keys->set_description("List of keys to match"); + keys->set_complete_hook_func([&ctx](const char * arg) { return match_specs(ctx, arg, false, true, false, false); }); + + resolve_option = dynamic_cast( + parser.add_init_value(std::unique_ptr(new libdnf5::OptionBool(false)))); + + alldeps_option = dynamic_cast( + parser.add_init_value(std::unique_ptr(new libdnf5::OptionBool(false)))); + + url_option = dynamic_cast( + parser.add_init_value(std::unique_ptr(new libdnf5::OptionBool(false)))); + + urlallmirrors_option = dynamic_cast( + parser.add_init_value(std::unique_ptr(new libdnf5::OptionBool(false)))); + + srpm_option = dynamic_cast( + parser.add_init_value(std::unique_ptr(new libdnf5::OptionBool(false)))); + + auto resolve = parser.add_new_named_arg("resolve"); + resolve->set_long_name("resolve"); + resolve->set_description("Resolve and download needed dependencies"); + resolve->set_const_value("true"); + resolve->link_value(resolve_option); + + auto alldeps = parser.add_new_named_arg("alldeps"); + alldeps->set_long_name("alldeps"); + alldeps->set_description( + "When running with --resolve, download all dependencies (do not exclude already installed ones)"); + alldeps->set_const_value("true"); + alldeps->link_value(alldeps_option); + + auto srpm = parser.add_new_named_arg("srpm"); + srpm->set_long_name("srpm"); + srpm->set_description("Download the src.rpm instead"); + srpm->set_const_value("true"); + srpm->link_value(srpm_option); + + auto url = parser.add_new_named_arg("url"); + url->set_long_name("url"); + url->set_description("Print a URL where the rpms can be downloaded instead of downloading"); + url->set_const_value("true"); + url->link_value(url_option); + + urlprotocol_valid_options = {"http", "https", "ftp", "file"}; + urlprotocol_option = {}; + auto urlprotocol = parser.add_new_named_arg("urlprotocol"); + urlprotocol->set_long_name("urlprotocol"); + urlprotocol->set_description("When running with --url, limit to specific protocols"); + urlprotocol->set_has_value(true); + urlprotocol->set_arg_value_help("{http|https|ftp|file},..."); + urlprotocol->set_parse_hook_func( + [this]( + [[maybe_unused]] ArgumentParser::NamedArg * arg, [[maybe_unused]] const char * option, const char * value) { + if (urlprotocol_valid_options.find(value) == urlprotocol_valid_options.end()) { + throw libdnf5::cli::ArgumentParserInvalidValueError( + M_("Invalid urlprotocol option: {}"), std::string(value)); + } + urlprotocol_option.emplace(value); + return true; + }); + + auto urlallmirrors = parser.add_new_named_arg("all-mirrors"); + urlallmirrors->set_long_name("all-mirrors"); + urlallmirrors->set_description("When running with --url, prints URLs from all available mirrors"); + urlallmirrors->set_const_value("true"); + urlallmirrors->link_value(urlallmirrors_option); + + arch_option = {}; + auto arch = parser.add_new_named_arg("arch"); + arch->set_long_name("arch"); + arch->set_description("Limit to packages of given architectures."); + arch->set_has_value(true); + arch->set_arg_value_help("ARCH,..."); + arch->set_parse_hook_func( + [this]( + [[maybe_unused]] ArgumentParser::NamedArg * arg, [[maybe_unused]] const char * option, const char * value) { + auto supported_arches = libdnf5::rpm::get_supported_arches(); + if (std::find(supported_arches.begin(), supported_arches.end(), value) == supported_arches.end()) { + std::string available_arches{}; + auto it = supported_arches.begin(); + if (it != supported_arches.end()) { + available_arches.append("\"" + *it + "\""); + ++it; + for (; it != supported_arches.end(); ++it) { + available_arches.append(", \"" + *it + "\""); + } + } + throw libdnf5::cli::ArgumentParserInvalidValueError( + M_("Unsupported architecture \"{0}\". Please choose one from {1}"), + std::string(value), + available_arches); + } + arch_option.emplace(value); + return true; + }); + + + cmd.register_named_arg(arch); + cmd.register_named_arg(resolve); + cmd.register_named_arg(alldeps); + create_destdir_option(*this); + cmd.register_named_arg(srpm); + cmd.register_named_arg(url); + cmd.register_named_arg(urlprotocol); + cmd.register_named_arg(urlallmirrors); + cmd.register_positional_arg(keys); +} + +void DownloadURLCommand::configure() { + auto & context = get_context(); + + std::vector pkg_specs; + for (auto & pattern : *patterns_to_download_options) { + auto option = dynamic_cast(pattern.get()); + pkg_specs.push_back(option->get_value()); + } + + context.update_repo_metadata_from_specs(pkg_specs); + if (resolve_option->get_value() && !alldeps_option->get_value()) { + context.set_load_system_repo(true); + } else if (!resolve_option->get_value() && alldeps_option->get_value()) { + throw libdnf5::cli::ArgumentParserMissingDependentArgumentError( + //TODO(jrohel): Add support for requiring an argument by another argument in ArgumentParser? + M_("Option \"--alldeps\" should be used with \"--resolve\"")); + } else { + context.set_load_system_repo(false); + } + + if (srpm_option->get_value()) { + context.get_base().get_repo_sack()->enable_source_repos(); + } + + context.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); + // Default destination for downloaded rpms is the current directory + context.get_base().get_config().get_destdir_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, "."); +} + +void DownloadURLCommand::run() { + auto & ctx = get_context(); + + auto create_nevra_pkg_pair = [](const libdnf5::rpm::Package & pkg) { return std::make_pair(pkg.get_nevra(), pkg); }; + + std::map download_pkgs; + libdnf5::rpm::PackageQuery full_pkg_query(ctx.get_base(), libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); + for (auto & pattern : *patterns_to_download_options) { + libdnf5::rpm::PackageQuery pkg_query(full_pkg_query); + auto option = dynamic_cast(pattern.get()); + pkg_query.resolve_pkg_spec(option->get_value(), {}, true); + pkg_query.filter_available(); + pkg_query.filter_priority(); + pkg_query.filter_latest_evr(); + if (!arch_option.empty()) { + pkg_query.filter_arch(std::vector(arch_option.begin(), arch_option.end())); + } + + for (const auto & pkg : pkg_query) { + download_pkgs.insert(create_nevra_pkg_pair(pkg)); + + if (resolve_option->get_value()) { + auto goal = std::make_unique(ctx.get_base()); + goal->add_rpm_install(pkg, {}); + + auto transaction = goal->resolve(); + auto transaction_problems = transaction.get_problems(); + if (transaction_problems != libdnf5::GoalProblem::NO_PROBLEM) { + if (transaction_problems != libdnf5::GoalProblem::NOT_FOUND) { + throw GoalResolveError(transaction); + } + } + for (auto & tspkg : transaction.get_transaction_packages()) { + if (transaction_item_action_is_inbound(tspkg.get_action()) && + tspkg.get_package().get_repo()->get_type() != libdnf5::repo::Repo::Type::COMMANDLINE) { + download_pkgs.insert(create_nevra_pkg_pair(tspkg.get_package())); + } + } + } + } + } + + if (download_pkgs.empty()) { + return; + } + + if (srpm_option->get_value()) { + std::map source_pkgs; + + libdnf5::rpm::PackageQuery source_pkg_query(ctx.get_base()); + source_pkg_query.filter_arch("src"); + source_pkg_query.filter_available(); + + for (auto & [nevra, pkg] : download_pkgs) { + auto sourcerpm = pkg.get_sourcerpm(); + + if (!sourcerpm.empty()) { + libdnf5::rpm::PackageQuery pkg_query(source_pkg_query); + pkg_query.filter_epoch({pkg.get_epoch()}); + + // Remove ".rpm" to get sourcerpm nevra + sourcerpm.erase(sourcerpm.length() - 4); + pkg_query.resolve_pkg_spec(sourcerpm, {}, true); + + for (const auto & spkg : pkg_query) { + source_pkgs.insert(create_nevra_pkg_pair(spkg)); + } + } else if (pkg.get_arch() == "src") { + source_pkgs.insert(create_nevra_pkg_pair(pkg)); + } else { + ctx.get_base().get_logger()->info("No source rpm defined for package: \"{}\"", pkg.get_name()); + continue; + } + } + + download_pkgs = source_pkgs; + } + + if (url_option->get_value()) { + // If no urlprotocols are specified, all values within the urlprotocol_valid_options will be used + if (urlprotocol_option.empty()) { + urlprotocol_option = urlprotocol_valid_options; + } + for (auto & [nerva, pkg] : download_pkgs) { + auto urls = pkg.get_remote_locations(urlprotocol_option); + if (urls.empty()) { + ctx.get_base().get_logger()->warning("Failed to get mirror for package: \"{}\"", pkg.get_name()); + continue; + } + if (urlallmirrors_option->get_value()) { + for (auto &url: urls) { + std::cout << url << std::endl; + } + } else { + std::cout << urls[0] << std::endl; + } + } + return; + } + libdnf5::repo::PackageDownloader downloader(ctx.get_base()); + + // for download command, we don't want to mark the packages for removal + downloader.force_keep_packages(true); + + for (auto & [nevra, pkg] : download_pkgs) { + downloader.add(pkg); + } + + std::cout << "Downloading Packages:" << std::endl; + downloader.download(); + std::cout << std::endl; +} + + +} // namespace dnf5 diff --git a/dnf5-plugins/downloadurl_plugin/downloadurl.hpp b/dnf5-plugins/downloadurl_plugin/downloadurl.hpp new file mode 100644 index 000000000..ff37ddf4f --- /dev/null +++ b/dnf5-plugins/downloadurl_plugin/downloadurl.hpp @@ -0,0 +1,60 @@ +/* +Copyright Contributors to the libdnf project. + +This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + +Libdnf is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +Libdnf is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with libdnf. If not, see . +*/ + + +#ifndef DNF5_COMMANDS_DOWNLOADURL_DOWNLOAD_HPP +#define DNF5_COMMANDS_DOWNLOADURL_DOWNLOAD_HPP + +#include +#include + +#include +#include +#include + + +namespace dnf5 { + + +class DownloadURLCommand : public Command { +public: + explicit DownloadURLCommand(Context & context) : Command(context, "downloadurl") {} + void set_parent_command() override; + void set_argument_parser() override; + void configure() override; + void run() override; + +private: + std::set urlprotocol_valid_options; + std::set urlprotocol_option; + std::set arch_option; + libdnf5::OptionBool * resolve_option{nullptr}; + libdnf5::OptionBool * alldeps_option{nullptr}; + libdnf5::OptionBool * url_option{nullptr}; + libdnf5::OptionBool * urlallmirrors_option{nullptr}; + libdnf5::OptionBool * srpm_option{nullptr}; + + std::vector> * patterns_to_download_options{nullptr}; +}; + + +} // namespace dnf5 + + +#endif // DNF5_COMMANDS_DOWNLOADURL_DOWNLOAD_HPP diff --git a/dnf5-plugins/downloadurl_plugin/downloadurl_cmd_plugin.cpp b/dnf5-plugins/downloadurl_plugin/downloadurl_cmd_plugin.cpp new file mode 100644 index 000000000..f69ad8e47 --- /dev/null +++ b/dnf5-plugins/downloadurl_plugin/downloadurl_cmd_plugin.cpp @@ -0,0 +1,74 @@ +#include "downloadurl.hpp" + +#include + +#include + +using namespace dnf5; + +namespace { + +constexpr const char * PLUGIN_NAME{"downloadurl"}; +constexpr PluginVersion PLUGIN_VERSION{.major = 0, .minor = 0, .micro = 1}; + +constexpr const char * attrs[]{"author.name", "author.email", "description", nullptr}; +constexpr const char * attrs_value[]{"Ali Mirjamali", "ali@mirjamali.com", "downloadurl command."}; + +class DownloadURLCmdPlugin : public IPlugin { +public: + using IPlugin::IPlugin; + + PluginAPIVersion get_api_version() const noexcept override { return PLUGIN_API_VERSION; } + + const char * get_name() const noexcept override { return PLUGIN_NAME; } + + PluginVersion get_version() const noexcept override { return PLUGIN_VERSION; } + + const char * const * get_attributes() const noexcept override { return attrs; } + + const char * get_attribute(const char * attribute) const noexcept override { + for (size_t i = 0; attrs[i]; ++i) { + if (std::strcmp(attribute, attrs[i]) == 0) { + return attrs_value[i]; + } + } + return nullptr; + } + + std::vector> create_commands() override; + + void finish() noexcept override {} +}; + + +std::vector> DownloadURLCmdPlugin::create_commands() { + std::vector> commands; + commands.push_back(std::make_unique(get_context())); + return commands; +} + + +} // namespace + + +PluginAPIVersion dnf5_plugin_get_api_version(void) { + return PLUGIN_API_VERSION; +} + +const char * dnf5_plugin_get_name(void) { + return PLUGIN_NAME; +} + +PluginVersion dnf5_plugin_get_version(void) { + return PLUGIN_VERSION; +} + +IPlugin * dnf5_plugin_new_instance([[maybe_unused]] ApplicationVersion application_version, Context & context) try { + return new DownloadURLCmdPlugin(context); +} catch (...) { + return nullptr; +} + +void dnf5_plugin_delete_instance(IPlugin * plugin_object) { + delete plugin_object; +}