Major refactor and API changes

- Updated `.gitignore` file
- Updated `CMakeLists.txt` to build static exectuable
- Changed some `Doxyfile` configurations to build more robust and complete documentation (WIP)
- Changed how `remote` works to better reflect `git`'s API (WIP)
- Changed how error handling works
- Improved `bin/compile.sh` script
- Improved `bin/lines.sh` script (kinda)
- Removed some instances of `fmt` in favor of `std` string functions
- Restructed style for better readibility
This commit is contained in:
David Allen 2023-05-22 17:54:45 -06:00
parent ba23299777
commit 5a73651ad1
29 changed files with 1836 additions and 1140 deletions

3
.gitignore vendored
View file

@ -1,7 +1,8 @@
build/**
builds/**
bin/gdpm
bin/gdpm-tests
bin/gdpm.static
bin/gdpm.tests
docs/doxygen
cache/**
tests/*

View file

@ -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})

View file

@ -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.

View file

@ -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

View file

@ -1,4 +1,4 @@
#!/usr/bin/env bash
# Call this from project root
wc -l include/*.hpp src/*.cpp
grep -v ^S include/*.hpp src/*.cpp | wc -l include/*.hpp src/*.cpp

View file

@ -2,27 +2,31 @@
#pragma once
#include "constants.hpp"
#include "package.hpp"
#include "error.hpp"
#include "result.hpp"
#include <sqlite3.h>
#include <vector>
#include <string>
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_info>& package, const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME);
std::vector<package_info> get_package_info_by_id(const std::vector<size_t>& package_ids, const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME);
std::vector<package_info> get_package_info_by_title(const std::vector<std::string>& package_titles, const std::string& cache_path = GDPM_PACKAGE_CACHE_PATH, const std::string& table_name = GDPM_PACKAGE_CACHE_TABLENAME);
std::vector<package_info> 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<package_info>& 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<std::string>& 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<std::string>& 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<size_t>& 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<package_info>& 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<package::info_list> get_package_info_by_id(const package::id_list& package_ids, const params& = params());
result_t<package::info_list> get_package_info_by_title(const package::title_list& package_titles, const params& params = cache::params());
result_t<package::info_list> 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<std::string>& 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<std::string> to_values(const package::info& package);
result_t<std::string> to_values(const package::info_list& packages);
}

View file

@ -3,32 +3,33 @@
#include "constants.hpp"
#include "error.hpp"
#include <rapidjson/document.h>
#include <string>
#include <filesystem>
#include <vector>
#include <set>
#include <unordered_map>
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<std::string> 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<std::string>& 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;
}

View file

@ -5,6 +5,7 @@
#include <string_view>
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<std::string, std::string>(constants::RemoteName, constants::HostUrl)
#define GDPM_CONFIG_THREADS 1
#define GDPM_CONFIG_TIMEOUT_MS 30000
#define GDPM_CONFIG_ENABLE_SYNC 1

View file

@ -1,43 +1,90 @@
#pragma once
#include <fmt/core.h>
#include <string>
#include "log.hpp"
#include "types.hpp"
#include <fmt/core.h>
#include <new>
#include <string>
#include <functional>
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

View file

@ -1,18 +1,31 @@
#pragma once
#include "constants.hpp"
#include "types.hpp"
#include <unordered_map>
namespace gdpm::http{
using headers_t = std::unordered_map<string, string>;
enum response_code{
OK = 200,
NOT_FOUND = 400
};
struct response{
long code = 0;
std::string body{};
std::unordered_map<std::string, std::string> 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());
}

View file

@ -3,14 +3,14 @@
#include "utils.hpp"
#include "colors.hpp"
#include <fmt/core.h>
// #include <fmt/core.h>
#if __cplusplus > 201703L
// #include <format>
#else
#endif
#include <fmt/printf.h>
#include <fmt/format.h>
// #include <fmt/printf.h>
// #include <fmt/format.h>
/*
TODO: Allow setting the logging *prefix*

113
include/package.hpp Normal file
View file

@ -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 <cstdio>
#include <filesystem>
#include <string>
#include <vector>
#include <rapidjson/document.h>
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<info> 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<info>;
using title_list = std::vector<string>;
using id_list = std::vector<size_t>;
using path = std::string;
using path_list = std::vector<path>;
/*!
@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<info_list> get_package_info(const opts_t& opts);
GDPM_DLL_EXPORT result_t<title_list> 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<info_list> synchronize_database(const config::context& config, const title_list& package_titles);
GDPM_DLL_EXPORT result_t<info_list> resolve_dependencies(const config::context& config, const title_list& package_titles);
}

View file

@ -1,7 +1,10 @@
#pragma once
#include "config.hpp"
#include "package.hpp"
#include "package_manager.hpp"
#include "remote.hpp"
#include "result.hpp"
#include <cstdio>
#include <cxxopts.hpp>
#include <memory>
@ -12,39 +15,23 @@
#include <rapidjson/document.h>
#include <curl/curl.h>
namespace gdpm::package_manager{
extern std::vector<std::string> 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<package_info> 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<package_info>;
using package_titles = std::vector<std::string>;
using cl_arg = std::variant<int, bool, float, std::string>;
using cl_args = std::vector<cl_arg>;
using cl_opts = std::unordered_map<std::string, cl_args>;
GDPM_DLL_EXPORT int initialize(int argc, char **argv);
GDPM_DLL_EXPORT int execute();
GDPM_DLL_EXPORT result_t<exec_args> 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<std::string>& package_titles, bool skip_prompt = false);
GDPM_DLL_EXPORT error remove_packages(const std::vector<std::string>& package_titles, bool skip_prompt = false);
GDPM_DLL_EXPORT error remove_all_packages();
GDPM_DLL_EXPORT error update_packages(const std::vector<std::string>& package_titles, bool skip_prompt = false);
GDPM_DLL_EXPORT error search_for_packages(const std::vector<std::string>& package_titles, bool skip_prompt = false);
GDPM_DLL_EXPORT error export_packages(const std::vector<std::string>& paths);
GDPM_DLL_EXPORT std::vector<std::string> list_information(const std::vector<std::string>& opts, bool print_list = true);
GDPM_DLL_EXPORT void clean_temporary(const std::vector<std::string>& package_titles);
GDPM_DLL_EXPORT void link_packages(const std::vector<std::string>& package_titles, const std::vector<std::string>& paths);
GDPM_DLL_EXPORT void clone_packages(const std::vector<std::string>& package_titles, const std::vector<std::string>& paths);
/* Remote API */
GDPM_DLL_EXPORT error _handle_remote(const std::vector<std::string>& args, const std::vector<std::string>& opts);
GDPM_DLL_EXPORT void remote_add_repository(const std::vector<std::string>& repositories);
GDPM_DLL_EXPORT void remote_remove_respository(const std::vector<std::string>& 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<std::string>& package_titles, const std::vector<std::string>& opts);
GDPM_DLL_EXPORT void print_package_list(const rapidjson::Document& json);
GDPM_DLL_EXPORT void print_package_list(const std::vector<package_info>& packages);
GDPM_DLL_EXPORT void print_remote_sources();
GDPM_DLL_EXPORT std::vector<std::string> get_package_titles(const std::vector<package_info>& packages);
/* Dependency Management API */
GDPM_DLL_EXPORT std::vector<package_info> synchronize_database(const std::vector<std::string>& package_titles);
GDPM_DLL_EXPORT std::vector<std::string> resolve_dependencies(const std::vector<std::string>& package_titles);
GDPM_DLL_EXPORT result_t<exec_args> _handle_arguments(const cxxargs& args);
GDPM_DLL_EXPORT void run_command(action_e command, const package::title_list& package_titles, const opts_t& opts);
}

