diff --git a/.gitignore b/.gitignore index 658a9e3..6ac8ef4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ build/** builds/** bin/gdpm -bin/gdpm-tests +bin/gdpm.static +bin/gdpm.tests docs/doxygen cache/** tests/* diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c9828d..d4afafd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,25 +43,30 @@ set(LINK_LIBS SQLiteCpp -lcurlpp -lzip + -lsqlite3 + -lcurl ) # Set library and executable targets add_library(${PROJECT_NAME}-shared SHARED "${SRC}") add_library(${PROJECT_NAME}-static STATIC "${SRC}") add_executable(${PROJECT_NAME} "src/main.cpp") -add_executable(${PROJECT_NAME}-tests "${TESTS}") +add_executable(${PROJECT_NAME}.static "src/main.cpp") +add_executable(${PROJECT_NAME}.tests "${TESTS}") # Set include directories for targets target_include_directories(${PROJECT_NAME} PRIVATE ${INCLUDE_DIRS}) +target_include_directories(${PROJECT_NAME}.static PRIVATE ${INCLUDE_DIRS}) +target_include_directories(${PROJECT_NAME}.tests PRIVATE ${INCLUDE_DIRS}) target_include_directories(${PROJECT_NAME}-shared PRIVATE ${INCLUDE_DIRS}) target_include_directories(${PROJECT_NAME}-static PRIVATE ${INCLUDE_DIRS}) -target_include_directories(${PROJECT_NAME}-tests PRIVATE ${INCLUDE_DIRS}) # Set link libraries for targets target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}-shared ${LINK_LIBS}) +target_link_libraries(${PROJECT_NAME}.static PRIVATE ${PROJECT_NAME}-static ${LINK_LIBS}) +target_link_libraries(${PROJECT_NAME}.tests PRIVATE ${PROJECT_NAME}-shared ${LINK_LIBS}) target_link_libraries(${PROJECT_NAME}-shared PRIVATE ${LINK_LIBS}) target_link_libraries(${PROJECT_NAME}-static PRIVATE ${LINK_LIBS}) -target_link_libraries(${PROJECT_NAME}-tests PRIVATE ${PROJECT_NAME}-shared ${LINK_LIBS}) # Add project unit tests # add_custom_target("${PROJECT_NAME}-tests" SOURCE ${TESTS}) diff --git a/Doxyfile b/Doxyfile index 604fc64..c345d6f 100644 --- a/Doxyfile +++ b/Doxyfile @@ -499,13 +499,13 @@ NUM_PROC_THREADS = 1 # normally produced when WARNINGS is set to YES. # The default value is: NO. -EXTRACT_ALL = NO +EXTRACT_ALL = YES # If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will # be included in the documentation. # The default value is: NO. -EXTRACT_PRIVATE = NO +EXTRACT_PRIVATE = YES # If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual # methods of a class will be included in the documentation. @@ -1008,7 +1008,7 @@ FILE_PATTERNS = *.c \ # be searched for input files as well. # The default value is: NO. -RECURSIVE = NO +RECURSIVE = YES # The EXCLUDE tag can be used to specify files and/or directories that should be # excluded from the INPUT source files. This way you can easily exclude a @@ -1017,14 +1017,14 @@ RECURSIVE = NO # Note that relative paths are relative to the directory from which doxygen is # run. -EXCLUDE = +EXCLUDE = bin build docs examples modules tests # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or # directories that are symbolic links (a Unix file system feature) are excluded # from the input. # The default value is: NO. -EXCLUDE_SYMLINKS = NO +EXCLUDE_SYMLINKS = YES # If the value of the INPUT tag contains directories, you can use the # EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude @@ -1153,7 +1153,7 @@ FORTRAN_COMMENT_AFTER = 72 # also VERBATIM_HEADERS is set to NO. # The default value is: NO. -SOURCE_BROWSER = NO +SOURCE_BROWSER = YES # Setting the INLINE_SOURCES tag to YES will include the body of functions, # classes and enums directly into the documentation. diff --git a/bin/compile.sh b/bin/compile.sh index fec1627..3e9e1f5 100755 --- a/bin/compile.sh +++ b/bin/compile.sh @@ -1,11 +1,38 @@ #!/bin/sh +script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +exe=gdpm +static=gdpm.static +tests=gdpm.tests + +function test_link(){ + path=$1 + link=$2 + if test -f "$path" + then + echo "Creating link from '$path' to '$link')" + if test -f "$link" + then + rm $link + fi + ln -s $path $link + fi +} + +function test_strip(){ + path=$1 + if test -f "$path" + then + echo "Stripping debug symbols from '$path'" + strip "$path" + fi +} + # Run this script at project root #meson configure build #CXX=clang++ meson compile -C build -j$(proc) - # CMake/ninja build system mkdir -p build cmake -B build -S . -D CMAKE_EXPORT_COMPILE_COMMANDS=1 -D CMAKE_BUILD_TYPE=Debug -G Ninja @@ -13,12 +40,17 @@ ninja -C build -j $(nproc) # Create symlinks to executables in build folder if necessary -if test -f "../build/gdpm"; then - rm bin/gdpm - ln -s ../build/gdpm bin/gdpm -fi +test_link $script_dir/../build/gdpm $script_dir/../bin/$exe +test_link $script_dir/../build/gdpm.static $script_dir/../bin/$static +test_link $script_dir/../build/gdpm.tests $script_dir/../bin/$tests -if test -f "../build/gdpm-tests"; then - rm bin/gdpm-tests - ln -s ../build/gdpm-tests bin/gdpm-tests -fi + +# Strip debug symbols +test_strip ${script_dir}/../build/gdpm +test_strip ${script_dir}/../build/gdpm.static +test_strip ${script_dir}/../build/gdpm.tests + + +# Generate documentation using `doxygen` +cd ${script_dir}/.. +doxygen diff --git a/bin/lines.sh b/bin/lines.sh index 5e23d4e..87fa5f9 100755 --- a/bin/lines.sh +++ b/bin/lines.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash # Call this from project root -wc -l include/*.hpp src/*.cpp \ No newline at end of file +grep -v ^S include/*.hpp src/*.cpp | wc -l include/*.hpp src/*.cpp diff --git a/include/cache.hpp b/include/cache.hpp index 5bcb2d6..038f035 100644 --- a/include/cache.hpp +++ b/include/cache.hpp @@ -2,27 +2,31 @@ #pragma once #include "constants.hpp" +#include "package.hpp" +#include "error.hpp" +#include "result.hpp" #include #include #include -namespace gdpm::package_manager{ - struct package_info; -} -namespace gdpm::cache{ - using namespace package_manager; - int create_package_database(const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME); - int insert_package_info(const std::vector& package, const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME); - std::vector get_package_info_by_id(const std::vector& package_ids, const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME); - std::vector get_package_info_by_title(const std::vector& package_titles, const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME); - std::vector get_installed_packages(const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME); - int update_package_info(const std::vector& packages, const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME); - int update_sync_info(const std::vector& download_urls, const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME); - int delete_packages(const std::vector& package_titles, const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME); - int delete_packages(const std::vector& package_ids, const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME); - int drop_package_database(const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME); +namespace gdpm::cache { + struct params { + std::string cache_path = GDPM_PACKAGE_CACHE_PATH; + std::string table_name = GDPM_PACKAGE_CACHE_TABLENAME; + }; - std::string to_values(const package_info& package); - std::string to_values(const std::vector& packages); + error create_package_database(bool overwrite = false, const params& = params()); + error insert_package_info(const package::info_list& packages, const params& = params()); + result_t get_package_info_by_id(const package::id_list& package_ids, const params& = params()); + result_t get_package_info_by_title(const package::title_list& package_titles, const params& params = cache::params()); + result_t get_installed_packages(const params& = params()); + error update_package_info(const package::info_list& packages, const params& = params()); + error update_sync_info(const std::vector& download_urls, const params& = params()); + error delete_packages(const package::title_list& package_titles, const params& = params()); + error delete_packages(const package::id_list& package_ids, const params& = params()); + error drop_package_database(const params& = params()); + + result_t to_values(const package::info& package); + result_t to_values(const package::info_list& packages); } \ No newline at end of file diff --git a/include/config.hpp b/include/config.hpp index 4143ebb..5a5d6a2 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -3,32 +3,33 @@ #include "constants.hpp" #include "error.hpp" +#include #include #include #include -#include - +#include namespace gdpm::config{ struct context{ - std::string username; - std::string password; - std::string path; - std::string token; - std::string godot_version; - std::string packages_dir; - std::string tmp_dir; - std::set remote_sources; + string username; + string password; + string path; + string token; + string godot_version; + string packages_dir; + string tmp_dir; + string_map remote_sources; size_t threads; size_t timeout; bool enable_sync; bool enable_file_logging; int verbose; }; - std::string to_json(const context& params); - gdpm::error load(std::filesystem::path path, context& config, int verbose = 0); - gdpm::error save(std::filesystem::path path, const context& config, int verbose = 0); - context make_context(const std::string& username = GDPM_CONFIG_USERNAME, const std::string& password = GDPM_CONFIG_PASSWORD, const std::string& path = GDPM_CONFIG_PATH, const std::string& token = GDPM_CONFIG_TOKEN, const std::string& godot_version = GDPM_CONFIG_GODOT_VERSION, const std::string& packages_dir = GDPM_CONFIG_LOCAL_PACKAGES_DIR, const std::string& tmp_dir = GDPM_CONFIG_LOCAL_TMP_DIR, const std::set& remote_sources = {GDPM_CONFIG_REMOTE_SOURCES}, size_t threads = GDPM_CONFIG_THREADS, size_t timeout = 0, bool enable_sync = GDPM_CONFIG_ENABLE_SYNC, bool enable_file_logging = GDPM_CONFIG_ENABLE_FILE_LOGGING, int verbose = GDPM_CONFIG_VERBOSE); + string to_json(const context& params); + error load(std::filesystem::path path, context& config, int verbose = 0); + error save(std::filesystem::path path, const context& config, int verbose = 0); + context make_context(const std::string& username = GDPM_CONFIG_USERNAME, const string& password = GDPM_CONFIG_PASSWORD, const string& path = GDPM_CONFIG_PATH, const string& token = GDPM_CONFIG_TOKEN, const string& godot_version = GDPM_CONFIG_GODOT_VERSION, const string& packages_dir = GDPM_CONFIG_LOCAL_PACKAGES_DIR, const string& tmp_dir = GDPM_CONFIG_LOCAL_TMP_DIR, const string_map& remote_sources = {GDPM_CONFIG_REMOTE_SOURCES}, size_t threads = GDPM_CONFIG_THREADS, size_t timeout = 0, bool enable_sync = GDPM_CONFIG_ENABLE_SYNC, bool enable_file_logging = GDPM_CONFIG_ENABLE_FILE_LOGGING, int verbose = GDPM_CONFIG_VERBOSE); + error validate(const rapidjson::Document& doc); extern context config; } \ No newline at end of file diff --git a/include/constants.hpp b/include/constants.hpp index 49c7f80..6d5d1f6 100644 --- a/include/constants.hpp +++ b/include/constants.hpp @@ -5,6 +5,7 @@ #include namespace gdpm::constants{ + const std::string RemoteName(std::string("origin")); const std::string HomePath(std::string(std::getenv("HOME")) + "/"); const std::string TestPath(HomePath + ".config/gdpm/tests"); const std::string ConfigPath(HomePath + ".config/gdpm/config.json"); @@ -24,7 +25,7 @@ namespace gdpm::constants{ #define GDPM_CONFIG_GODOT_VERSION "3.4" #define GDPM_CONFIG_LOCAL_PACKAGES_DIR gdpm::constants::LocalPackagesDir #define GDPM_CONFIG_LOCAL_TMP_DIR gdpm::constants::TemporaryPath -#define GDPM_CONFIG_REMOTE_SOURCES constants::HostUrl +#define GDPM_CONFIG_REMOTE_SOURCES std::pair(constants::RemoteName, constants::HostUrl) #define GDPM_CONFIG_THREADS 1 #define GDPM_CONFIG_TIMEOUT_MS 30000 #define GDPM_CONFIG_ENABLE_SYNC 1 diff --git a/include/error.hpp b/include/error.hpp index a784688..64abf1a 100644 --- a/include/error.hpp +++ b/include/error.hpp @@ -1,43 +1,90 @@ +#pragma once -#include -#include #include "log.hpp" +#include "types.hpp" +#include +#include +#include +#include + +namespace gdpm::constants::error{ -namespace gdpm::error_codes{ enum { - NONE = 0, - UNKNOWN = 1, - NOT_FOUND = 2, - FILE_EXISTS = 3, - HOST_UNREACHABLE = 4, + NONE = 0, + UNKNOWN, + UNKNOWN_COMMAND, + NOT_FOUND, + NOT_DEFINED, + NOT_IMPLEMENTED, + NO_PACKAGE_FOUND, + PATH_NOT_DEFINED, + FILE_EXISTS, + FILE_NOT_FOUND, + DIRECTORY_EXISTS, + DIRECTORY_NOT_FOULD, + HOST_UNREACHABLE, + EMPTY_RESPONSE, + INVALID_ARGS, + INVALID_CONFIG, + INVALID_KEY, + HTTP_RESPONSE_ERROR, + STD_ERROR }; - inline std::string to_string(int error_code) { - return ""; + const string_list messages { + "", + "An unknown error has occurred.", + "Unknown command.", + "Resource not found.", + "Function not defined.", + "Function not implemented.", + "No package found.", + "Path is not well-defined", + "File found.", + "File does not exist.", + "Directory exists.", + "Directory not found.", + "No response from host. Host is unreacheable", + "Empty response from host.", + "Invalid arguments.", + "Invalid configuration.", + "Invalid key.", + "An HTTP response error has occurred.", + "An error has occurred." + }; + + + inline string get_message(int error_code) { + string message{}; + try{ message = messages[error_code]; } + catch(const std::bad_alloc& e){ + log::error("No default message for error code."); + } + return message; } }; namespace gdpm{ class error { public: - error(int code = 0, const std::string& message = "", bool print_message = false): - m_code(code), m_message(message) - { - if(print_message) - print(); - } + constexpr explicit error(int code = 0, const string& message = "{code}"): + m_code(code), m_message(message == "{code}" ? constants::error::get_message(code): message) + {} void set_code(int code) { m_code = code; } - void set_message(const std::string& message) { m_message = message; } + void set_message(const string& message) { m_message = message; } int get_code() const { return m_code; } - std::string get_message() const { return m_message; } - bool has_error() const { return m_code != 0; } - void print(){ log::println(GDPM_COLOR_LOG_ERROR "ERROR: {}" GDPM_COLOR_LOG_RESET, m_message); } + string get_message() const { return m_message; } + bool has_occurred() const { return m_code != 0; } + + bool operator()(){ + return has_occurred(); + } private: int m_code; - std::string m_message; + string m_message; }; // Add logging function that can handle error objects diff --git a/include/http.hpp b/include/http.hpp index a73e1a7..3e012f4 100644 --- a/include/http.hpp +++ b/include/http.hpp @@ -1,18 +1,31 @@ #pragma once #include "constants.hpp" - +#include "types.hpp" #include namespace gdpm::http{ + using headers_t = std::unordered_map; + enum response_code{ + OK = 200, + NOT_FOUND = 400 + }; struct response{ long code = 0; - std::string body{}; - std::unordered_map headers{}; + string body{}; + headers_t headers{}; + error error(); }; - response request_get(const std::string& url, size_t timeout = GDPM_CONFIG_TIMEOUT_MS, int verbose = 0); - response request_post(const std::string& url, const char *post_fields="", size_t timeout = GDPM_CONFIG_TIMEOUT_MS, int verbose = 0); - response download_file(const std::string& url, const std::string& storage_path, size_t timeout = GDPM_CONFIG_TIMEOUT_MS, int verbose = 0); + + struct params { + size_t timeout = GDPM_CONFIG_TIMEOUT_MS; + int verbose = 0; + }; + + string url_escape(const string& url); + response request_get(const string& url, const http::params& params = http::params()); + response request_post(const string& url, const char *post_fields="", const http::params& params = http::params()); + response download_file(const string& url, const string& storage_path, const http::params& params = http::params()); } \ No newline at end of file diff --git a/include/log.hpp b/include/log.hpp index 9e697c2..cb2de48 100644 --- a/include/log.hpp +++ b/include/log.hpp @@ -3,14 +3,14 @@ #include "utils.hpp" #include "colors.hpp" -#include +// #include #if __cplusplus > 201703L // #include #else #endif -#include -#include +// #include +// #include /* TODO: Allow setting the logging *prefix* diff --git a/include/package.hpp b/include/package.hpp new file mode 100644 index 0000000..4568397 --- /dev/null +++ b/include/package.hpp @@ -0,0 +1,113 @@ +#pragma once + +#include "constants.hpp" +#include "error.hpp" +#include "package.hpp" +#include "types.hpp" +#include "result.hpp" +#include "config.hpp" +#include +#include +#include +#include +#include + + +namespace gdpm::package { + + struct info { + size_t asset_id; + string type; + string title; + string author; + size_t author_id; + string version; + string godot_version; + string cost; + string description; + string modify_date; + string support_level; + string category; + string remote_source; + string download_url; + string download_hash; + bool is_installed; + string install_path; + std::vector dependencies; + }; + + struct params { + enum install_method_e{ + GLOBAL_LINK_LOCAL, + GLOBAL_CLONE_LOCAL, + GLOBAL_ONLY, + LOCAL_ONLY + }; + bool parallel_jobs = 1; + bool use_cache = true; + bool use_remote = true; + bool skip_prompt = false; + std::string remote_source = ""; + install_method_e install_method = GLOBAL_LINK_LOCAL; + }; + + using info_list = std::vector; + using title_list = std::vector; + using id_list = std::vector; + using path = std::string; + using path_list = std::vector; + + /*! + @brief Install a Godot package from the Asset Library in the current project. + By default, packages are stored in a global directory and linked to the project + where the tool is executed. Use the `--jobs` option to install packages in + parallel. Specify which remote source to use by passing the `--remote` option. + By default, the first remote source will by used. Alternatively, if the + `--use-remote=false` option is passed, then the tool will only attempt to fetch the + package from cache. Use the `--use-cache=false` option to fetch package only from + remote source. + + `gdpm install "super cool example package" + + To only install a package global without linking to the project, use the + `--global-only` option. + + `gdpm install --global-only "super cool example package" + + To install a package to a local project only, use the `--local-only` option. + This will extract the package contents to the project location instead of the + global install location. Use the `--path` option to specify an alternative + path. + + `gdpm install --local-only "super cool example package" --path addons/example + + To copy the package to a project instead of linking, use the `--clone` option. + + `gdpm install --clone "super cool examle package" + + */ + GDPM_DLL_EXPORT error install(const config::context& config, const title_list& package_titles, const params& params = package::params()); + + /*! + @brief Remove's package and contents from local database. + */ + GDPM_DLL_EXPORT error remove(const config::context& config, const title_list& package_titles, const params& params = package::params()); + GDPM_DLL_EXPORT error remove_all(const config::context& config, const params& params = package::params()); + GDPM_DLL_EXPORT error update(const config::context& config, const title_list& package_titles, const params& params = package::params()); + GDPM_DLL_EXPORT error search(const config::context& config, const title_list& package_titles, const params& params = package::params()); + GDPM_DLL_EXPORT error list(const config::context& config, const args_t& args, const opts_t& opts); + GDPM_DLL_EXPORT error export_to(const path_list& paths); + GDPM_DLL_EXPORT error link(const config::context& config, const title_list& package_titles, const opts_t& opts); + GDPM_DLL_EXPORT error clone(const config::context& config, const title_list& package_titles, const opts_t& opts); + + + GDPM_DLL_EXPORT void print_list(const rapidjson::Document& json); + GDPM_DLL_EXPORT void print_list(const info_list& packages); + GDPM_DLL_EXPORT result_t get_package_info(const opts_t& opts); + GDPM_DLL_EXPORT result_t get_package_titles(const info_list& packages); + GDPM_DLL_EXPORT void clean_temporary(const config::context& config, const title_list& package_titles); + + /* Dependency Management API */ + GDPM_DLL_EXPORT result_t synchronize_database(const config::context& config, const title_list& package_titles); + GDPM_DLL_EXPORT result_t resolve_dependencies(const config::context& config, const title_list& package_titles); +} \ No newline at end of file diff --git a/include/package_manager.hpp b/include/package_manager.hpp index 9584014..1ec1771 100644 --- a/include/package_manager.hpp +++ b/include/package_manager.hpp @@ -1,7 +1,10 @@ #pragma once #include "config.hpp" +#include "package.hpp" #include "package_manager.hpp" +#include "remote.hpp" +#include "result.hpp" #include #include #include @@ -12,39 +15,23 @@ #include #include -namespace gdpm::package_manager{ - extern std::vector repo_sources; +namespace gdpm::package_manager { + extern remote::repository_map repo_sources; extern CURL *curl; extern CURLcode res; extern config::context config; - struct package_info{ - size_t asset_id; - std::string type; - std::string title; - std::string author; - size_t author_id; - std::string version; - std::string godot_version; - std::string cost; - std::string description; - std::string modify_date; - std::string support_level; - std::string category; - std::string remote_source; - std::string download_url; - std::string download_hash; - bool is_installed; - std::string install_path; - std::vector dependencies; - }; - struct cxxargs{ cxxopts::ParseResult result; cxxopts::Options options; }; - enum command_e{ + struct exec_args{ + args_t args; + opts_t opts; + }; + + enum class action_e{ install, remove, update, @@ -60,45 +47,12 @@ namespace gdpm::package_manager{ none }; - using package_list = std::vector; - using package_titles = std::vector; - using cl_arg = std::variant; - using cl_args = std::vector; - using cl_opts = std::unordered_map; - - GDPM_DLL_EXPORT int initialize(int argc, char **argv); - GDPM_DLL_EXPORT int execute(); +GDPM_DLL_EXPORT result_t initialize(int argc, char **argv); + GDPM_DLL_EXPORT int execute(const args_t& args, const opts_t& opts); GDPM_DLL_EXPORT void finalize(); - /* Package management API */ - GDPM_DLL_EXPORT error install_packages(const std::vector& package_titles, bool skip_prompt = false); - GDPM_DLL_EXPORT error remove_packages(const std::vector& package_titles, bool skip_prompt = false); - GDPM_DLL_EXPORT error remove_all_packages(); - GDPM_DLL_EXPORT error update_packages(const std::vector& package_titles, bool skip_prompt = false); - GDPM_DLL_EXPORT error search_for_packages(const std::vector& package_titles, bool skip_prompt = false); - GDPM_DLL_EXPORT error export_packages(const std::vector& paths); - GDPM_DLL_EXPORT std::vector list_information(const std::vector& opts, bool print_list = true); - GDPM_DLL_EXPORT void clean_temporary(const std::vector& package_titles); - GDPM_DLL_EXPORT void link_packages(const std::vector& package_titles, const std::vector& paths); - GDPM_DLL_EXPORT void clone_packages(const std::vector& package_titles, const std::vector& paths); - - /* Remote API */ - GDPM_DLL_EXPORT error _handle_remote(const std::vector& args, const std::vector& opts); - GDPM_DLL_EXPORT void remote_add_repository(const std::vector& repositories); - GDPM_DLL_EXPORT void remote_remove_respository(const std::vector& repositories); - GDPM_DLL_EXPORT void remote_remove_respository(ssize_t index); - GDPM_DLL_EXPORT void remote_move_repository(int old_position, int new_position); - /* Auxiliary Functions */ GDPM_DLL_EXPORT cxxargs _parse_arguments(int argc, char **argv); - GDPM_DLL_EXPORT void _handle_arguments(const cxxargs& args); - GDPM_DLL_EXPORT void run_command(command_e command, const std::vector& package_titles, const std::vector& opts); - GDPM_DLL_EXPORT void print_package_list(const rapidjson::Document& json); - GDPM_DLL_EXPORT void print_package_list(const std::vector& packages); - GDPM_DLL_EXPORT void print_remote_sources(); - GDPM_DLL_EXPORT std::vector get_package_titles(const std::vector& packages); - - /* Dependency Management API */ - GDPM_DLL_EXPORT std::vector synchronize_database(const std::vector& package_titles); - GDPM_DLL_EXPORT std::vector resolve_dependencies(const std::vector& package_titles); + GDPM_DLL_EXPORT result_t _handle_arguments(const cxxargs& args); + GDPM_DLL_EXPORT void run_command(action_e command, const package::title_list& package_titles, const opts_t& opts); } \ No newline at end of file diff --git a/include/plugin.hpp b/include/plugin.hpp index b57d56a..e4441aa 100644 --- a/include/plugin.hpp +++ b/include/plugin.hpp @@ -2,7 +2,7 @@ #include -namespace towk::plugin{ +namespace gdpm::plugin{ struct info{ std::string name; std::string description; diff --git a/include/remote.hpp b/include/remote.hpp new file mode 100644 index 0000000..3446c54 --- /dev/null +++ b/include/remote.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "constants.hpp" +#include "error.hpp" +#include "types.hpp" +#include +#include +#include "config.hpp" + +namespace gdpm::remote{ + using repo_names = string_list; + using repo_urls = string_list; + using repository_map = string_map; + + GDPM_DLL_EXPORT error _handle_remote(config::context& config, const args_t& args, const opts_t& opts); + GDPM_DLL_EXPORT void set_repositories(config::context& context, const repository_map& repos); + GDPM_DLL_EXPORT void add_repositories(config::context& context, const repository_map& repos); + GDPM_DLL_EXPORT void remove_respositories(config::context& context, const repo_names& name); + GDPM_DLL_EXPORT void move_repository(config::context& context, int old_position, int new_position); + GDPM_DLL_EXPORT void print_repositories(const config::context& context); +} \ No newline at end of file diff --git a/include/rest_api.hpp b/include/rest_api.hpp index 88d47f8..0bd8a83 100644 --- a/include/rest_api.hpp +++ b/include/rest_api.hpp @@ -1,5 +1,6 @@ #include "constants.hpp" +#include "config.hpp" #include #include #include @@ -47,6 +48,7 @@ namespace gdpm::rest_api{ int verbose; }; + context make_from_config(const config::context& config); context make_context(type_e type = GDPM_DEFAULT_ASSET_TYPE, int category = GDPM_DEFAULT_ASSET_CATEGORY, support_e support = GDPM_DEFAULT_ASSET_SUPPORT, const std::string& filter = GDPM_DEFAULT_ASSET_FILTER, const std::string& user = GDPM_DEFAULT_ASSET_USER, const std::string& godot_version = GDPM_DEFAULT_ASSET_GODOT_VERSION, int max_results = GDPM_DEFAULT_ASSET_MAX_RESULTS, int page = GDPM_DEFAULT_ASSET_PAGE, sort_e sort = GDPM_DEFAULT_ASSET_SORT, bool reverse = GDPM_DEFAULT_ASSET_REVERSE, int verbose = GDPM_DEFAULT_ASSET_VERBOSE); std::string to_string(type_e type); diff --git a/include/result.hpp b/include/result.hpp new file mode 100644 index 0000000..ac9229a --- /dev/null +++ b/include/result.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "log.hpp" +#include "error.hpp" +#include "types.hpp" +#include +#include + +namespace gdpm{ + + template + class result_t { + public: + result_t() = delete; + result_t( + std::tuple tuple, + std::function ok = []() -> T{}, + std::function error = [](){} + ): data(tuple), fn_ok(ok), fn_error(error) + { } + + result_t( + T target, + U error, + std::function()> _fn_ok = []() -> std::unique_ptr{ return nullptr; }, + std::function _fn_error = [](){} + ): data(std::make_tuple(target, error)), fn_ok(_fn_ok), fn_error(_fn_error) + {} + + void define( + std::function ok, + std::function error + ){ + fn_ok = ok; + fn_error = error; + } + + constexpr std::unique_ptr unwrap() const { + /* First, check if ok() and error() are defined. */ + if(!fn_error || !fn_ok){ + error error( + constants::error::NOT_DEFINED + ); + log::error(error); + return nullptr; + } + /* Then, attempt unwrap the data. */ + U err = std::get(data); + if (err.has_occurred()) + if(fn_error){ + fn_error(); + return nullptr; + } + return fn_ok(); + } + + constexpr T unwrap_or(T default_value) const { + U err = std::get(data); + if(err.has_occurred()) + return default_value; + return fn_ok(); + } + + constexpr T unwrap_unsafe() const { + return std::get(data); + } + + private: + std::tuple data; + std::function()> fn_ok; + std::function fn_error; + }; +} \ No newline at end of file diff --git a/include/types.hpp b/include/types.hpp index eeba2af..bada267 100644 --- a/include/types.hpp +++ b/include/types.hpp @@ -1,6 +1,14 @@ #pragma once +#include +#include +#include +#include +#include + namespace gdpm{ + class error; + /* Base class to prevent derived class from creating copies. */ @@ -20,4 +28,20 @@ namespace gdpm{ non_movable(const non_movable&) = delete; non_movable(non_movable&&) = delete; }; + + template + concept error_t = requires{ std::is_same::value; }; + + using string = std::string; + using string_list = std::vector; + using string_map = std::unordered_map; + using string_pair = std::pair; + using var = std::variant; + template + using _args_t = std::vector; + using args_t = _args_t; + template > + using _opts_t = std::unordered_map; + using opts_t = _opts_t; + } diff --git a/include/utils.hpp b/include/utils.hpp index 959ebe9..89293ed 100644 --- a/include/utils.hpp +++ b/include/utils.hpp @@ -102,21 +102,6 @@ namespace gdpm::utils{ std::string prompt_user(const char *message); bool prompt_user_yn(const char *message); void delay(std::chrono::milliseconds milliseconds = GDPM_REQUEST_DELAY); + std::string join(const std::vector& target, const std::string& delimiter = ", "); // TODO: Add function to get size of decompressed zip } - -namespace gdpm{ - class non_copyable{ - public: - non_copyable(){} - - private: - non_copyable(const non_copyable&); - non_copyable& operator=(const non_copyable&); - }; - - class non_movable{ - non_movable(const non_movable&) = delete; - non_movable(non_movable&&) = delete; - }; -} \ No newline at end of file diff --git a/src/cache.cpp b/src/cache.cpp index add534a..73fa7b0 100644 --- a/src/cache.cpp +++ b/src/cache.cpp @@ -2,35 +2,44 @@ #include "cache.hpp" #include "log.hpp" #include "constants.hpp" -#include "package_manager.hpp" +#include "package.hpp" #include "utils.hpp" +#include "result.hpp" #include #include +#include +#include namespace gdpm::cache{ - int create_package_database(const std::string& cache_path, const std::string& table_name){ + error create_package_database(bool overwrite, const params& params){ sqlite3 *db; sqlite3_stmt *res; char *errmsg; /* Check and make sure directory is created before attempting to open */ namespace fs = std::filesystem; - fs::path dir_path = fs::path(cache_path).parent_path(); + fs::path dir_path = fs::path(params.cache_path).parent_path(); if(!fs::exists(dir_path)){ - log::info("Creating cache directories...{}", cache_path); + log::info("Creating cache directories...{}", params.cache_path); fs::create_directories(dir_path); } - int rc = sqlite3_open(cache_path.c_str(), &db); + int rc = sqlite3_open(params.cache_path.c_str(), &db); if(rc != SQLITE_OK){ - log::error("create_package_database.sqlite3_open(): {}", sqlite3_errmsg(db)); + error error(rc, + std::format( + "create_package_database.sqlite3_open(): {}", + sqlite3_errmsg(db) + ) + ); + log::error(error); sqlite3_close(db); - return rc; + return error; } - std::string sql = "CREATE TABLE IF NOT EXISTS " + - table_name + "(" + string sql = "CREATE TABLE IF NOT EXISTS " + + params.table_name + "(" "id INTEGER PRIMARY KEY AUTOINCREMENT," "asset_id INT NOT NULL," "type INT NOT NULL," @@ -54,60 +63,71 @@ namespace gdpm::cache{ rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errmsg); if(rc != SQLITE_OK){ // log::error("Failed to fetch data: {}\n", sqlite3_errmsg(db)); - log::error("create_package_database.sqlite3_exec(): {}", errmsg); + error error(rc, std::format( + "create_package_database.sqlite3_exec(): {}", + errmsg + )); + log::error(error); sqlite3_free(errmsg); sqlite3_close(db); - return rc; + return error; } sqlite3_close(db); - return 0; + return error(); } - int insert_package_info(const std::vector& packages, const std::string& cache_path, const std::string& table_name){ + error insert_package_info(const package::info_list& packages, const params& params){ sqlite3 *db; sqlite3_stmt *res; char *errmsg = nullptr; /* Prepare values to use in sql statement */ - std::string sql{"BEGIN TRANSACTION; "}; + string sql{"BEGIN TRANSACTION; "}; for(const auto& p : packages){ - sql += "INSERT INTO " + table_name + " (" GDPM_PACKAGE_CACHE_COLNAMES ") "; - sql += "VALUES (" + to_values(p) + "); "; + sql += "INSERT INTO " + params.table_name + " (" GDPM_PACKAGE_CACHE_COLNAMES ") "; + sql += "VALUES (" + to_values(p).unwrap_unsafe() + "); "; } sql += "COMMIT;"; // log::println("{}", sql); - int rc = sqlite3_open(cache_path.c_str(), &db); + int rc = sqlite3_open(params.cache_path.c_str(), &db); if(rc != SQLITE_OK){ - log::error("insert_package_info.sqlite3_open(): {}", sqlite3_errmsg(db)); + error error(rc, std::format( + "insert_package_info.sqlite3_open(): {}", + sqlite3_errmsg(db) + )); + log::error(error); sqlite3_close(db); - return rc; + return error; } rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errmsg); if(rc != SQLITE_OK){ - log::error("insert_package_info.sqlite3_exec(): {}", errmsg); + error error(rc, std::format( + "insert_package_info.sqlite3_exec(): {}", errmsg + )); + log::error(error); sqlite3_free(errmsg); sqlite3_close(db); - return rc; + return error; } sqlite3_close(db); - return 0; + return error(); } - std::vector get_package_info_by_id(const std::vector& package_ids, const std::string& cache_path, const std::string& table_name){ + result_t get_package_info_by_id(const package::id_list& package_ids, const params& params){ sqlite3 *db; sqlite3_stmt *res; char *errmsg = nullptr; size_t p_size = 0; - std::vector p_vector; - std::string sql{"BEGIN TRANSACTION;\n"}; + package::info_list p_vector; + string sql{"BEGIN TRANSACTION;\n"}; auto callback = [](void *data, int argc, char **argv, char **colnames){ // log::error("{}", (const char*)data); // p_data *_data = (p_data*)data; - std::vector *_p_vector = (std::vector*) data; - package_info p{ + package::info_list *_p_vector = (package::info_list*) data; + package::info p{ .asset_id = std::stoul(argv[1]), .type = argv[2], .title = argv[3], @@ -130,41 +150,47 @@ namespace gdpm::cache{ return 0; }; - int rc = sqlite3_open(cache_path.c_str(), &db); + int rc = sqlite3_open(params.cache_path.c_str(), &db); if(rc != SQLITE_OK){ - log::error("get_package_info_by_id.sqlite3_open(): {}", sqlite3_errmsg(db)); + error error(rc, std::format( + "get_package_info_by_id.sqlite3_open(): {}", sqlite3_errmsg(db) + )); + log::error(error); sqlite3_close(db); - return {}; + return result_t(package::info_list(), error); } for(const auto& p_id : package_ids){ - sql += "SELECT * FROM " + table_name + " WHERE asset_id=" + fmt::to_string(p_id)+ ";\n"; + sql += "SELECT * FROM " + params.table_name + " WHERE asset_id=" + std::to_string(p_id)+ ";\n"; } sql += "COMMIT;\n"; rc = sqlite3_exec(db, sql.c_str(), callback, (void*)&p_vector, &errmsg); if(rc != SQLITE_OK){ + error error(rc, std::format( + "get_package_info_by_id.sqlite3_exec(): {}", errmsg + )); log::error("get_package_info_by_id.sqlite3_exec(): {}", errmsg); sqlite3_free(errmsg); sqlite3_close(db); - return {}; + return result_t(package::info_list(), error); } sqlite3_close(db); - return p_vector; + return result_t(p_vector, error()); } - std::vector get_package_info_by_title(const std::vector& package_titles, const std::string& cache_path, const std::string& table_name){ + result_t get_package_info_by_title(const package::title_list& package_titles, const params& params){ sqlite3 *db; sqlite3_stmt *res; char *errmsg = nullptr; - std::vector p_vector; + package::info_list p_vector; auto callback = [](void *data, int argc, char **argv, char **colnames){ if(argc <= 0) return 1; - std::vector *_p_vector = (std::vector*)data; + package::info_list *_p_vector = (package::info_list*)data; // log::println("get_package_info_by_title.callback.argv: \n\t{}\n\t{}\n\t{}\n\t{}\n\t{}", argv[0], argv[1], argv[2],argv[3], argv[4]); - package_info p{ + package::info p{ .asset_id = std::stoul(argv[1]), .type = argv[2], .title = argv[3], @@ -188,44 +214,50 @@ namespace gdpm::cache{ }; /* Check to make sure the directory is there before attempting to open */ - if(!std::filesystem::exists(cache_path)) - std::filesystem::create_directories(cache_path); + if(!std::filesystem::exists(params.cache_path)) + std::filesystem::create_directories(params.cache_path); - int rc = sqlite3_open(cache_path.c_str(), &db); + int rc = sqlite3_open(params.cache_path.c_str(), &db); if(rc != SQLITE_OK){ - log::error("get_package_info_by_title.sqlite3_open(): {}", sqlite3_errmsg(db)); + error error(rc, std::format( + "get_package_info_by_title.sqlite3_open(): {}", sqlite3_errmsg(db) + )); + log::error(error); sqlite3_close(db); - return {}; + return result_t(package::info_list(), error); } - std::string sql{"BEGIN TRANSACTION;"}; + string sql{"BEGIN TRANSACTION;"}; for(const auto& p_title : package_titles){ - sql += "SELECT * FROM " + table_name + " WHERE title='" + p_title + "';"; + sql += "SELECT * FROM " + params.table_name + " WHERE title='" + p_title + "';"; } sql += "COMMIT;"; // log::println(sql); rc = sqlite3_exec(db, sql.c_str(), callback, (void*)&p_vector, &errmsg); if(rc != SQLITE_OK){ - log::error("get_package_info_by_title.sqlite3_exec(): {}", errmsg); + error error(rc, std::format( + "get_package_info_by_title.sqlite3_exec(): {}", errmsg + )); + log::error(error); sqlite3_free(errmsg); sqlite3_close(db); - return {}; + return result_t(package::info_list(), error); } sqlite3_close(db); - return p_vector; + return result_t(p_vector, error()); } - std::vector get_installed_packages(const std::string& cache_path, const std::string& table_name){ + result_t get_installed_packages(const params& params){ sqlite3 *db; sqlite3_stmt *res; char *errmsg = nullptr; - std::vector p_vector; - std::string sql{"BEGIN TRANSACTION;"}; + package::info_list p_vector; + string sql{"BEGIN TRANSACTION;"}; auto callback = [](void *data, int argc, char **argv, char **colnames){ - std::vector *_p_vector = (std::vector*) data; - package_info p{ + package::info_list *_p_vector = (package::info_list*) data; + package::info p{ .asset_id = std::stoul(argv[1]), .type = argv[2], .title = argv[3], @@ -248,46 +280,55 @@ namespace gdpm::cache{ return 0; }; - int rc = sqlite3_open(cache_path.c_str(), &db); + int rc = sqlite3_open(params.cache_path.c_str(), &db); if(rc != SQLITE_OK){ - log::error("get_installed_packages.sqlite3_open(): {}", sqlite3_errmsg(db)); + error error(rc, std::format( + "get_installed_packages.sqlite3_open(): {}", sqlite3_errmsg(db) + )); + log::error(error); sqlite3_close(db); - return {}; + return result_t(package::info_list(), error); } - sql += "SELECT * FROM " + table_name + " WHERE is_installed=1; COMMIT;"; + sql += "SELECT * FROM " + params.table_name + " WHERE is_installed=1; COMMIT;"; rc = sqlite3_exec(db, sql.c_str(), callback, (void*)&p_vector, &errmsg); if(rc != SQLITE_OK){ - log::error("get_installed_packages.sqlite3_exec(): {}", errmsg); + error error(rc, std::format( + "get_installed_packages.sqlite3_exec(): {}", errmsg + )); + log::error(error); sqlite3_free(errmsg); sqlite3_close(db); - return {}; + return result_t(package::info_list(), error); } sqlite3_close(db); - return p_vector; + return result_t(p_vector, error()); } - int update_package_info(const std::vector& packages, const std::string& cache_path, const std::string& table_name){ + error update_package_info(const package::info_list& packages, const params& params){ sqlite3 *db; sqlite3_stmt *res; char *errmsg = nullptr; - int rc = sqlite3_open(cache_path.c_str(), &db); + int rc = sqlite3_open(params.cache_path.c_str(), &db); if(rc != SQLITE_OK){ - log::error("update_package_info.sqlite3_open(): {}", sqlite3_errmsg(db)); + error error(rc, std::format( + "update_package_info.sqlite3_open(): {}", sqlite3_errmsg(db) + )); + log::error(error); sqlite3_close(db); - return rc; + return error; } - std::string sql; + string sql; for(const auto& p : packages){ - sql += "UPDATE " + table_name + " SET " - " asset_id=" + fmt::to_string(p.asset_id) + ", " + sql += "UPDATE " + params.table_name + " SET " + " asset_id=" + std::to_string(p.asset_id) + ", " " type='" + p.type + "', " " title='" + p.title + "', " " author='" + p.author + "', " + - " author_id=" + fmt::to_string(p.author_id) + ", " + " author_id=" + std::to_string(p.author_id) + ", " " version='" + p.version + "', " + " godot_version='" + p.godot_version + "', " + " cost='" + p.cost + "', " + @@ -298,115 +339,137 @@ namespace gdpm::cache{ " remote_source='" + p.remote_source + "', " + " download_url='" + p.download_url + "', " + " download_hash='" + p.download_hash + "', " + - " is_installed=" + fmt::to_string(p.is_installed) + ", " + " is_installed=" + std::to_string(p.is_installed) + ", " " install_path='" + p.install_path + "'" // " dependencies='" + p.dependencies + "'" - " WHERE title='" + p.title + "' AND asset_id=" + fmt::to_string(p.asset_id) + " WHERE title='" + p.title + "' AND asset_id=" + std::to_string(p.asset_id) + ";\n"; } rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errmsg); if(rc != SQLITE_OK){ - log::error("update_package_info.sqlite3_exec(): {}", errmsg); + error error(rc, std::format( + "update_package_info.sqlite3_exec(): {}", errmsg + )); + log::error(error); sqlite3_free(errmsg); sqlite3_close(db); - return rc; + return error; } sqlite3_close(db); - return 0; + return error(); } - int delete_packages(const std::vector& package_titles, const std::string& cache_path, const std::string& table_name){ + error delete_packages(const package::title_list& package_titles, const params& params){ sqlite3 *db; sqlite3_stmt *res; char *errmsg = nullptr; - std::string sql; + string sql; - int rc = sqlite3_open(cache_path.c_str(), &db); + int rc = sqlite3_open(params.cache_path.c_str(), &db); if(rc != SQLITE_OK){ - log::error("delete_packages.sqlite3_open(): {}", sqlite3_errmsg(db)); + error error(rc, std::format( + "delete_packages.sqlite3_open(): {}", sqlite3_errmsg(db) + )); + log::error(error); sqlite3_close(db); - return rc; + return error; } for(const auto& p_title : package_titles){ - sql += "DELETE FROM " + table_name + " WHERE title='" + sql += "DELETE FROM " + params.table_name + " WHERE title='" + p_title + "';\n"; } rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errmsg); if(rc != SQLITE_OK){ - log::error("delete_packages.sqlite3_exec(): {}", errmsg); + error error(rc, std::format( + "delete_packages.sqlite3_exec(): {}", errmsg + )); + log::error(error); sqlite3_free(errmsg); sqlite3_close(db); - return rc; + return error; } sqlite3_close(db); - return 0; + return error(); } - int delete_packages(const std::vector& package_ids, const std::string& cache_path, const std::string& table_name){ + error delete_packages(const package::id_list& package_ids, const params& params){ sqlite3 *db; sqlite3_stmt *res; char *errmsg = nullptr; - std::string sql; + string sql; - int rc = sqlite3_open(cache_path.c_str(), &db); + int rc = sqlite3_open(params.cache_path.c_str(), &db); if(rc != SQLITE_OK){ - log::error("delete_packages.sqlite3_open(): {}", errmsg); + error error(rc, std::format( + "delete_packages.sqlite3_open(): {}", errmsg + )); + log::error(error); sqlite3_close(db); - return rc; + return error; } for(const auto& p_id : package_ids){ - sql += "DELETE FROM " + table_name + " WHERE asset_id=" - + fmt::to_string(p_id) + ";\n"; + sql += "DELETE FROM " + params.table_name + " WHERE asset_id=" + + std::to_string(p_id) + ";\n"; } rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errmsg); if(rc != SQLITE_OK){ - log::error("delete_packages.sqlite3_exec(): {}", errmsg); + error error(rc, std::format( + "delete_packages.sqlite3_exec(): {}", errmsg + )); + log::error(error); sqlite3_free(errmsg); sqlite3_close(db); - return rc; + return error; } sqlite3_close(db); - return 0; + return error(); } - int drop_package_database(const std::string& cache_path, const std::string& table_name){ + error drop_package_database(const params& params){ sqlite3 *db; sqlite3_stmt *res; char *errmsg = nullptr; - std::string sql{"DROP TABLE IF EXISTS " + table_name + ";\n"}; + string sql{"DROP TABLE IF EXISTS " + params.table_name + ";\n"}; - int rc = sqlite3_open(cache_path.c_str(), &db); + int rc = sqlite3_open(params.cache_path.c_str(), &db); if(rc != SQLITE_OK){ - log::error("drop_package_database.sqlite3_open(): {}", sqlite3_errmsg(db)); + error error(rc, std::format( + "drop_package_database.sqlite3_open(): {}", sqlite3_errmsg(db) + )); + log::error(error); sqlite3_close(db); - return rc; + return error; } rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errmsg); if(rc != SQLITE_OK){ - log::error("drop_package_database.sqlite3_exec(): {}", errmsg); + error error(rc, std::format( + "drop_package_database.sqlite3_exec(): {}", errmsg + )); + log::error(error); sqlite3_free(errmsg); sqlite3_close(db); - return rc; + return error; } - return 0; + return error(); } - std::string to_values(const package_info& p){ - std::string p_values{}; - std::string p_title = p.title; /* need copy for utils::replace_all() */ - p_values += fmt::to_string(p.asset_id) + ", "; + result_t to_values(const package::info& p){ + string p_values{}; + string p_title = p.title; /* need copy for utils::replace_all() */ + + p_values += std::to_string(p.asset_id) + ", "; p_values += "'" + p.type + "', "; p_values += "'" + utils::replace_all(p_title, "'", "''") + "', "; p_values += "'" + p.author + "', "; - p_values += fmt::to_string(p.author_id) + ", "; + p_values += std::to_string(p.author_id) + ", "; p_values += "'" + p.version + "', "; p_values += "'" + p.godot_version + "', "; p_values += "'" + p.cost + "', "; @@ -417,16 +480,17 @@ namespace gdpm::cache{ p_values += "'" + p.remote_source + "', "; p_values += "'" + p.download_url + "', "; p_values += "'" + p.download_hash + "', "; - p_values += fmt::to_string(p.is_installed) + ", "; + p_values += std::to_string(p.is_installed) + ", "; p_values += "'" + p.install_path + "'"; - return p_values; + return result_t(p_values, error()); } - std::string to_values(const std::vector& packages){ - std::string o; + + result_t to_values(const package::info_list& packages){ + string o; for(const auto& p : packages) - o += to_values(p); - return o; + o += to_values(p).unwrap_unsafe(); + return result_t(o, error()); } } \ No newline at end of file diff --git a/src/config.cpp b/src/config.cpp index 683fce4..137bf41 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -1,7 +1,9 @@ #include "config.hpp" +#include "error.hpp" #include "log.hpp" #include "utils.hpp" #include "constants.hpp" +#include "error.hpp" // RapidJSON #include @@ -23,6 +25,8 @@ #include #include #include +#include +#include namespace gdpm::config{ @@ -38,8 +42,19 @@ namespace gdpm::config{ return o; }; + auto _build_json_object = [](const string_map& m){ + string o{"{"}; + std::for_each(m.begin(), m.end(), [&o](const string_pair& p){ + o += std::format("\n\"{}\": \"{}\",", p.first, p.second); + }); + if(o.back() == ',') + o.pop_back(); + o += "}"; + return o; + }; + /* Build a JSON string to pass to document */ - std::string json{ + string json{ "{\"username\":\"" + params.username + "\"," + "\"password\":\"" + params.password + "\"," + "\"path\":\"" + params.path + "\"," @@ -47,7 +62,7 @@ namespace gdpm::config{ + "\"godot_version\":\"" + params.godot_version + "\"," + "\"packages_dir\":\"" + params.packages_dir + "\"," + "\"tmp_dir\":\"" + params.tmp_dir + "\"," - + "\"remote_sources\":" + _build_json_array(params.remote_sources) + "," + + "\"remote_sources\":" + _build_json_object(params.remote_sources) + "," + "\"threads\":" + fmt::to_string(params.threads) + "," + "\"timeout\":" + fmt::to_string(params.timeout) + "," + "\"enable_sync\":" + fmt::to_string(params.enable_sync) + "," @@ -58,17 +73,19 @@ namespace gdpm::config{ } - gdpm::error load(std::filesystem::path path, context& config, int verbose){ + error load( + std::filesystem::path path, + context& config, + int verbose + ){ std::fstream file; - gdpm::error error; file.open(path, std::ios::in); if(!file){ - if(verbose){ + if(verbose) log::info("No configuration file found. Creating a new one."); - config = make_context(); - save(config.path, config, verbose); - } - return error; + config = make_context(); + save(config.path, config, verbose); + return error(); } else if(file.is_open()){ /* @@ -79,24 +96,30 @@ namespace gdpm::config{ using namespace rapidjson; /* Read JSON from config, parse, and check document. Must make sure that program does not crash here and use default config instead! */ - std::string contents, line; + string contents, line; while(std::getline(file, line)) contents += line + "\n"; if(verbose > 0) - log::info("Load config...\n{}", contents.c_str()); + log::info("Loading configuration file...\n{}", contents.c_str()); Document doc; ParseErrorCode status = doc.Parse(contents.c_str()).GetParseError(); if(!doc.IsObject()){ - log::error("Could not load config file."); + error error( + constants::error::FILE_NOT_FOUND, + "Could not load config file." + ); + log::error(error); return error; } - assert(doc.IsObject()); - assert(doc.HasMember("remote_sources")); - assert(doc["remote_sources"].IsArray()); + error error = validate(doc); + if(error()){ + log::error(error); + return error; + } /* Make sure contents were read correctly. */ // if(!status){ @@ -105,16 +128,23 @@ namespace gdpm::config{ // return context(); // } - /* Must check if keys exists first, then populate _config_params. */ + /* Must check if keys exists first, then populate `_config_params`. */ if(doc.HasMember("remote_sources")){ if(doc["remote_sources"].IsArray()){ const Value& srcs = doc["remote_sources"]; - for(auto& src : srcs.GetArray()){ + for(auto& src : srcs.GetObject()){ // config.remote_sources.push_back(src.GetString()); - config.remote_sources.insert(src.GetString()); + config.remote_sources.insert( + std::pair(src.name.GetString(), src.value.GetString()) + ); } - } else{ - log::error("Malformed sources found."); + } else { + gdpm::error error( + constants::error::INVALID_KEY, + "Could not read key `remote_sources`." + ); + log::error(error); + return error; } } auto _get_value_string = [](Document& doc, const char *property){ @@ -135,23 +165,27 @@ namespace gdpm::config{ config.path = _get_value_string(doc, "path"); config.token = _get_value_string(doc, "token"); config.godot_version = _get_value_string(doc, "godot_version"); - config.packages_dir = _get_value_string(doc, "packages_dir"); - config.tmp_dir = _get_value_string(doc, "tmp_dir"); + config.packages_dir = _get_value_string(doc, "packages_dir"); + config.tmp_dir = _get_value_string(doc, "tmp_dir"); config.threads = _get_value_int(doc, "threads"); config.enable_sync = _get_value_int(doc, "enable_sync"); config.enable_file_logging = _get_value_int(doc, "enable_file_logging"); } - return error; + return error(); } - gdpm::error save(std::filesystem::path path, const context& config, int verbose){ + error save( + std::filesystem::path path, + const context& config, + int verbose + ){ using namespace rapidjson; /* Build a JSON string to pass to document */ - std::string json = to_json(config); + string json = to_json(config); if(verbose > 0) - log::info("Save config...\n{}", json.c_str()); + log::info("Saving configuration file...\n{}", json.c_str()); /* Dump JSON config to file */ Document doc; @@ -166,14 +200,28 @@ namespace gdpm::config{ } - context make_context(const std::string& username, const std::string& password, const std::string& path, const std::string& token, const std::string& godot_version, const std::string& packages_dir, const std::string& tmp_dir, const std::set& remote_sources, size_t threads, size_t timeout, bool enable_sync, bool enable_file_logging, int verbose){ + context make_context( + const string& username, + const string& password, + const string& path, + const string& token, + const string& godot_version, + const string& packages_dir, + const string& tmp_dir, + const string_map& remote_sources, + size_t threads, + size_t timeout, + bool enable_sync, + bool enable_file_logging, + int verbose + ){ context config { .username = username, .password = password, .path = path, .token = token, .godot_version = godot_version, - .packages_dir = (packages_dir.empty()) ? std::string(getenv("HOME")) + ".gdpm" : packages_dir, + .packages_dir = (packages_dir.empty()) ? string(getenv("HOME")) + ".gdpm" : packages_dir, .tmp_dir = tmp_dir, .remote_sources = remote_sources, .threads = threads, @@ -185,4 +233,23 @@ namespace gdpm::config{ return config; } + + error validate(const rapidjson::Document& doc){ + error error(constants::error::INVALID_CONFIG, ""); + if(!doc.IsObject()){ + error.set_message("Document is not a JSON object."); + return error; + } + if(!doc.HasMember("remote_sources")){ + error.set_message("Could not find `remote_sources` in config."); + return error; + } + if(!doc["remote_sources"].IsObject()){ + error.set_message("Key `remote_sources` is not a JSON object."); + return error; + } + error.set_code(constants::error::NONE); + return error; + } + } \ No newline at end of file diff --git a/src/http.cpp b/src/http.cpp index 3ffa9a3..050ce9f 100644 --- a/src/http.cpp +++ b/src/http.cpp @@ -8,7 +8,20 @@ namespace gdpm::http{ - response request_get(const std::string& url, size_t timeout, int verbose){ + + string url_escape(const string &url){ + CURL *curl = nullptr; + curl_global_init(CURL_GLOBAL_ALL); + char *escaped_url = curl_easy_escape(curl, url.c_str(), url.size()); + std::string url_copy = escaped_url; + curl_global_cleanup(); + return escaped_url; + } + + response request_get( + const string& url, + const http::params& params + ){ CURL *curl = nullptr; CURLcode res; utils::memory_buffer buf = utils::make_buffer(); @@ -28,10 +41,10 @@ namespace gdpm::http{ curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&buf); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::curl_write_to_buffer); curl_easy_setopt(curl, CURLOPT_USERAGENT, constants::UserAgent.c_str()); - curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, params.timeout); res = curl_easy_perform(curl); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &r.code); - if(res != CURLE_OK && verbose > 0) + if(res != CURLE_OK && params.verbose > 0) log::error("_make_request.curl_easy_perform(): {}", curl_easy_strerror(res)); curl_easy_cleanup(curl); } @@ -42,7 +55,12 @@ namespace gdpm::http{ return r; } - response request_post(const std::string& url, const char *post_fields, size_t timeout, int verbose){ + + response request_post( + const string& url, + const char *post_fields, + const http::params& params + ){ CURL *curl = nullptr; CURLcode res; utils::memory_buffer buf = utils::make_buffer(); @@ -62,10 +80,10 @@ namespace gdpm::http{ curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&buf); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::curl_write_to_buffer); curl_easy_setopt(curl, CURLOPT_USERAGENT, constants::UserAgent.c_str()); - curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, params.timeout); res = curl_easy_perform(curl); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &r.code); - if(res != CURLE_OK && verbose > 0) + if(res != CURLE_OK && params.verbose > 0) log::error("_make_request.curl_easy_perform(): {}", curl_easy_strerror(res)); curl_easy_cleanup(curl); } @@ -76,7 +94,12 @@ namespace gdpm::http{ return r; } - response download_file(const std::string& url, const std::string& storage_path, size_t timeout, int verbose){ + + response download_file( + const string& url, + const string& storage_path, + const http::params& params + ){ CURL *curl = nullptr; CURLcode res; response r; @@ -110,10 +133,10 @@ namespace gdpm::http{ curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::curl_write_to_stream); curl_easy_setopt(curl, CURLOPT_USERAGENT, constants::UserAgent.c_str()); - curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, params.timeout); res = curl_easy_perform(curl); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &r.code); - if(res != CURLE_OK && verbose > 0){ + if(res != CURLE_OK && params.verbose > 0){ log::error("download_file.curl_easy_perform() failed: {}", curl_easy_strerror(res)); } fclose(fp); diff --git a/src/main.cpp b/src/main.cpp index 9174348..f19746d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,11 +4,14 @@ #include "log.hpp" #include "config.hpp" #include "package_manager.hpp" +#include "result.hpp" int main(int argc, char **argv){ - gdpm::package_manager::initialize(argc, argv); - gdpm::package_manager::execute(); - gdpm::package_manager::finalize(); + using namespace gdpm; + result_t r_input = package_manager::initialize(argc, argv); + package_manager::exec_args input = r_input.unwrap_unsafe(); + package_manager::execute(input.args, input.opts); + package_manager::finalize(); return 0; } \ No newline at end of file diff --git a/src/package.cpp b/src/package.cpp new file mode 100644 index 0000000..d5fae0a --- /dev/null +++ b/src/package.cpp @@ -0,0 +1,763 @@ + +#include "package.hpp" +#include "error.hpp" +#include "rest_api.hpp" +#include "config.hpp" +#include "cache.hpp" +#include "http.hpp" +#include "remote.hpp" +#include +#include +#include + +namespace gdpm::package{ + + error install( + const config::context& config, + const package::title_list& package_titles, + const package::params& params + ){ + using namespace rapidjson; + + /* TODO: Need a way to use remote sources from config until none left */ + + /* Check if the package data is already stored in cache. If it is, there + is no need to do a lookup to synchronize the local database since we + have all the information we need to fetch the asset data. */ + result_t result = cache::get_package_info_by_title(package_titles); + package::info_list p_found = {}; + package::info_list p_cache = result.unwrap_unsafe(); + + /* Synchronize database information and then try to get data again from + cache if possible. */ + if(config.enable_sync){ + if(p_cache.empty()){ + result_t result = synchronize_database(config, package_titles); + p_cache = result.unwrap_unsafe(); + } + } + + for(const auto& p_title : package_titles){ + auto found = std::find_if( + p_cache.begin(), + p_cache.end(), + [&p_title](const package::info& p){ + return p.title == p_title; + } + ); + if(found != p_cache.end()){ + p_found.emplace_back(*found); + } + } + + /* Found nothing to install so there's nothing to do at this point. */ + if(p_found.empty()){ + constexpr const char *message = "No packages found to install."; + log::error(message); + return error(constants::error::NOT_FOUND, message); + } + + log::println("Packages to install: "); + for(const auto& p : p_found){ + std::string output((p.is_installed) ? p.title + " (reinstall)" : p.title); + log::print(" {} ", (p.is_installed) ? p.title + " (reinstall)" : p.title); + } + log::println(""); + + if(!params.skip_prompt){ + if(!utils::prompt_user_yn("Do you want to install these packages? (y/n)")) + return error(); + } + + /* Try and obtain all requested packages. */ + using ss_pair = std::pair; + std::vector dir_pairs; + std::vector> tasks; + rest_api::context rest_api_params = rest_api::make_from_config(config); + for(auto& p : p_found){ // TODO: Execute each in parallel using coroutines?? + + /* Check if a remote source was provided. If not, then try to get packages + in global storage location only. */ + + log::info("Fetching asset data for \"{}\"...", p.title); + std::string url{config.remote_sources.at(params.remote_source) + rest_api::endpoints::GET_AssetId}; + std::string package_dir, tmp_dir, tmp_zip; + + /* Retrieve necessary asset data if it was found already in cache */ + Document doc; + bool is_valid = p.download_url.empty() || p.category.empty() || p.description.empty() || p.support_level.empty(); + if(is_valid){ + doc = rest_api::get_asset(url, p.asset_id, rest_api_params); + if(doc.HasParseError() || doc.IsNull()){ + constexpr const char *message = "\nError parsing HTTP response."; + log::error(message); + return error(doc.GetParseError(), message); + } + p.category = doc["category"].GetString(); + p.description = doc["description"].GetString(); + p.support_level = doc["support_level"].GetString(); + p.download_url = doc["download_url"].GetString(); + p.download_hash = doc["download_hash"].GetString(); + } + else{ + log::error("Not a valid package."); + /* Package for in cache so no remote request. Still need to populate RapidJson::Document to write to package.json. + NOTE: This may not be necessary at all! + */ + // doc["asset_id"].SetUint64(p.asset_id + // doc["type"].SetString(p.type, doc.GetAllocator()); + // doc["title"].SetString(p.title, doc.GetAllocator()); + // doc["author"].SetString(p.author, doc.GetAllocator()); + // doc["author_id"].SetUint64(p.author_id); + // doc["version"].SetString(p.version, doc.GetAllocator()); + // doc["category"].SetString(p.category, doc.GetAllocator()); + // doc["godot_version"].SetString(p.godot_version, doc.GetAllocator()); + // doc["cost"].SetString(p.cost, doc.GetAllocator()); + // doc["description"].SetString(p.description, doc.GetAllocator()); + // doc["support_level"].SetString(p.support_level, doc.GetAllocator()); + // doc["download_url"].SetString(p.download_url, doc.GetAllocator()); + // doc["download_hash"].SetString(p.download_hash, doc.GetAllocator; + } + + /* Set directory and temp paths for storage */ + package_dir = config.packages_dir + "/" + p.title; + tmp_dir = config.tmp_dir + "/" + p.title; + tmp_zip = tmp_dir + ".zip"; + + /* Make directories for packages if they don't exist to keep everything organized */ + if(!std::filesystem::exists(config.tmp_dir)) + std::filesystem::create_directories(config.tmp_dir); + if(!std::filesystem::exists(config.packages_dir)) + std::filesystem::create_directories(config.packages_dir); + + /* Dump asset information for lookup into JSON in package directory */ + if(!std::filesystem::exists(package_dir)) + std::filesystem::create_directory(package_dir); + + std::ofstream ofs(package_dir + "/asset.json"); + OStreamWrapper osw(ofs); + PrettyWriter writer(osw); + doc.Accept(writer); + + /* Check if we already have a stored temporary file before attempting to download */ + if(std::filesystem::exists(tmp_zip) && std::filesystem::is_regular_file(tmp_zip)){ + log::println("Found cached package. Skipping download.", p.title); + } + else{ + /* Download all the package files and place them in tmp directory. */ + log::info_n("Downloading \"{}\"...", p.title); + std::string download_url = p.download_url;// doc["download_url"].GetString(); + std::string title = p.title;// doc["title"].GetString(); + http::response response = http::download_file(download_url, tmp_zip); + if(response.code == http::OK){ + log::println("Done."); + }else{ + error error( + constants::error::HTTP_RESPONSE_ERROR, + std::format("HTTP Error: {}", response.code) + ); + log::error(error); + return error; + } + } + + dir_pairs.emplace_back(ss_pair(tmp_zip, package_dir + "/")); + + p.is_installed = true; + p.install_path = package_dir; + + /* Extract all the downloaded packages to their appropriate directory location. */ + for(const auto& p : dir_pairs) + utils::extract_zip(p.first.c_str(), p.second.c_str()); + + /* Update the cache data with information from */ + log::info_n("Updating local asset data..."); + cache::update_package_info(p_found); + log::println("done."); + // }) + // ); + } + + return error(); + } + + + error remove( + const config::context& config, + const string_list& package_titles, + const package::params& params + ){ + using namespace rapidjson; + using namespace std::filesystem; + + /* Find the packages to remove if they're is_installed and show them to the user */ + result_t result = cache::get_package_info_by_title(package_titles); + std::vector p_cache = result.unwrap_unsafe(); + if(p_cache.empty()){ + error error( + constants::error::NOT_FOUND, + "\nCould not find any packages to remove." + ); + log::error(error); + return error; + } + + /* Count number packages in cache flagged as is_installed. If there are none, then there's nothing to do. */ + size_t p_count = 0; + std::for_each(p_cache.begin(), p_cache.end(), [&p_count](const package::info& p){ + p_count += (p.is_installed) ? 1 : 0; + }); + + if(p_count == 0){ + error error( + constants::error::NOT_FOUND, + "\nNo packages to remove." + ); + log::error(error); + return error; + } + + log::println("Packages to remove:"); + for(const auto& p : p_cache) + if(p.is_installed) + log::print(" {} ", p.title); + log::println(""); + + if(!params.skip_prompt){ + if(!utils::prompt_user_yn("Do you want to remove these packages? (y/n)")) + return error(); + } + + log::info_n("Removing packages..."); + for(auto& p : p_cache){ + const std::filesystem::path path{config.packages_dir}; + std::filesystem::remove_all(config.packages_dir + "/" + p.title); + if(config.verbose > 0){ + log::debug("package directory: {}", path.string()); + } + + /* Traverse the package directory */ + // for(const auto& entry : recursive_directory_iterator(path)){ + // if(entry.is_directory()){ + // } + // else if(entry.is_regular_file()){ + // std::string filename = entry.path().filename().string(); + // std::string pkg_path = entry.path().lexically_normal().string(); + + // // pkg_path = utils::replace_all(pkg_path, " ", "\\ "); + // if(filename == "package.json"){ + // std::string contents = utils::readfile(pkg_path); + // Document doc; + // if(config.verbose > 0){ + // log::debug("package path: {}", pkg_path); + // log::debug("contents: \n{}", contents); + // } + // doc.Parse(contents.c_str()); + // if(doc.IsNull()){ + // log::println(""); + // log::error("Could not remove packages. Parsing 'package.json' returned NULL."); + // return; + // } + // } + // } + // } + p.is_installed = false; + } + log::println("Done."); + log::info_n("Updating local asset data..."); + cache::update_package_info(p_cache); + log::println("done."); + + return error(); + } + + + /** + Removes all local packages. + */ + error remove_all( + const config::context& config, + const package::params& params + ){ + /* Get the list of all packages to remove then remove */ + result_t r_installed = cache::get_installed_packages(); + package::info_list p_installed = r_installed.unwrap_unsafe(); + result_t r_titles = get_package_titles(p_installed); + package::title_list p_titles = r_titles.unwrap_unsafe(); + return remove(config, p_titles, params); + } + + + error update( + const config::context& config, + const package::title_list& package_titles, + const package::params& params + ){ + using namespace rapidjson; + + /* If no package titles provided, update everything and then exit */ + rest_api::context rest_api_params = rest_api::make_from_config(config); + if(package_titles.empty()){ + std::string url{constants::HostUrl}; + url += rest_api::endpoints::GET_AssetId; + Document doc = rest_api::get_assets_list(url, rest_api_params); + if(doc.IsNull()){ + constexpr const char *message = "Could not get response from server. Aborting."; + log::error(message); + return error(constants::error::HOST_UNREACHABLE, message); + } + return error(); + } + + /* Fetch remote asset data and compare to see if there are package updates */ + std::vector p_updates = {}; + result_t r_cache = cache::get_package_info_by_title(package_titles); + package::info_list p_cache = r_cache.unwrap_unsafe(); + + log::println("Packages to update: "); + for(const auto& p_title : p_updates) + log::print(" {} ", p_title); + log::println(""); + + /* Check version information to see if packages need updates */ + for(const auto& p : p_cache){ + std::string url{constants::HostUrl}; + url += rest_api::endpoints::GET_AssetId; + Document doc = rest_api::get_asset(url, p.asset_id); + std::string remote_version = doc["version"].GetString(); + if(p.version != remote_version){ + p_updates.emplace_back(p.title); + } + } + + if(!params.skip_prompt){ + if(!utils::prompt_user_yn("Do you want to update the following packages? (y/n)")) + return error(); + } + + { + error error; + error = remove(config, p_updates); + error = install(config, p_updates, params); + } + return error(); + } + + + error search( + const config::context& config, + const package::title_list &package_titles, + const package::params& params + ){ + result_t r_cache = cache::get_package_info_by_title(package_titles); + std::vector p_cache = r_cache.unwrap_unsafe(); + + if(!p_cache.empty() && !config.enable_sync){ + print_list(p_cache); + return error(); + } + + rest_api::context rest_api_params = rest_api::make_from_config(config); + for(const auto& p_title : package_titles){ + using namespace rapidjson; + + rest_api_params.filter = http::url_escape(p_title); + rest_api_params.verbose = config.verbose; + rest_api_params.godot_version = config.godot_version; + rest_api_params.max_results = 200; + + std::string request_url{constants::HostUrl}; + request_url += rest_api::endpoints::GET_Asset; + Document doc = rest_api::get_assets_list(request_url, rest_api_params); + if(doc.IsNull()){ + error error( + constants::error::HOST_UNREACHABLE, + "Could not fetch metadata." + ); + log::error(error); + return error; + } + + log::info("{} package(s) found...", doc["total_items"].GetInt()); + print_list(doc); + } + return error(); + } + + + error list( + const config::context& config, + const args_t& args, + const opts_t& opts + ){ + using namespace rapidjson; + using namespace std::filesystem; + + if(opts.empty() || opts.contains("packages")){ + result_t r_installed = cache::get_installed_packages(); + info_list p_installed = r_installed.unwrap_unsafe(); + if(!p_installed.empty()){ + log::println("Installed packages: "); + print_list(p_installed); + } + } + else if(opts.contains("remote")){ + remote::print_repositories(config); + } + else{ + error error( + constants::error::UNKNOWN_COMMAND, + "Unrecognized subcommand. Try either 'packages' or 'remote' instead." + + ); + log::error(error); + } + return error(); + } + + + error export_to(const string_list& paths){ + /* Get all installed package information for export */ + result_t r_installed = cache::get_installed_packages(); + info_list p_installed = r_installed.unwrap_unsafe(); + + result_t r_titles = get_package_titles(p_installed); + title_list p_titles = r_titles.unwrap_unsafe(); + + /* Build string of contents with one package title per line */ + string output{}; + std::for_each(p_titles.begin(), p_titles.end(), [&output](const string& p){ + output += p + "\n"; + }); + + /* Write contents of installed packages in reusable format */ + for(const auto& path : paths ){ + std::ofstream of(path); + if(std::filesystem::exists(path)){ + constexpr const char *message = "File or directory exists!"; + log::error(message); + of.close(); + return error(constants::error::FILE_EXISTS, message); + } + log::println("writing contents to file"); + of << output; + of.close(); + } + + return error(); + } + + + error link( + const config::context& config, + const title_list& package_titles, + const opts_t& opts + ){ + using namespace std::filesystem; + + path_list paths = {}; + if(opts.contains("path")){ + paths = opts.at("path"); + } + + if(paths.empty()){ + error error( + constants::error::PATH_NOT_DEFINED, + "No path set. Use '--path' option to set a path." + ); + log::error(error); + return error; + } + result_t r_cache = cache::get_package_info_by_title(package_titles); + info_list p_found = {}; + info_list p_cache = r_cache.unwrap_unsafe(); + if(p_cache.empty()){ + error error( + constants::error::NOT_FOUND, + "Could not find any packages to link." + ); + log::error(error); + return error; + } + + for(const auto& p_title : package_titles){ + auto found = std::find_if(p_cache.begin(), p_cache.end(), [&p_title](const package::info& p){ return p.title == p_title; }); + if(found != p_cache.end()){ + p_found.emplace_back(*found); + } + } + + if(p_found.empty()){ + error error( + constants::error::NO_PACKAGE_FOUND, + "No packages found to link." + ); + log::error(error); + return error; + } + + /* Get the storage paths for all packages to create symlinks */ + const path package_dir{config.packages_dir}; + for(const auto& p : p_found){ + for(const auto& path : paths){ + log::info_n("Creating symlink for \"{}\" package to '{}'...", p.title, path + "/" + p.title); + // std::filesystem::path target{config.packages_dir + "/" + p.title}; + std::filesystem::path target = {current_path().string() + "/" + config.packages_dir + "/" + p.title}; + std::filesystem::path symlink_path{path + "/" + p.title}; + if(!std::filesystem::exists(symlink_path.string())) + std::filesystem::create_directories(path + "/"); + std::error_code ec; + std::filesystem::create_directory_symlink(target, symlink_path, ec); + if(ec){ + error error( + constants::error::STD_ERROR, + std::format("Could not create symlink: {}", ec.message()) + ); + log::error(error); + } + log::println("Done."); + } + } + return error(); + } + + + error clone( + const config::context& config, + const title_list& package_titles, + const opts_t& paths + ){ + using namespace std::filesystem; + + if(paths.empty()){ + error error( + constants::error::PATH_NOT_DEFINED, + "No path set. Use '--path' option to set a path." + ); + log::error(error); + return error; + } + + result_t r_cache = cache::get_package_info_by_title(package_titles); + package::info_list p_found = {}; + package::info_list p_cache = r_cache.unwrap_unsafe(); + if(p_cache.empty()){ + error error( + constants::error::NO_PACKAGE_FOUND, + "Could not find any packages to clone." + ); + log::error(error); + return error; + } + + for(const auto& p_title : package_titles){ + auto found = std::find_if(p_cache.begin(), p_cache.end(), [&p_title](const package::info& p){ return p.title == p_title; }); + if(found != p_cache.end()){ + p_found.emplace_back(*found); + } + } + + if(p_found.empty()){ + error error( + constants::error::NO_PACKAGE_FOUND, + "No packages found to clone." + ); + log::error(error); + return error; + } + + /* Get the storage paths for all packages to create clones */ + const path package_dir{config.packages_dir}; + for(const auto& p : p_found){ + for(const auto& path_list : paths){ + for(const auto& path : path_list.second){ + log::info("Cloning \"{}\" package to {}", p.title, path + "/" + p.title); + std::filesystem::path from{config.packages_dir + "/" + p.title}; + std::filesystem::path to{path + "/" + p.title}; + if(!std::filesystem::exists(to.string())) + std::filesystem::create_directories(to); + + /* TODO: Add an option to force overwriting (i.e. --overwrite) */ + std::filesystem::copy(from, to, copy_options::update_existing | copy_options::recursive); + } + } + } + return error(); + } + + + void print_list(const info_list& packages){ + for(const auto& p : packages){ + log::println("{}/{}/{} {} id={}\n\tGodot {}, {}, {}, Last Modified: {}", + p.support_level, + p.author, + p.title, + p.version, + p.asset_id, + p.godot_version, + p.cost, + p.category, + p.modify_date + ); + } + } + + + void print_list(const rapidjson::Document& json){ + for(const auto& o : json["result"].GetArray()){ + log::println("{}/{}/{} {} id={}\n\tGodot {}, {}, {}, Last Modified: {}", + o["support_level"] .GetString(), + o["author"] .GetString(), + o["title"] .GetString(), + o["version_string"] .GetString(), + o["asset_id"] .GetString(), + o["godot_version"] .GetString(), + o["cost"] .GetString(), + o["category"] .GetString(), + o["modify_date"] .GetString() + ); + } + } + + + result_t get_package_info(const title_list& package_titles){ + return cache::get_package_info_by_title(package_titles); + } + + + result_t get_package_titles(const info_list &packages){ + title_list package_titles; + std::for_each(packages.begin(), packages.end(), [&package_titles](const package::info& p){ + package_titles.emplace_back(p.title); + }); + return result_t(package_titles, error()); + } + + + void clean_temporary( + const config::context& config, + const title_list& package_titles + ){ + if(package_titles.empty()){ + log::info("Cleaned all temporary files."); + std::filesystem::remove_all(config.tmp_dir); + } + /* Find the path of each packages is_installed then delete temporaries */ + log::info_n("Cleaning temporary files..."); + for(const auto& p_title : package_titles){ + string tmp_zip = config.tmp_dir + "/" + p_title + ".zip"; + if(config.verbose > 0) + log::info("Removed '{}'", tmp_zip); + std::filesystem::remove_all(tmp_zip); + } + log::println("Done."); + } + + + result_t synchronize_database( + const config::context& config, + const title_list& package_titles + ){ + using namespace rapidjson; + + rest_api::context rest_api_params = rest_api::make_from_config(config); + rest_api_params.page = 0; + int page = 0; + int page_length = 0; + // int total_pages = 0; + int total_items = 0; + int items_left = 0; + + log::info("Sychronizing database..."); + do{ + /* Make the GET request to get page data and store it in the local + package database. Also, check to see if we need to keep going. */ + std::string url{constants::HostUrl}; + url += rest_api::endpoints::GET_Asset; + Document doc = rest_api::get_assets_list(url, rest_api_params); + rest_api_params.page += 1; + + if(doc.IsNull()){ + error error( + constants::error::EMPTY_RESPONSE, + "Could not get response from server. Aborting." + ); + log::error(error); + return result_t(info_list(), error); + } + + /* Need to know how many pages left to get and how many we get per + request. */ + page = doc["page"].GetInt(); + page_length = doc["page_length"].GetInt(); + // total_pages = doc["pages"].GetInt(); + total_items = doc["total_items"].GetInt(); + items_left = total_items - (page + 1) * page_length; + + // log::info("page: {}, page length: {}, total pages: {}, total items: {}, items left: {}", page, page_length, total_pages, total_items, items_left); + + if(page == 0){ + error error; + error = cache::drop_package_database(); + error = cache::create_package_database(); + } + + info_list packages; + for(const auto& o : doc["result"].GetArray()){ + // log::println("======================="); + info p{ + .asset_id = std::stoul(o["asset_id"].GetString()), + .title = o["title"].GetString(), + .author = o["author"].GetString(), + .author_id = std::stoul(o["author_id"].GetString()), + .version = o["version"].GetString(), + .godot_version = o["godot_version"].GetString(), + .cost = o["cost"].GetString(), + .modify_date = o["modify_date"].GetString(), + .category = o["category"].GetString(), + .remote_source = url + }; + packages.emplace_back(p); + } + error error = cache::insert_package_info(packages); + if (error.has_occurred()){ + log::error(error); + /* FIXME: Should this stop here or keep going? */ + } + /* Make the same request again to get the rest of the needed data + using the same request, but with a different page, then update + variables as needed. */ + + + } while(items_left > 0); + + log::println("Done."); + + return cache::get_package_info_by_title(package_titles); + } + + + result_t resolve_dependencies( + const config::context& config, + const title_list& package_titles + ){ + result_t r_cache = cache::get_package_info_by_title(package_titles); + info_list p_cache = r_cache.unwrap_unsafe(); + info_list p_deps = {}; + + /* Build an graph of every thing to check then install in order */ + for(const auto& p : p_cache){ + if(p.dependencies.empty()) + continue; + + /* Check if dependency has a dependency. If so, resolve those first. */ + for(const auto& d : p.dependencies){ + result_t r_temp = resolve_dependencies(config, {d.title}); + info_list temp = r_temp.unwrap_unsafe(); + utils::move_if_not(temp, p_deps, [](const info& p){ return true; }); + } + } + + return result_t(p_deps, error()); + } +} \ No newline at end of file diff --git a/src/package_manager.cpp b/src/package_manager.cpp index ad41f66..e63edc9 100644 --- a/src/package_manager.cpp +++ b/src/package_manager.cpp @@ -1,12 +1,16 @@ #include "package_manager.hpp" +#include "error.hpp" +#include "package.hpp" #include "utils.hpp" #include "rest_api.hpp" +#include "remote.hpp" #include "config.hpp" #include "constants.hpp" #include "log.hpp" #include "http.hpp" #include "cache.hpp" +#include "types.hpp" #include #include @@ -31,620 +35,61 @@ */ namespace gdpm::package_manager{ - std::vector repo_sources; + remote::repository_map repo_sources; CURL *curl; CURLcode res; config::context config; - rest_api::context params; - command_e command; - std::vector packages; - std::vector opts; + rest_api::context api_params; + action_e action; + string_list packages; + // opts_t opts; bool skip_prompt = false; bool clean_tmp_dir = false; int priority = -1; - int initialize(int argc, char **argv){ + result_t initialize(int argc, char **argv){ // curl_global_init(CURL_GLOBAL_ALL); curl = curl_easy_init(); config = config::make_context(); - params = rest_api::make_context(); - command = none; + api_params = rest_api::make_context(); + action = action_e::none; /* Check for config and create if not exists */ if(!std::filesystem::exists(config.path)){ config::save(config.path, config); } - config::load(config.path, config); - config.enable_sync = true; - std::string json = to_json(config); + error error = config::load(config.path, config); + if(error.has_occurred()){ + log::error(error); + } /* Create the local databases if it doesn't exist already */ - cache::create_package_database(); + error = cache::create_package_database(); + if(error.has_occurred()){ + log::error(error); + } /* Run the rest of the program then exit */ cxxargs args = _parse_arguments(argc, argv); - _handle_arguments(args); - return 0; + return _handle_arguments(args); } - int execute(){ - run_command(command, packages, opts); + int execute(const args_t& args, const opts_t& opts){ + run_command(action, packages, opts); if(clean_tmp_dir) - clean_temporary(packages); + package::clean_temporary(config, packages); return 0; } void finalize(){ curl_easy_cleanup(curl); - config::save(config.path, config); - // curl_global_cleanup(); - } + error error = config::save(config.path, config); + if(error()){ - - error install_packages(const std::vector& package_titles, bool skip_prompt){ - using namespace rapidjson; - params.verbose = config.verbose; - - /* TODO: Need a way to use remote sources from config until none left */ - - /* Check if the package data is already stored in cache. If it is, there - is no need to do a lookup to synchronize the local database since we - have all the information we need to fetch the asset data. */ - std::vector p_found = {}; - std::vector p_cache = cache::get_package_info_by_title(package_titles); - - /* Synchronize database information and then try to get data again from - cache if possible. */ - if(config.enable_sync){ - if(p_cache.empty()){ - p_cache = synchronize_database(package_titles); - } } - - for(const auto& p_title : package_titles){ - auto found = std::find_if(p_cache.begin(), p_cache.end(), [&p_title](const package_info& p){ return p.title == p_title; }); - if(found != p_cache.end()){ - p_found.emplace_back(*found); - } - } - - /* Found nothing to install so there's nothing to do at this point. */ - if(p_found.empty()){ - constexpr const char *message = "No packages found to install."; - log::error(message); - return error(error_codes::NOT_FOUND, message); - } - - log::println("Packages to install: "); - for(const auto& p : p_found){ - std::string output((p.is_installed) ? p.title + " (reinstall)" : p.title); - log::print(" {} ", (p.is_installed) ? p.title + " (reinstall)" : p.title); - } - log::println(""); - - if(!skip_prompt){ - if(!utils::prompt_user_yn("Do you want to install these packages? (y/n)")) - return error(); - } - - using ss_pair = std::pair; - std::vector dir_pairs; - std::vector> tasks; - for(auto& p : p_found){ // TODO: Execute each in parallel using coroutines?? - - // tasks.emplace_back( - // std::async(std::launch::async, [&p, &p_found, &error, &dir_pairs](){ - - log::info("Fetching asset data for \"{}\"...", p.title); - - /* TODO: Try fetching the data with all available remote sources until retrieved */ - for(const auto& remote_url : config.remote_sources){ - std::string url{remote_url}, package_dir, tmp_dir, tmp_zip; - - url += rest_api::endpoints::GET_AssetId; - - /* Retrieve necessary asset data if it was found already in cache */ - // log::debug("download_url: {}\ncategory: {}\ndescription: {}\nsupport_level: {}", p.download_url, p.category, p.description, p.support_level); - Document doc; - bool is_valid = p.download_url.empty() || p.category.empty() || p.description.empty() || p.support_level.empty(); - if(is_valid){ - params.verbose = config.verbose; - doc = rest_api::get_asset(url, p.asset_id, params); - if(doc.HasParseError() || doc.IsNull()){ - constexpr const char *message = "\nError parsing HTTP response."; - log::error(message); - return error(doc.GetParseError(), message); - } - p.category = doc["category"].GetString(); - p.description = doc["description"].GetString(); - p.support_level = doc["support_level"].GetString(); - p.download_url = doc["download_url"].GetString(); - p.download_hash = doc["download_hash"].GetString(); - } - else{ - /* Package for in cache so no remote request. Still need to populate RapidJson::Document to write to package.json. - NOTE: This may not be necessary at all! - */ - // doc["asset_id"].SetUint64(p.asset_id - // doc["type"].SetString(p.type, doc.GetAllocator()); - // doc["title"].SetString(p.title, doc.GetAllocator()); - // doc["author"].SetString(p.author, doc.GetAllocator()); - // doc["author_id"].SetUint64(p.author_id); - // doc["version"].SetString(p.version, doc.GetAllocator()); - // doc["category"].SetString(p.category, doc.GetAllocator()); - // doc["godot_version"].SetString(p.godot_version, doc.GetAllocator()); - // doc["cost"].SetString(p.cost, doc.GetAllocator()); - // doc["description"].SetString(p.description, doc.GetAllocator()); - // doc["support_level"].SetString(p.support_level, doc.GetAllocator()); - // doc["download_url"].SetString(p.download_url, doc.GetAllocator()); - // doc["download_hash"].SetString(p.download_hash, doc.GetAllocator; - } - - /* Set directory and temp paths for storage */ - package_dir = config.packages_dir + "/" + p.title; - tmp_dir = config.tmp_dir + "/" + p.title; - tmp_zip = tmp_dir + ".zip"; - - /* Make directories for packages if they don't exist to keep everything organized */ - if(!std::filesystem::exists(config.tmp_dir)) - std::filesystem::create_directories(config.tmp_dir); - if(!std::filesystem::exists(config.packages_dir)) - std::filesystem::create_directories(config.packages_dir); - - /* Dump asset information for lookup into JSON in package directory */ - if(!std::filesystem::exists(package_dir)) - std::filesystem::create_directory(package_dir); - - std::ofstream ofs(package_dir + "/asset.json"); - OStreamWrapper osw(ofs); - PrettyWriter writer(osw); - doc.Accept(writer); - - /* Check if we already have a stored temporary file before attempting to download */ - if(std::filesystem::exists(tmp_zip) && std::filesystem::is_regular_file(tmp_zip)){ - log::println("Found cached package. Skipping download.", p.title); - } - else{ - /* Download all the package files and place them in tmp directory. */ - log::info_n("Downloading \"{}\"...", p.title); - std::string download_url = p.download_url;// doc["download_url"].GetString(); - std::string title = p.title;// doc["title"].GetString(); - http::response response = http::download_file(download_url, tmp_zip); - if(response.code == 200){ - log::println("Done."); - }else{ - constexpr const char *message = "Error in HTTP response."; - log::error(message); - return error(response.code, message); - } - } - - dir_pairs.emplace_back(ss_pair(tmp_zip, package_dir + "/")); - - p.is_installed = true; - p.install_path = package_dir; - } - - /* Extract all the downloaded packages to their appropriate directory location. */ - for(const auto& p : dir_pairs) - utils::extract_zip(p.first.c_str(), p.second.c_str()); - - /* Update the cache data with information from */ - log::info_n("Updating local asset data..."); - cache::update_package_info(p_found); - log::println("done."); - // }) - // ); - } - - return error(); - } - - - error remove_packages(const std::vector& package_titles, bool skip_prompt){ - using namespace rapidjson; - using namespace std::filesystem; - - /* Find the packages to remove if they're is_installed and show them to the user */ - std::vector p_cache = cache::get_package_info_by_title(package_titles); - if(p_cache.empty()){ - constexpr const char *message("\nCould not find any packages to remove."); - log::error(message); - return error(error_codes::NOT_FOUND, message, true); - } - - /* Count number packages in cache flagged as is_installed. If there are none, then there's nothing to do. */ - size_t p_count = 0; - std::for_each(p_cache.begin(), p_cache.end(), [&p_count](const package_info& p){ - p_count += (p.is_installed) ? 1 : 0; - }); - - if(p_count == 0){ - constexpr const char *message("\nNo packages to remove."); - log::error(message); - return error(error_codes::NOT_FOUND, message, true); - } - - log::println("Packages to remove:"); - for(const auto& p : p_cache) - if(p.is_installed) - log::print(" {} ", p.title); - log::println(""); - - if(!skip_prompt){ - if(!utils::prompt_user_yn("Do you want to remove these packages? (y/n)")) - return error(); - } - - log::info_n("Removing packages..."); - for(auto& p : p_cache){ - const path path{config.packages_dir}; - std::filesystem::remove_all(config.packages_dir + "/" + p.title); - if(config.verbose > 0){ - log::debug("package directory: {}", path.string()); - } - - /* Traverse the package directory */ - // for(const auto& entry : recursive_directory_iterator(path)){ - // if(entry.is_directory()){ - // } - // else if(entry.is_regular_file()){ - // std::string filename = entry.path().filename().string(); - // std::string pkg_path = entry.path().lexically_normal().string(); - - // // pkg_path = utils::replace_all(pkg_path, " ", "\\ "); - // if(filename == "package.json"){ - // std::string contents = utils::readfile(pkg_path); - // Document doc; - // if(config.verbose > 0){ - // log::debug("package path: {}", pkg_path); - // log::debug("contents: \n{}", contents); - // } - // doc.Parse(contents.c_str()); - // if(doc.IsNull()){ - // log::println(""); - // log::error("Could not remove packages. Parsing 'package.json' returned NULL."); - // return; - // } - // } - // } - // } - p.is_installed = false; - } - log::println("Done."); - log::info_n("Updating local asset data..."); - cache::update_package_info(p_cache); - log::println("done."); - - return error(); - } - - - /** - Removes all local packages. - */ - error remove_all_packages(){ - /* Get the list of all packages to remove then remove */ - std::vector p_installed = cache::get_installed_packages(); - std::vector p_titles = get_package_titles(p_installed); - return remove_packages(p_titles); - } - - - error update_packages(const std::vector& package_titles, bool skip_prompt){ - using namespace rapidjson; - - /* If no package titles provided, update everything and then exit */ - if(package_titles.empty()){ - std::string url{constants::HostUrl}; - url += rest_api::endpoints::GET_AssetId; - Document doc = rest_api::get_assets_list(url, params); - if(doc.IsNull()){ - constexpr const char *message = "Could not get response from server. Aborting."; - log::error(message); - return error(error_codes::HOST_UNREACHABLE, message); - } - return error(); - } - - /* Fetch remote asset data and compare to see if there are package updates */ - std::vector p_updates = {}; - std::vector p_cache = cache::get_package_info_by_title(package_titles); - - log::println("Packages to update: "); - for(const auto& p_title : p_updates) - log::print(" {} ", p_title); - log::println(""); - - /* Check version information to see if packages need updates */ - for(const auto& p : p_cache){ - std::string url{constants::HostUrl}; - url += rest_api::endpoints::GET_AssetId; - Document doc = rest_api::get_asset(url, p.asset_id); - std::string remote_version = doc["version"].GetString(); - if(p.version != remote_version){ - p_updates.emplace_back(p.title); - } - } - - if(!skip_prompt){ - if(!utils::prompt_user_yn("Do you want to update the following packages? (y/n)")) - return error(); - } - - remove_packages(p_updates); - install_packages(p_updates); - return error(); - } - - - error search_for_packages(const std::vector &package_titles, bool skip_prompt){ - std::vector p_cache = cache::get_package_info_by_title(package_titles); - - if(!p_cache.empty() && !config.enable_sync){ - print_package_list(p_cache); - return error(); - } - for(const auto& p_title : package_titles){ - using namespace rapidjson; - - params.filter = curl_easy_escape(curl, p_title.c_str(), p_title.size()); - params.verbose = config.verbose; - params.godot_version = config.godot_version; - params.max_results = 200; - - std::string request_url{constants::HostUrl}; - request_url += rest_api::endpoints::GET_Asset; - Document doc = rest_api::get_assets_list(request_url, params); - if(doc.IsNull()){ - constexpr const char *message = "Could not fetch metadata."; - log::error(message); - return error(error_codes::HOST_UNREACHABLE, message); - } - - log::info("{} package(s) found...", doc["total_items"].GetInt()); - print_package_list(doc); - } - return error(); - } - - - error export_packages(const std::vector& paths){ - - /* Get all installed package information for export */ - std::vector p_installed = cache::get_installed_packages(); - std::vector p_titles = get_package_titles(p_installed); - - /* Build string of contents with one package title per line */ - std::string output{}; - std::for_each(p_titles.begin(), p_titles.end(), [&output](const std::string& p){ - output += p + "\n"; - }); - - /* Write contents of installed packages in reusable format */ - for(const auto& path : paths ){ - std::ofstream of(path); - if(std::filesystem::exists(path)){ - constexpr const char *message = "File or directory exists!"; - log::error(message); - of.close(); - return error(error_codes::FILE_EXISTS, message); - } - log::println("writing contents to file"); - of << output; - of.close(); - } - - return error(); - } - - - std::vector list_information(const std::vector& opts, bool print_list){ - using namespace rapidjson; - using namespace std::filesystem; - - if(opts.empty() || opts[0] == "packages"){ - const path path{config.packages_dir}; - std::vector p_installed = cache::get_installed_packages(); - if(p_installed.empty()) - return get_package_titles(p_installed); - log::println("Installed packages:"); - if(print_list) - print_package_list(p_installed); - return get_package_titles(p_installed); - } - else if(opts[0] == "remote"){ - if(print_list) - print_remote_sources(); - return std::vector(); - } - else{ - log::error("Unrecognized subcommand. Use either 'packages' or 'remote-sources' instead."); - return std::vector(); - } - } - - - void clean_temporary(const std::vector& package_titles){ - if(package_titles.empty()){ - log::info("Cleaned all temporary files."); - std::filesystem::remove_all(config.tmp_dir); - } - /* Find the path of each packages is_installed then delete temporaries */ - log::info_n("Cleaning temporary files..."); - for(const auto& p_title : package_titles){ - std::string tmp_zip = config.tmp_dir + "/" + p_title + ".zip"; - if(config.verbose > 0) - log::info("Removed '{}'", tmp_zip); - std::filesystem::remove_all(tmp_zip); - } - log::println("Done."); - } - - - void link_packages(const std::vector& package_titles, const std::vector& paths){ - using namespace std::filesystem; - - if(paths.empty()){ - log::error("No path set. Use '--path' option to set a path."); - return; - } - - std::vector p_found = {}; - std::vector p_cache = cache::get_package_info_by_title(package_titles); - if(p_cache.empty()){ - log::error("Could not find any packages to link."); - return; - } - - for(const auto& p_title : package_titles){ - auto found = std::find_if(p_cache.begin(), p_cache.end(), [&p_title](const package_info& p){ return p.title == p_title; }); - if(found != p_cache.end()){ - p_found.emplace_back(*found); - } - } - - if(p_found.empty()){ - log::error("No packages found to link."); - return; - } - - /* Get the storage paths for all packages to create symlinks */ - const path package_dir{config.packages_dir}; - for(const auto& p : p_found){ - for(const auto& path : paths){ - log::info_n("Creating symlink for \"{}\" package to '{}'...", p.title, path + "/" + p.title); - // std::filesystem::path target{config.packages_dir + "/" + p.title}; - std::filesystem::path target = {current_path().string() + "/" + config.packages_dir + "/" + p.title}; - std::filesystem::path symlink_path{path + "/" + p.title}; - if(!std::filesystem::exists(symlink_path.string())) - std::filesystem::create_directories(path + "/"); - std::error_code ec; - std::filesystem::create_directory_symlink(target, symlink_path, ec); - if(ec){ - log::error("Could not create symlink: {}", ec.message()); - } - log::println("Done."); - } - } - } - - - void clone_packages(const std::vector& package_titles, const std::vector& paths){ - using namespace std::filesystem; - - if(paths.empty()){ - log::error("No path set. Use '--path' option to set a path."); - return; - } - - std::vector p_found = {}; - std::vector p_cache = cache::get_package_info_by_title(package_titles); - if(p_cache.empty()){ - log::error("Could not find any packages to clone."); - return; - } - - for(const auto& p_title : package_titles){ - auto found = std::find_if(p_cache.begin(), p_cache.end(), [&p_title](const package_info& p){ return p.title == p_title; }); - if(found != p_cache.end()){ - p_found.emplace_back(*found); - } - } - - if(p_found.empty()){ - log::error("No packages to clone."); - return; - } - - /* Get the storage paths for all packages to create clones */ - const path package_dir{config.packages_dir}; - for(const auto& p : p_found){ - for(const auto& path : paths){ - log::info("Cloning \"{}\" package to {}", p.title, path + "/" + p.title); - std::filesystem::path from{config.packages_dir + "/" + p.title}; - std::filesystem::path to{path + "/" + p.title}; - if(!std::filesystem::exists(to.string())) - std::filesystem::create_directories(to); - - /* TODO: Add an option to force overwriting (i.e. --overwrite) */ - std::filesystem::copy(from, to, copy_options::update_existing | copy_options::recursive); - } - } - } - - - error _handle_remote(const std::vector& args, const std::vector&){ - /* Check if enough arguments are supplied */ - size_t argc = args.size(); - if (argc < 0){ - constexpr const char *message = "No arguments supplied. Aborting."; - log::error(message); - return error(0, message); - } - - /* Check which subcommand is supplied */ - std::string sub = args.front(); - std::vector argv(args.begin()+1, args.end()); - if(sub == "add") remote_add_repository(argv); - else if (sub == "remove") remote_remove_respository(argv); - else if (sub == "list") print_remote_sources(); - else{ - constexpr const char *message = "Unknown sub-command. Try 'gdpm help remote' for options."; - log::error(message); - return error(error_codes::UNKNOWN, message); - } - return error(); - } - - - void remote_add_repository(const std::vector& repos){ - std::for_each(repos.begin(), repos.end(), [](const std::string& repo){ - config.remote_sources.insert(repo); - }); - } - - - void remote_remove_respository(const std::vector& repos){ - auto& s = config.remote_sources; - std::for_each(repos.end(), repos.begin(), [](const std::string& repo){ - s.erase(repo); - }); - } - - - /* TODO: Need to finish implementation...will do that when it's needed. */ - void remote_remove_respository(size_t index){ - auto& s = config.remote_sources; - // std::erase(s, index); - } - - - void remote_move_respository(int old_position, int new_position){ - - } - - - std::vector resolve_dependencies(const std::vector& package_titles){ - std::vector p_deps = {}; - std::vector p_cache = cache::get_package_info_by_title(package_titles); - - /* Build an graph of every thing to check then install in order */ - for(const auto& p : p_cache){ - if(p.dependencies.empty()) - continue; - - /* Check if dependency has a dependency. If so, resolve those first. */ - for(const auto& d : p.dependencies){ - auto temp = resolve_dependencies({d.title}); - utils::move_if_not(temp, p_deps, [](const std::string& p){ return true; }); - } - } - - return p_deps; } @@ -657,35 +102,35 @@ namespace gdpm::package_manager{ options.allow_unrecognised_options(); options.custom_help("[COMMAND] [OPTIONS...]"); options.add_options("Command") - ("command", "Specify the input parameters", cxxopts::value>()) - ("install", "Install package or packages.", cxxopts::value>()->implicit_value(""), "") - ("remove", "Remove a package or packages.", cxxopts::value>()->implicit_value(""), "") - ("update", "Update a package or packages. This will update all packages if no argument is provided.", cxxopts::value>()->implicit_value(""), "") - ("search", "Search for a package or packages.", cxxopts::value>(), "") - ("export", "Export list of packages", cxxopts::value()->default_value("./gdpm-packages.txt")) + ("command", "Specify the input parameters", cxxopts::value()) + ("install", "Install package or packages.", cxxopts::value()->implicit_value(""), "") + ("remove", "Remove a package or packages.", cxxopts::value()->implicit_value(""), "") + ("update", "Update a package or packages. This will update all packages if no argument is provided.", cxxopts::value()->implicit_value(""), "") + ("search", "Search for a package or packages.", cxxopts::value(), "") + ("export", "Export list of packages", cxxopts::value()->default_value("./gdpm-packages.txt")) ("list", "Show list of installed packages.") - ("link", "Create a symlink (or shortcut) to target directory. Must be used with the `--path` argument.", cxxopts::value>(), "") - ("clone", "Clone packages into target directory. Must be used with the `--path` argument.", cxxopts::value>(), "") + ("link", "Create a symlink (or shortcut) to target directory. Must be used with the `--path` argument.", cxxopts::value(), "") + ("clone", "Clone packages into target directory. Must be used with the `--path` argument.", cxxopts::value(), "") ("clean", "Clean temporary downloaded files.") ("fetch", "Fetch asset data from remote sources.") - ("remote", "Set a source repository.", cxxopts::value()->default_value(constants::AssetRepo), "") + ("remote", "Set a source repository.", cxxopts::value()->default_value(constants::AssetRepo), "") ("h,help", "Print this message and exit.") ("version", "Show the current version and exit.") ; options.parse_positional({"command"}); options.positional_help(""); - options.add_options("Options") - ("c,config", "Set the config file path.", cxxopts::value()) - ("f,file", "Read file to install or remove packages.", cxxopts::value(), "") - ("path", "Specify a path to use with a command", cxxopts::value>()) - ("type", "Set package type (any|addon|project).", cxxopts::value()) - ("sort", "Sort packages in order (rating|cost|name|updated).", cxxopts::value()) + options.add_options("Other") + ("c,config", "Set the config file path.", cxxopts::value()) + ("f,file", "Read file to install or remove packages.", cxxopts::value(), "") + ("path", "Specify a path to use with a command", cxxopts::value()) + ("type", "Set package type (any|addon|project).", cxxopts::value()) + ("sort", "Sort packages in order (rating|cost|name|updated).", cxxopts::value()) ("support", "Set the support level for API (all|official|community|testing).") ("max-results", "Set the max results to return from search.", cxxopts::value()->default_value("500"), "") - ("godot-version", "Set the Godot version to include in request.", cxxopts::value()) + ("godot-version", "Set the Godot version to include in request.", cxxopts::value()) ("set-priority", "Set the priority for remote source. Lower values are used first (0...100).", cxxopts::value()) - ("set-packages-directory", "Set the local package storage location.", cxxopts::value()) - ("set-temporary-directory", "Set the local temporary storage location.", cxxopts::value()) + ("set-packages-directory", "Set the local package storage location.", cxxopts::value()) + ("set-temporary-directory", "Set the local temporary storage location.", cxxopts::value()) ("timeout", "Set the amount of time to wait for a response.", cxxopts::value()) ("no-sync", "Disable synchronizing with remote.", cxxopts::value()->implicit_value("true")->default_value("false")) ("y,no-prompt", "Bypass yes/no prompt for installing or removing packages.") @@ -697,73 +142,74 @@ namespace gdpm::package_manager{ } - void _handle_arguments(const cxxargs& args){ + result_t _handle_arguments(const cxxargs& args){ const auto& result = args.result; const auto& options = args.options; + exec_args in; /* Set option variables first to be used in functions below. */ if(result.count("help")){ log::println("{}", options.help()); } if(result.count("config")){ - config.path = result["config"].as(); + config.path = result["config"].as(); config::load(config.path, config); log::info("Config: {}", config.path); } - if(result.count("add-remote")){ - std::string repo = result["remote-add"].as(); - // config.remote_sources.emplace_back(repo); - config.remote_sources.insert(repo); - } - if(result.count("delete-remote")){ - std::string repo = result["remote-add"].as(); - auto iter = std::find(config.remote_sources.begin(), config.remote_sources.end(), repo); - if(iter != config.remote_sources.end()) - config.remote_sources.erase(iter); - } if(result.count("remote")){ + string sub_command = result["remote"].as(); + log::print("sub command: {}", sub_command); + if(sub_command == "add"){ + string argv = result.arguments_string(); + log::println("argv: {}", argv); + remote::add_repositories(config, {}); + } + else if(sub_command == "delete"){ + remote::remove_respositories(config, {}); + log::println("argv: {}"); + } } if(result.count("file")){ - std::string path = result["file"].as(); - std::string contents = utils::readfile(path); + string path = result["file"].as(); + string contents = utils::readfile(path); packages = utils::parse_lines(contents); } if(result.count("path")){ - opts = result["path"].as>(); + in.opts.insert({"path", result["path"].as()}); } if(result.count("sort")){ - std::string r = result["sort"].as(); + string r = result["sort"].as(); rest_api::sort_e sort = rest_api::sort_e::none; if(r == "none") sort = rest_api::sort_e::none; else if(r == "rating") sort = rest_api::sort_e::rating; else if(r == "cost") sort = rest_api::sort_e::cost; else if(r == "name") sort = rest_api::sort_e::name; else if(r == "updated") sort = rest_api::sort_e::updated; - params.sort = sort; + api_params.sort = sort; } if(result.count("type")){ - std::string r = result["type"].as(); + string r = result["type"].as(); rest_api::type_e type = rest_api::type_e::any; if(r == "any") type = rest_api::type_e::any; else if(r == "addon") type = rest_api::type_e::addon; else if(r == "project") type = rest_api::type_e::project; - params.type = type; + api_params.type = type; } if(result.count("support")){ - std::string r = result["support"].as(); + string r = result["support"].as(); rest_api::support_e support = rest_api::support_e::all; if(r == "all") support = rest_api::support_e::all; else if(r == "official") support = rest_api::support_e::official; else if(r == "community") support = rest_api::support_e::community; else if(r == "testing") support = rest_api::support_e::testing; - params.support = support; + api_params.support = support; } if(result.count("max-results")){ - params.max_results = result["max-results"].as(); + api_params.max_results = result["max-results"].as(); } if(result.count("godot-version")){ - config.godot_version = result["godot-version"].as(); + config.godot_version = result["godot-version"].as(); } if(result.count("timeout")){ config.timeout = result["timeout"].as(); @@ -775,26 +221,26 @@ namespace gdpm::package_manager{ priority = result["set-priority"].as(); } if(result.count("set-packages-directory")){ - config.packages_dir = result["set-packages-directory"].as(); + config.packages_dir = result["set-packages-directory"].as(); } if(result.count("set-temporary-directory")){ - config.tmp_dir = result["set-temporary-directory"].as(); + config.tmp_dir = result["set-temporary-directory"].as(); } if(result.count("yes")){ skip_prompt = true; } if(result.count("link")){ - packages = result["link"].as>(); + packages = result["link"].as(); } if(result.count("clone")){ - packages = result["clone"].as>(); + packages = result["clone"].as(); } if(result.count("clean")){ clean_tmp_dir = true; } config.verbose = 0; config.verbose += result["verbose"].as(); - std::string json = to_json(config); + string json = to_json(config); if(config.verbose > 0){ log::println("Verbose set to level {}", config.verbose); log::println("{}", json); @@ -802,181 +248,62 @@ namespace gdpm::package_manager{ if(!result.count("command")){ log::error("Command required. See \"help\" for more information."); - return; + return result_t(in, error()); } - std::vector argv = result["command"].as>(); - std::vector opts{argv.begin()+1, argv.end()}; - if(packages.empty() && opts.size() > 0){ - for(const auto& opt : opts){ - packages.emplace_back(opt); + args_t _argv = result["command"].as(); + string sub_command = _argv[0]; + args_t argv{_argv.begin()+1, _argv.end()}; + if(packages.empty() && in.opts.size() > 0){ + for(const auto& arg : argv){ + packages.emplace_back(arg); } } - /* Catch arguments passed with or without dashes */ - if(argv[0] == "install" || argv[0] == "--install") command = install; - else if (argv[0] == "remove" || argv[0] == "--remove") command = remove; - else if(argv[0] == "update" || argv[0] == "--update") command = update; - else if(argv[0] == "search" || argv[0] == "--search") command = search; - else if(argv[0] == "export" || argv[0] == "--export") command = p_export; - else if(argv[0] == "list" || argv[0] == "-ls") command = list; - else if(argv[0] == "link" || argv[0] == "--link") command = link; - else if(argv[0] == "clone" || argv[0] == "--clone") command = clone; - else if(argv[0] == "clean" || argv[0] == "--clean") command = clean; - else if(argv[0] == "sync" || argv[0] == "--sync") command = sync; - else if(argv[0] == "remote" || argv[0] == "--remote") command = remote; - else if(argv[0] == "help" || argv[0] == "-h" || argv[0] == "--help" || argv[0].empty()){ + /* Catch arguments passed with dashes */ + if(sub_command == "install") action = action_e::install; + else if(sub_command == "remove") action = action_e::remove; + else if(sub_command == "update") action = action_e::update; + else if(sub_command == "search") action = action_e::search; + else if(sub_command == "export") action = action_e::p_export; + else if(sub_command == "list") action = action_e::list; + else if(sub_command == "link") action = action_e::link; + else if(sub_command == "clone") action = action_e::clone; + else if(sub_command == "clean") action = action_e::clean; + else if(sub_command == "sync") action = action_e::sync; + else if(sub_command == "remote") action = action_e::remote; + else if(sub_command == "help" || argv[0] == "-h" || argv[0] == "--help" || argv[0].empty()){ log::println("{}", options.help()); } else{ log::error("Unrecognized command. Try 'gdpm help' for more info."); } + return result_t(in, error()); } /* Used to run the command AFTER parsing and setting all command line args. */ - void run_command(command_e c, const std::vector& args, const std::vector& opts){ + void run_command(action_e c, const args_t& args, const opts_t& opts){ + package::params params; + params.skip_prompt = skip_prompt; switch(c){ - case install: install_packages(args, skip_prompt); break; - case remove: remove_packages(args, skip_prompt); break; - case update: update_packages(args, skip_prompt); break; - case search: search_for_packages(args, skip_prompt); break; - case p_export: export_packages(args); break; - case list: list_information(args); break; + case action_e::install: package::install(config, args, params); break; + case action_e::remove: package::remove(config, args, params); break; + case action_e::update: package::update(config, args, params); break; + case action_e::search: package::search(config, args, params); break; + case action_e::p_export: package::export_to(args); break; + case action_e::list: package::list(config, args, opts); break; /* ...opts are the paths here */ - case link: link_packages(args, opts); break; - case clone: clone_packages(args, opts); break; - case clean: clean_temporary(args); break; - case sync: synchronize_database(args); break; - case remote: _handle_remote(args, opts); break; - case help: /* ...runs in handle_arguments() */ break; - case none: /* ...here to run with no command */ break; + case action_e::link: package::link(config, args, opts); break; + case action_e::clone: package::clone(config, args, opts); break; + case action_e::clean: package::clean_temporary(config, args); break; + case action_e::sync: package::synchronize_database(config, args); break; + case action_e::remote: remote::_handle_remote(config, args, opts); break; + case action_e::help: /* ...runs in handle_arguments() */ break; + case action_e::none: /* ...here to run with no command */ break; } } - - void print_package_list(const std::vector& packages){ - for(const auto& p : packages){ - log::println("{}/{}/{} {} id={}\n\tGodot {}, {}, {}, Last Modified: {}", - p.support_level, - p.author, - p.title, - p.version, - p.asset_id, - p.godot_version, - p.cost, - p.category, - p.modify_date - ); - } - } - - - void print_package_list(const rapidjson::Document& json){ - for(const auto& o : json["result"].GetArray()){ - log::println("{}/{}/{} {} id={}\n\tGodot {}, {}, {}, Last Modified: {}", - o["support_level"] .GetString(), - o["author"] .GetString(), - o["title"] .GetString(), - o["version_string"] .GetString(), - o["asset_id"] .GetString(), - o["godot_version"] .GetString(), - o["cost"] .GetString(), - o["category"] .GetString(), - o["modify_date"] .GetString() - ); - } - } - - - std::vector get_package_titles(const std::vector &packages){ - std::vector package_titles; - std::for_each(packages.begin(), packages.end(), [&package_titles](const package_info& p){ - package_titles.emplace_back(p.title); - }); - return package_titles; - } - - - void print_remote_sources(){ - log::println("Remote sources:"); - for(const auto& s : config.remote_sources){ - log::println("\t{}", s); - } - } - - - std::vector synchronize_database(const std::vector& package_titles){ - using namespace rapidjson; - params.verbose = config.verbose; - params.godot_version = config.godot_version; - params.page = 0; - int page = 0; - int page_length = 0; - // int total_pages = 0; - int total_items = 0; - int items_left = 0; - - log::info("Sychronizing database..."); - do{ - /* Make the GET request to get page data and store it in the local - package database. Also, check to see if we need to keep going. */ - std::string url{constants::HostUrl}; - url += rest_api::endpoints::GET_Asset; - Document doc = rest_api::get_assets_list(url, params); - params.page += 1; - - if(doc.IsNull()){ - log::error("Could not get response from server. Aborting."); - return {}; - } - - /* Need to know how many pages left to get and how many we get per - request. */ - page = doc["page"].GetInt(); - page_length = doc["page_length"].GetInt(); - // total_pages = doc["pages"].GetInt(); - total_items = doc["total_items"].GetInt(); - items_left = total_items - (page + 1) * page_length; - - // log::info("page: {}, page length: {}, total pages: {}, total items: {}, items left: {}", page, page_length, total_pages, total_items, items_left); - - if(page == 0){ - cache::drop_package_database(); - cache::create_package_database(); - } - - std::vector packages; - for(const auto& o : doc["result"].GetArray()){ - // log::println("======================="); - package_info p{ - .asset_id = std::stoul(o["asset_id"].GetString()), - .title = o["title"].GetString(), - .author = o["author"].GetString(), - .author_id = std::stoul(o["author_id"].GetString()), - .version = o["version"].GetString(), - .godot_version = o["godot_version"].GetString(), - .cost = o["cost"].GetString(), - .modify_date = o["modify_date"].GetString(), - .category = o["category"].GetString(), - .remote_source = url - }; - packages.emplace_back(p); - } - cache::insert_package_info(packages); - - /* Make the same request again to get the rest of the needed data - using the same request, but with a different page, then update - variables as needed. */ - - - } while(items_left > 0); - - log::println("Done."); - - return cache::get_package_info_by_title(package_titles); - } - } // namespace gdpm::package_manager diff --git a/src/remote.cpp b/src/remote.cpp new file mode 100644 index 0000000..52de291 --- /dev/null +++ b/src/remote.cpp @@ -0,0 +1,103 @@ + +#include "remote.hpp" +#include "error.hpp" +#include "log.hpp" +#include "types.hpp" +#include + +namespace gdpm::remote{ + error _handle_remote( + config::context& config, + const args_t& args, + const opts_t& opts + ){ + log::println("_handle_remote"); + for(const auto& arg : args){ + log::println("arg: {}", arg); + } + for(const auto& opt : opts){ + log::println("opt: {}:{}", opt.first, utils::join(opt.second)); + } + + /* Check if enough arguments are supplied */ + size_t argc = args.size(); + if (argc < 1){ + print_repositories(config); + return error(); + } + + /* Check which subcommand is supplied */ + string sub_command = args.front(); + args_t argv(args.begin()+1, args.end()); + if(argv.size() < 2){ + error error( + constants::error::INVALID_ARGS, + "Invalid number of args" + ); + log::error(error); + return error; + } + string name = argv[1]; + string url = argv[2]; + if(sub_command == "add") add_repositories(config, {{name, url}}); + else if (sub_command == "remove") remove_respositories(config, argv); + // else if (sub_command == "set") set_repositories(config::context &context, const repository_map &repos) + else if (sub_command == "list") print_repositories(config); + else{ + error error( + constants::error::UNKNOWN, + "Unknown sub-command. Try 'gdpm help remote' for options." + ); + log::error(error); + return error; + } + return error(); + } + + + void set_repositories( + config::context& config, + const repository_map &repos + ){ + config.remote_sources = repos; + } + + + void add_repositories( + config::context& config, + const repository_map &repos + ){ + std::for_each(repos.begin(), repos.end(), + [&config](const string_pair& p){ + config.remote_sources.insert(p); + } + ); + } + + + void remove_respositories( + config::context& config, + const repo_names& names + ){ + std::for_each(names.end(), names.begin(), [&config](const string& repo){ + config.remote_sources.erase(repo); + }); + } + + + void move_respository( + config::context& config, + int old_position, + int new_position + ){ + + } + + void print_repositories(const config::context& config){ + log::println("Remote sources:"); + const auto &rs = config.remote_sources; + std::for_each(rs.begin(), rs.end(), [](const string_pair& p){ + log::println("\t{}: {}", p.first, p.second); + }); + } +} \ No newline at end of file diff --git a/src/rest_api.cpp b/src/rest_api.cpp index ef23e70..ac3cdb4 100644 --- a/src/rest_api.cpp +++ b/src/rest_api.cpp @@ -15,16 +15,12 @@ #include namespace gdpm::rest_api{ - bool register_account(const std::string& username, const std::string& password, const std::string& email){ - return false; - } - - bool login(const std::string& username, const std::string& password){ - return false; - } - - bool logout(){ - return false; + + context make_from_config(const config::context& config){ + context params = make_context(); + params.godot_version = config.godot_version; + params.verbose = config.verbose; + return params; } context make_context(type_e type, int category, support_e support, const std::string& filter, const std::string& user, const std::string& godot_version, int max_results, int page, sort_e sort, bool reverse, int verbose){ @@ -44,7 +40,29 @@ namespace gdpm::rest_api{ return params; } - rapidjson::Document _parse_json(const std::string& r, int verbose){ + bool register_account( + const string& username, + const string& password, + const string& email + ){ + return false; + } + + bool login( + const string& username, + const string& password + ){ + return false; + } + + bool logout(){ + return false; + } + + rapidjson::Document _parse_json( + const string& r, + int verbose + ){ using namespace rapidjson; Document d; d.Parse(r.c_str()); @@ -58,7 +76,7 @@ namespace gdpm::rest_api{ return d; } - std::string to_string(type_e type){ + string to_string(type_e type){ std::string _s{"type="}; switch(type){ case any: _s += "any"; break; @@ -68,8 +86,8 @@ namespace gdpm::rest_api{ return _s; } - std::string to_string(support_e support){ - std::string _s{"support="}; + string to_string(support_e support){ + string _s{"support="}; switch(support){ case all: _s += "official+community+testing"; break; case official: _s += "official"; break; @@ -79,8 +97,8 @@ namespace gdpm::rest_api{ return _s; } - std::string to_string(sort_e sort){ - std::string _s{"sort="}; + string to_string(sort_e sort){ + string _s{"sort="}; switch(sort){ case none: _s += ""; break; case rating: _s += "rating"; break; @@ -91,16 +109,19 @@ namespace gdpm::rest_api{ return _s; } - std::string _prepare_request(const std::string &url, const context &c){ - std::string request_url{url}; + string _prepare_request( + const string &url, + const context &c + ){ + string request_url{url}; request_url += to_string(c.type); - request_url += (c.category <= 0) ? "&category=" : "&category="+fmt::to_string(c.category); + request_url += (c.category <= 0) ? "&category=" : "&category="+std::to_string(c.category); request_url += "&" + to_string(c.support); request_url += "&" + to_string(c.sort); request_url += (!c.filter.empty()) ? "&filter="+c.filter : ""; request_url += (!c.godot_version.empty()) ? "&godot_version="+c.godot_version : ""; - request_url += "&max_results=" + fmt::to_string(c.max_results); - request_url += "&page=" + fmt::to_string(c.page); + request_url += "&max_results=" + std::to_string(c.max_results); + request_url += "&page=" + std::to_string(c.page); request_url += (c.reverse) ? "&reverse" : ""; return request_url; } @@ -122,8 +143,12 @@ namespace gdpm::rest_api{ ); } - rapidjson::Document configure(const std::string& url, type_e type, int verbose){ - std::string request_url{url}; + rapidjson::Document configure( + const string& url, + type_e type, + int verbose + ){ + string request_url{url}; request_url += to_string(type); http::response r = http::request_get(url); if(verbose > 0) @@ -131,7 +156,20 @@ namespace gdpm::rest_api{ return _parse_json(r.body); } - rapidjson::Document get_assets_list(const std::string& url, type_e type, int category, support_e support, const std::string& filter,const std::string& user, const std::string& godot_version, int max_results, int page, sort_e sort, bool reverse, int verbose){ + rapidjson::Document get_assets_list( + const string& url, + type_e type, + int category, + support_e support, + const string& filter, + const string& user, + const string& godot_version, + int max_results, + int page, + sort_e sort, + bool reverse, + int verbose + ){ context c{ .type = type, .category = category, @@ -148,16 +186,23 @@ namespace gdpm::rest_api{ return get_assets_list(url, c); } - rapidjson::Document get_assets_list(const std::string& url, const context& c){ - std::string request_url = _prepare_request(url, c); + rapidjson::Document get_assets_list( + const string& url, + const context& c + ){ + string request_url = _prepare_request(url, c); http::response r = http::request_get(request_url); if(c.verbose > 0) log::info("URL: {}", request_url); return _parse_json(r.body, c.verbose); } - rapidjson::Document get_asset(const std::string& url, int asset_id, const context& params){ - std::string request_url = _prepare_request(url, params); + rapidjson::Document get_asset( + const string& url, + int asset_id, + const context& params + ){ + string request_url = _prepare_request(url, params); utils::replace_all(request_url, "{id}", std::to_string(asset_id)); http::response r = http::request_get(request_url.c_str()); if(params.verbose > 0) @@ -187,16 +232,16 @@ namespace gdpm::rest_api{ } - std::string review_asset_edit(int asset_id){ - return std::string(); + string review_asset_edit(int asset_id){ + return string(); } - std::string accept_asset_edit(int asset_id){ - return std::string(); + string accept_asset_edit(int asset_id){ + return string(); } - std::string reject_asset_edit(int asset_id){ - return std::string(); + string reject_asset_edit(int asset_id){ + return string(); } } // namespace edits diff --git a/src/utils.cpp b/src/utils.cpp index 38bace7..46b36f6 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -66,14 +66,22 @@ namespace gdpm::utils{ return result; } - std::string replace_first(std::string &s, const std::string &from, const std::string &to){ + std::string replace_first( + std::string &s, + const std::string &from, + const std::string &to + ){ size_t pos = s.find(from); if(pos == std::string::npos) return s; return s.replace(pos, from.length(), to); } - std::string replace_all(std::string& s, const std::string& from, const std::string& to){ + std::string replace_all( + std::string& s, + const std::string& from, + const std::string& to + ){ size_t pos = 0; while((pos = s.find(from, pos)) != std::string::npos){ s.replace(pos, s.length(), to); @@ -83,7 +91,11 @@ namespace gdpm::utils{ } /* Ref: https://gist.github.com/mobius/1759816 */ - int extract_zip(const char *archive, const char *dest, int verbose){ + int extract_zip( + const char *archive, + const char *dest, + int verbose + ){ const char *prog = "gpdm"; struct zip *za; struct zip_file *zf; @@ -179,4 +191,15 @@ namespace gdpm::utils{ sleep_for(millis); // sleep_until(system_clock::now() + millis); } + + std::string join( + const std::vector& target, + const std::string& delimiter + ){ + std::string o; + std::for_each(target.begin(), target.end(), [&o, &delimiter](const std::string& s){ + o += s + delimiter; + }); + return o; + } } // namespace gdpm::utils diff --git a/tests/basic.cpp b/tests/basic.cpp index 1bf039f..f482362 100644 --- a/tests/basic.cpp +++ b/tests/basic.cpp @@ -4,11 +4,12 @@ #include "log.hpp" #include "cache.hpp" #include "config.hpp" +#include "package.hpp" #include -TEST_SUITE("Cache functions"){ +TEST_SUITE("Caching functions"){ TEST_CASE("Test cache database functions"){ gdpm::cache::create_package_database(); @@ -21,33 +22,34 @@ TEST_SUITE("Command functions"){ using namespace gdpm::package_manager; config::context config = config::make_context(); - std::vector packages{"ResolutionManagerPlugin","godot-hmac", "Godot"}; + package::params params = package::params(); + package::title_list package_titles{"ResolutionManagerPlugin","godot-hmac", "Godot"}; auto check_error = [](const error& error){ - if(error.has_error()){ - log::error(error.get_message()); + if(error.has_occurred()){ + log::error(error); } - CHECK(!error.has_error()); + CHECK(!error.has_occurred()); }; TEST_CASE("Test install packages"){ - check_error(install_packages(packages, true)); + check_error(package::install(config, package_titles, params)); } TEST_CASE("Test searching packages"){ - check_error(search_for_packages(packages, true)); + check_error(package::search(config, package_titles, params)); } TEST_CASE("Test remove packages"){ - check_error(remove_packages(packages, true)); + check_error(package::remove(config, package_titles, params)); } TEST_CASE("Test exporting installed package list"){ - check_error(export_packages({"tests/gdpm/.tmp/packages.txt"})); + check_error(package::export_to({"tests/gdpm/.tmp/packages.txt"})); } }