View file

@ -2,7 +2,7 @@
#include <string>
namespace towk::plugin{
namespace gdpm::plugin{
struct info{
std::string name;
std::string description;

21
include/remote.hpp Normal file
View file

@ -0,0 +1,21 @@
#pragma once
#include "constants.hpp"
#include "error.hpp"
#include "types.hpp"
#include <fmt/core.h>
#include <unordered_map>
#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);
}

View file

@ -1,5 +1,6 @@
#include "constants.hpp"
#include "config.hpp"
#include <rapidjson/document.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>
@ -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);

73
include/result.hpp Normal file
View file

@ -0,0 +1,73 @@
#pragma once
#include "log.hpp"
#include "error.hpp"
#include "types.hpp"
#include <functional>
#include <type_traits>
namespace gdpm{
template <class T, error_t U = error>
class result_t {
public:
result_t() = delete;
result_t(
std::tuple<T, U> tuple,
std::function<T()> ok = []() -> T{},
std::function<void()> error = [](){}
): data(tuple), fn_ok(ok), fn_error(error)
{ }
result_t(
T target,
U error,
std::function<std::unique_ptr<T>()> _fn_ok = []() -> std::unique_ptr<T>{ return nullptr; },
std::function<void()> _fn_error = [](){}
): data(std::make_tuple(target, error)), fn_ok(_fn_ok), fn_error(_fn_error)
{}
void define(
std::function<T()> ok,
std::function<U()> error
){
fn_ok = ok;
fn_error = error;
}
constexpr std::unique_ptr<T> 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<U>(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<U>(data);
if(err.has_occurred())
return default_value;
return fn_ok();
}
constexpr T unwrap_unsafe() const {
return std::get<T>(data);
}
private:
std::tuple<T, U> data;
std::function<std::unique_ptr<T>()> fn_ok;
std::function<void()> fn_error;
};
}

View file

@ -1,6 +1,14 @@
#pragma once
#include <tuple>
#include <functional>
#include <type_traits>
#include <string>
#include <variant>
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 <typename T>
concept error_t = requires{ std::is_same<error, T>::value; };
using string = std::string;
using string_list = std::vector<string>;
using string_map = std::unordered_map<string, string>;
using string_pair = std::pair<string, string>;
using var = std::variant<int, bool, float, std::string>;
template <typename T = var>
using _args_t = std::vector<T>;
using args_t = _args_t<string>;
template <typename Key = std::string, class Value = _args_t<var>>
using _opts_t = std::unordered_map<Key, Value>;
using opts_t = _opts_t<string, string_list>;
}

View file

@ -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<std::string>& 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;
};
}

View file

@ -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 <filesystem>
#include <string>
#include <format>
#include <tuple>
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<package_info>& 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<package_info> get_package_info_by_id(const std::vector<size_t>& package_ids, const std::string& cache_path, const std::string& table_name){
result_t<package::info_list> 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<package_info> 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<package_info> *_p_vector = (std::vector<package_info>*) 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<package_info> get_package_info_by_title(const std::vector<std::string>& package_titles, const std::string& cache_path, const std::string& table_name){
result_t<package::info_list> get_package_info_by_title(const package::title_list& package_titles, const params& params){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
std::vector<package_info> p_vector;
package::info_list p_vector;
auto callback = [](void *data, int argc, char **argv, char **colnames){
if(argc <= 0)
return 1;
std::vector<package_info> *_p_vector = (std::vector<package_info>*)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<package_info> get_installed_packages(const std::string& cache_path, const std::string& table_name){
result_t<package::info_list> get_installed_packages(const params& params){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
std::vector<package_info> 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<package_info> *_p_vector = (std::vector<package_info>*) 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<package_info>& 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<std::string>& 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<size_t>& 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<string> 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<package_info>& packages){
std::string o;
result_t<string> 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());
}
}

View file

@ -1,7 +1,9 @@
#include "config.hpp"
#include "error.hpp"
#include "log.hpp"
#include "utils.hpp"
#include "constants.hpp"
#include "error.hpp"
// RapidJSON
#include <rapidjson/ostreamwrapper.h>
@ -23,6 +25,8 @@
#include <fstream>
#include <ios>
#include <memory>
#include <set>
#include <unordered_map>
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<std::string>& 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;
}
}

View file

@ -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);

View file

@ -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 <package_manager::exec_args> 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;
}

763
src/package.cpp Normal file
View file

@ -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 <future>
#include <rapidjson/ostreamwrapper.h>
#include <rapidjson/prettywriter.h>
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::string, std::string>;
std::vector<ss_pair> dir_pairs;
std::vector<std::future<error>> 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<OStreamWrapper> 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<package::info> 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<std::string> 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<package::info> 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<info_list> get_package_info(const title_list& package_titles){
return cache::get_package_info_by_title(package_titles);
}
result_t<title_list> 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<info_list> 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<info_list> 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());
}
}

File diff suppressed because it is too large Load diff

103
src/remote.cpp Normal file
View file

@ -0,0 +1,103 @@
#include "remote.hpp"
#include "error.hpp"
#include "log.hpp"
#include "types.hpp"
#include <readline/readline.h>
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);
});
}
}

View file

@ -15,16 +15,12 @@
#include <curlpp/Exception.hpp>
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

View file

@ -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<std::string>& 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

View file

@ -4,11 +4,12 @@
#include "log.hpp"
#include "cache.hpp"
#include "config.hpp"
#include "package.hpp"
#include <doctest.h>
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<std::string> 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"}));
}
}