Initial commit

First commit with most of the main features implemented. May still need
some bug fixes here and there.
This commit is contained in:
David Allen 2021-12-30 12:56:37 -06:00
commit 1893c7c36b
26 changed files with 2839 additions and 0 deletions

416
src/cache.cpp Normal file
View file

@ -0,0 +1,416 @@
#include "cache.hpp"
#include "log.hpp"
#include "constants.hpp"
#include "package_manager.hpp"
#include "utils.hpp"
#include <string>
namespace gdpm::cache{
int create_package_database(){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg;
int rc = sqlite3_open(GDPM_PACKAGE_CACHE_PATH, &db);
if(rc != SQLITE_OK){
log::error("create_package_database.sqlite3_open(): {}", sqlite3_errmsg(db));
sqlite3_close(db);
return rc;
}
constexpr const char *sql = "CREATE TABLE IF NOT EXISTS "
GDPM_PACKAGE_CACHE_TABLENAME "("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"asset_id INT NOT NULL,"
"type INT NOT NULL,"
"title TEXT NOT NULL,"
"author TEXT NOT NULL,"
"author_id INT NOT NULL,"
"version TEXT NOT NULL,"
"godot_version TEXT NOT NULL,"
"cost TEXT NOT NULL,"
"description TEXT NOT NULL,"
"modify_date TEXT NOT NULL,"
"support_level TEXT NOT NULL,"
"category TEXT NOT NULL,"
"remote_source TEXT NOT NULL,"
"download_url TEXT NOT NULL,"
"download_hash TEXT NOT NULL,"
"is_installed TEXT NOT NULL,"
"install_path TEXT NOT NULL);";
// rc = sqlite3_prepare_v2(db, "SELECT", -1, &res, 0);
rc = sqlite3_exec(db, sql, 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);
sqlite3_free(errmsg);
sqlite3_close(db);
return rc;
}
sqlite3_close(db);
return 0;
}
int insert_package_info(const std::vector<package_info>& packages){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
/* Prepare values to use in sql statement */
std::string sql{"BEGIN TRANSACTION; "};
for(const auto& p : packages){
sql += "INSERT INTO " GDPM_PACKAGE_CACHE_TABLENAME " (" GDPM_PACKAGE_CACHE_COLNAMES ") ";
sql += "VALUES (" + to_values(p) + "); ";
}
sql += "COMMIT;";
// log::println("{}", sql);
int rc = sqlite3_open(GDPM_PACKAGE_CACHE_PATH, &db);
if(rc != SQLITE_OK){
log::error("insert_package_info.sqlite3_open(): {}", sqlite3_errmsg(db));
sqlite3_close(db);
return rc;
}
rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errmsg);
if(rc != SQLITE_OK){
log::error("insert_package_info.sqlite3_exec(): {}", errmsg);
sqlite3_free(errmsg);
sqlite3_close(db);
return rc;
}
sqlite3_close(db);
return 0;
}
std::vector<package_info> get_package_info_by_id(const std::vector<size_t>& package_ids){
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"};
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{
.asset_id = std::stoul(argv[1]),
.type = argv[2],
.title = argv[3],
.author = argv[4],
.author_id = std::stoul(argv[5]),
.version = argv[6],
.godot_version = argv[7],
.cost = argv[8],
.description = argv[9],
.modify_date = argv[10],
.support_level = argv[11],
.category = argv[12],
.remote_source = argv[13],
.download_url = argv[14],
.download_hash = argv[15],
.is_installed = static_cast<bool>(std::stoi(argv[16])),
.install_path = argv[17]
};
_p_vector->emplace_back(p);
return 0;
};
int rc = sqlite3_open(GDPM_PACKAGE_CACHE_PATH, &db);
if(rc != SQLITE_OK){
log::error("get_package_info_by_id.sqlite3_open(): {}", sqlite3_errmsg(db));
sqlite3_close(db);
return {};
}
for(const auto& p_id : package_ids){
sql += "SELECT * FROM " GDPM_PACKAGE_CACHE_TABLENAME " WHERE asset_id=" + fmt::to_string(p_id)+ ";\n";
}
sql += "COMMIT;\n";
rc = sqlite3_exec(db, sql.c_str(), callback, (void*)&p_vector, &errmsg);
if(rc != SQLITE_OK){
log::error("get_package_info_by_id.sqlite3_exec(): {}", errmsg);
sqlite3_free(errmsg);
sqlite3_close(db);
return {};
}
sqlite3_close(db);
return p_vector;
}
std::vector<package_info> get_package_info_by_title(const std::vector<std::string>& package_titles){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
std::vector<package_info> 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;
// 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{
.asset_id = std::stoul(argv[1]),
.type = argv[2],
.title = argv[3],
.author = argv[4],
.author_id = std::stoul(argv[5]),
.version = argv[6],
.godot_version = argv[7],
.cost = argv[8],
.description = argv[9],
.modify_date = argv[10],
.support_level = argv[11],
.category = argv[12],
.remote_source = argv[13],
.download_url = argv[14],
.download_hash = argv[15],
.is_installed = static_cast<bool>(std::stoi(argv[16])),
.install_path = argv[17]
};
_p_vector->emplace_back(p);
return 0;
};
int rc = sqlite3_open(GDPM_PACKAGE_CACHE_PATH, &db);
if(rc != SQLITE_OK){
log::error("get_package_info_by_title.sqlite3_open(): {}", sqlite3_errmsg(db));
sqlite3_close(db);
return {};
}
std::string sql{"BEGIN TRANSACTION;"};
for(const auto& p_title : package_titles){
sql += "SELECT * FROM " GDPM_PACKAGE_CACHE_TABLENAME " 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);
sqlite3_free(errmsg);
sqlite3_close(db);
return {};
}
sqlite3_close(db);
return p_vector;
}
std::vector<package_info> get_installed_packages(){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
std::vector<package_info> p_vector;
std::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{
.asset_id = std::stoul(argv[1]),
.type = argv[2],
.title = argv[3],
.author = argv[4],
.author_id = std::stoul(argv[5]),
.version = argv[6],
.godot_version = argv[7],
.cost = argv[8],
.description = argv[9],
.modify_date = argv[10],
.support_level = argv[11],
.category = argv[12],
.remote_source = argv[13],
.download_url = argv[14],
.download_hash = argv[15],
.is_installed = static_cast<bool>(std::stoi(argv[16])),
.install_path = argv[17]
};
_p_vector->emplace_back(p);
return 0;
};
int rc = sqlite3_open(GDPM_PACKAGE_CACHE_PATH, &db);
if(rc != SQLITE_OK){
log::error("get_installed_packages.sqlite3_open(): {}", sqlite3_errmsg(db));
sqlite3_close(db);
return {};
}
sql += "SELECT * FROM " GDPM_PACKAGE_CACHE_TABLENAME " 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);
sqlite3_free(errmsg);
sqlite3_close(db);
return {};
}
sqlite3_close(db);
return p_vector;
}
int update_package_info(const std::vector<package_info>& packages){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
int rc = sqlite3_open(GDPM_PACKAGE_CACHE_PATH, &db);
if(rc != SQLITE_OK){
log::error("update_package_info.sqlite3_open(): {}", sqlite3_errmsg(db));
sqlite3_close(db);
return rc;
}
std::string sql;
for(const auto& p : packages){
sql += "UPDATE " GDPM_PACKAGE_CACHE_TABLENAME " SET "
" asset_id=" + fmt::to_string(p.asset_id) + ", "
" type='" + p.type + "', "
" title='" + p.title + "', "
" author='" + p.author + "', " +
" author_id=" + fmt::to_string(p.author_id) + ", "
" version='" + p.version + "', " +
" godot_version='" + p.godot_version + "', " +
" cost='" + p.cost + "', " +
" description='" + p.description + "', " +
" modify_date='" + p.modify_date + "', " +
" support_level='" + p.support_level + "', " +
" category='" + p.category + "', " +
" remote_source='" + p.remote_source + "', " +
" download_url='" + p.download_url + "', " +
" download_hash='" + p.download_hash + "', " +
" is_installed=" + fmt::to_string(p.is_installed) + ", "
" install_path='" + p.install_path + "'"
" WHERE title='" + p.title + "' AND asset_id=" + fmt::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);
sqlite3_free(errmsg);
sqlite3_close(db);
return rc;
}
sqlite3_close(db);
return 0;
}
int delete_packages(const std::vector<std::string>& package_titles){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
std::string sql;
int rc = sqlite3_open(GDPM_PACKAGE_CACHE_PATH, &db);
if(rc != SQLITE_OK){
log::error("delete_packages.sqlite3_open(): {}", sqlite3_errmsg(db));
sqlite3_close(db);
return rc;
}
for(const auto& p_title : package_titles){
sql += "DELETE FROM " GDPM_PACKAGE_CACHE_PATH " 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);
sqlite3_free(errmsg);
sqlite3_close(db);
return rc;
}
sqlite3_close(db);
return 0;
}
int delete_packages(const std::vector<size_t>& package_ids){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
std::string sql;
int rc = sqlite3_open(GDPM_PACKAGE_CACHE_PATH, &db);
if(rc != SQLITE_OK){
log::error("delete_packages.sqlite3_open(): {}", errmsg);
sqlite3_close(db);
return rc;
}
for(const auto& p_id : package_ids){
sql += "DELETE FROM " GDPM_PACKAGE_CACHE_PATH " WHERE asset_id="
+ fmt::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);
sqlite3_free(errmsg);
sqlite3_close(db);
return rc;
}
sqlite3_close(db);
return 0;
}
int drop_package_database(){
sqlite3 *db;
sqlite3_stmt *res;
char *errmsg = nullptr;
std::string sql{"DROP TABLE IF EXISTS " GDPM_PACKAGE_CACHE_TABLENAME ";\n"};
int rc = sqlite3_open(GDPM_PACKAGE_CACHE_PATH, &db);
if(rc != SQLITE_OK){
log::error("drop_package_database.sqlite3_open(): {}", sqlite3_errmsg(db));
sqlite3_close(db);
return rc;
}
rc = sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errmsg);
if(rc != SQLITE_OK){
log::error("drop_package_database.sqlite3_exec(): {}", errmsg);
sqlite3_free(errmsg);
sqlite3_close(db);
return rc;
}
return 0;
}
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) + ", ";
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 += "'" + p.version + "', ";
p_values += "'" + p.godot_version + "', ";
p_values += "'" + p.cost + "', ";
p_values += "'" + p.description + "', ";
p_values += "'" + p.modify_date + "', ";
p_values += "'" + p.support_level + "', ";
p_values += "'" + p.category + "', ";
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 += "'" + p.install_path + "'";
return p_values;
}
std::string to_values(const std::vector<package_info>& packages){
std::string o;
for(const auto& p : packages)
o += to_values(p);
return o;
}
}

179
src/config.cpp Normal file
View file

@ -0,0 +1,179 @@
#include "config.hpp"
#include "log.hpp"
#include "utils.hpp"
#include "constants.hpp"
// RapidJSON
#include <rapidjson/ostreamwrapper.h>
#include <rapidjson/rapidjson.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
// fmt
#include <fmt/format.h>
#include <cxxopts.hpp>
#include <iostream>
#include <string>
#include <string_view>
#include <ostream>
#include <fstream>
#include <ios>
#include <memory>
namespace gdpm::config{
config_context config;
std::string to_json(const config_context& params){
auto _build_json_array = [](std::vector<std::string> a){
std::string o{"["};
for(const std::string& src : a)
o += "\"" + src + "\",";
if(o.back() == ',')
o.pop_back();
o += "]";
return o;
};
/* Build a JSON string to pass to document */
std::string json{
"{\"username\":\"" + params.username + "\","
+ "\"password\":\"" + params.password + "\","
+ "\"path\":\"" + params.path + "\","
+ "\"token\":\"" + params.token + "\","
+ "\"godot_version\":\"" + params.godot_version + "\","
+ "\"packages_dir\":\"" + params.packages_dir + "\","
+ "\"tmp_dir\":\"" + params.tmp_dir + "\","
+ "\"remote_sources\":" + _build_json_array(params.remote_sources) + ","
+ "\"threads\":" + fmt::to_string(params.threads) + ","
+ "\"timeout\":" + fmt::to_string(params.timeout) + ","
+ "\"enable_sync\":" + fmt::to_string(params.enable_sync) + ","
+ "\"enable_file_logging\":" + fmt::to_string(params.enable_file_logging)
+ "}"
};
return json;
}
config_context load(std::filesystem::path path, int verbose){
std::fstream file;
file.open(path, std::ios::in);
if(!file){
log::error("Could not open file");
return config;
}
else if(file.is_open()){
/*
* See RapidJson docs:
*
* https://rapidjson.org/md_doc_tutorial.html
*/
using namespace rapidjson;
/* Read JSON fro config, parse, and check document. Must make sure that program does not crash here and use default config instead! */
std::string contents, line;
while(std::getline(file, line))
contents += line + "\n";
if(verbose > 0)
log::info("Load config...\n{}", contents.c_str());
Document doc;
ParseErrorCode status = doc.Parse(contents.c_str()).GetParseError();
if(!doc.IsObject()){
log::error("Could not load config file.");
return config;
}
assert(doc.IsObject());
assert(doc.HasMember("remote_sources"));
assert(doc["remote_sources"].IsArray());
/* Make sure contents were read correctly. */
// if(!status){
// log::error("config::load: Could not parse contents of file (Error: {}/{}).", GetParseError_En(status), doc.GetErrorOffset());
// return config_context();
// }
/* 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()){
config.remote_sources.emplace_back(src.GetString());
}
} else{
log::error("Malformed sources found.");
}
}
auto _get_value_string = [](Document& doc, const char *property){
if(doc.HasMember(property))
if(doc[property].IsString())
return doc[property].GetString();
return "";
};
auto _get_value_int = [](Document& doc, const char *property){
if(doc.HasMember(property))
if(doc[property].IsInt())
return doc[property].GetInt();
return 0;
};
config.username = _get_value_string(doc, "username");
config.password = _get_value_string(doc, "password");
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.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 config;
}
int save(const config_context& config, int verbose){
using namespace rapidjson;
/* Build a JSON string to pass to document */
std::string json = to_json(config);
if(verbose > 0)
log::info("Save config...\n{}", json.c_str());
/* Dump JSON config to file */
Document doc;
doc.Parse(json.c_str());
std::ofstream ofs(config.path);
OStreamWrapper osw(ofs);
PrettyWriter<OStreamWrapper> writer(osw);
doc.Accept(writer);
return 0;
}
config_context make_config(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::vector<std::string>& remote_sources, size_t threads, size_t timeout, bool enable_sync, bool enable_file_logging, int verbose){
config_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,
.tmp_dir = tmp_dir,
.remote_sources = remote_sources,
.threads = threads,
.timeout = timeout,
.enable_sync = enable_sync,
.enable_file_logging = enable_file_logging,
.verbose = verbose
};
return config;
}
}

125
src/http.cpp Normal file
View file

@ -0,0 +1,125 @@
#include "http.hpp"
#include "utils.hpp"
#include "log.hpp"
#include <curl/curl.h>
#include <stdio.h>
#include <chrono>
namespace gdpm::http{
response request_get(const std::string& url, size_t timeout){
CURL *curl = nullptr;
CURLcode res;
utils::memory_buffer buf = utils::make_buffer();
response r;
#if (GDPM_DELAY_HTTP_REQUESTS == 1)
using namespace std::chrono_literals;
utils::delay();
#endif
curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();
if(curl){
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
// curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "name=daniel&project=curl");
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET");
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);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout);
res = curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &r.code);
if(res != CURLE_OK)
log::error("_make_request.curl_easy_perform(): {}", curl_easy_strerror(res));
curl_easy_cleanup(curl);
}
r.body = buf.addr;
utils::free_buffer(buf);
curl_global_cleanup();
return r;
}
response request_post(const std::string& url, const char *post_fields, size_t timeout){
CURL *curl = nullptr;
CURLcode res;
utils::memory_buffer buf = utils::make_buffer();
response r;
#if (GDPM_DELAY_HTTP_REQUESTS == 1)
using namespace std::chrono_literals;
utils::delay();
#endif
curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();
if(curl){
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
// curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "name=daniel&project=curl");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_fields);
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);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout);
res = curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &r.code);
if(res != CURLE_OK)
log::error("_make_request.curl_easy_perform(): {}", curl_easy_strerror(res));
curl_easy_cleanup(curl);
}
r.body = buf.addr;
utils::free_buffer(buf);
curl_global_cleanup();
return r;
}
response download_file(const std::string& url, const std::string& storage_path, size_t timeout){
CURL *curl = nullptr;
CURLcode res;
response r;
FILE *fp;
#if (GDPM_DELAY_HTTP_REQUESTS == 1)
using namespace std::chrono_literals;
utils::delay();
#endif
curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();
if(curl){
fp = fopen(storage_path.c_str(), "wb");
// if(!config.username.empty() && !config.password.empty()){
// std::string curlopt_userpwd{config.username + ":" + config.password};
// curl_easy_setopt(curl, CURLOPT_USERPWD, curlopt_userpwd.c_str());
// }
// /* Switch on full protocol/debug output while testing and disable
// * progress meter by setting to 0L */
// if(config.verbose){
// curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
// curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
// }
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
// curl_easy_setopt(curl, CURLOPT_USERPWD, "user:pass");
curl_easy_setopt(curl, CURLOPT_FAILONERROR, true);
curl_easy_setopt(curl, CURLOPT_HEADER, 0);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);
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);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout);
res = curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &r.code);
if(res != CURLE_OK){
log::error("download_file.curl_easy_perform() failed: {}", curl_easy_strerror(res));
}
fclose(fp);
}
curl_global_cleanup();
return r;
}
}

14
src/main.cpp Normal file
View file

@ -0,0 +1,14 @@
// Godot Package Manager (GPM)
#include "constants.hpp"
#include "log.hpp"
#include "config.hpp"
#include "package_manager.hpp"
int main(int argc, char **argv){
gdpm::package_manager::initialize(argc, argv);
gdpm::package_manager::execute();
gdpm::package_manager::finalize();
return 0;
}

771
src/package_manager.cpp Normal file
View file

@ -0,0 +1,771 @@
#include "package_manager.hpp"
#include "utils.hpp"
#include "rest_api.hpp"
#include "config.hpp"
#include "constants.hpp"
#include "log.hpp"
#include "http.hpp"
#include "cache.hpp"
#include <algorithm>
#include <curl/curl.h>
#include <curl/easy.h>
#include <filesystem>
#include <regex>
#include <fmt/printf.h>
#include <rapidjson/document.h>
#include <cxxopts.hpp>
#include <rapidjson/ostreamwrapper.h>
#include <rapidjson/prettywriter.h>
#include <system_error>
/*
* For cURLpp examples...see the link below:
*
* https://github.com/jpbarrette/curlpp/tree/master/examples
*/
namespace gdpm::package_manager{
std::vector<std::string> repo_sources;
CURL *curl;
CURLcode res;
config::config_context config;
rest_api::asset_list_context params;
command_e command;
std::vector<std::string> packages;
std::vector<std::string> opts;
bool skip_prompt = false;
bool clean_tmp_dir = false;
int priority = -1;
int initialize(int argc, char **argv){
// curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();
config = config::make_config();
params = rest_api::make_context();
command = none;
/* Check for config and create if not exists */
if(!std::filesystem::exists(config.path)){
config::save(config);
}
config = config::load(config.path);
config.enable_sync = true;
std::string json = to_json(config);
/* Create the local databases if it doesn't exist already */
cache::create_package_database();
/* Run the rest of the program then exit */
cxxargs args = parse_arguments(argc, argv);
handle_arguments(args);
return 0;
}
int execute(){
run_command(command, packages, opts);
if(clean_tmp_dir)
clean_temporary(packages);
return 0;
}
void finalize(){
curl_easy_cleanup(curl);
config::save(config);
// curl_global_cleanup();
}
void install_packages(const std::vector<std::string>& package_titles){
using namespace rapidjson;
/* 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<package_info> p_found = {};
std::vector<package_info> 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()){
log::info("Synchronizing database...");
p_cache = synchronize_database(package_titles);
p_cache = cache::get_package_info_by_title(package_titles);
}
}
// FIXME: This does not return the package to be is_installed correctly
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()){
log::error("No packages found to install.");
return;
}
log::println("Packages to install: ");
for(const auto& p : p_found)
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;
}
using ss_pair = std::pair<std::string, std::string>;
std::vector<ss_pair> dir_pairs;
for(auto& p : p_found){
log::info_n("Fetching asset data for \"{}\"...", p.title);
std::string url{constants::HostUrl}, package_dir, tmp_dir, tmp_zip;
url += rest_api::endpoints::GET_AssetId;
/* Retrieve necessary asset data if it was found already in cache */
Document doc;
if(p.download_url.empty() || p.category.empty() || p.description.empty() || p.support_level.empty()){
doc = rest_api::get_asset(url, p.asset_id, config.verbose);
if(doc.HasParseError() || doc.IsNull()){
log::error("Could not get a response from server. ({})", doc.GetParseError());
return;
}
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{
}
/* 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 + "/package.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::print("Downloading \"{}\"...", p.title);
std::string download_url = doc["download_url"].GetString();
std::string title = doc["title"].GetString();
http::response response = http::download_file(download_url, tmp_zip);
if(response.code == 200){
log::println("Done.");
}else{
log::error("Something went wrong...(code {})", response.code);
return;
}
}
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 package database...");
cache::update_package_info(p_found);
log::println("Done.");
}
void remove_packages(const std::vector<std::string>& package_titles){
using namespace rapidjson;
using namespace std::filesystem;
if(package_titles.empty()){
log::error("No packages to remove.");
return;
}
/* Find the packages to remove if they're is_installed and show them to the user */
std::vector<package_info> p_cache = cache::get_package_info_by_title(package_titles);
if(p_cache.empty()){
log::error("Could not find any packages to remove.");
return;
}
/* 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){
log::error("No packages to remove.");
return;
}
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;
}
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::error("Could not remove packages. Parsing 'package.json' returned NULL.");
return;
}
}
}
}
p.is_installed = false;
}
log::println("Done.");
log::info_n("Updating local package database...");
cache::update_package_info(p_cache);
log::println("Done.");
}
void update_packages(const std::vector<std::string>& package_titles){
using namespace rapidjson;
/* If no package titles provided, update everything and then exit */
if(package_titles.empty()){
return;
}
/* Fetch remote asset data and compare to see if there are package updates */
std::vector<package_info> p_updates = {};
std::vector<package_info> p_cache = cache::get_package_info_by_title(package_titles);
std::string url{constants::HostUrl};
url += rest_api::endpoints::GET_Asset;
Document doc = rest_api::get_assets_list(url, params);
if(doc.IsNull()){
log::error("Could not get response from server. Aborting.");
return;
}
for(const auto& p : p_updates){
for(const auto& o : doc["result"].GetArray()){
size_t local_version = std::stoul(p.version);
std::string remote_version_s = o[""].GetString();
}
}
}
void search_for_packages(const std::vector<std::string> &package_titles){
std::vector<package_info> p_cache = cache::get_package_info_by_title(package_titles);
if(!p_cache.empty()){
print_package_list(p_cache);
return;
}
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()){
log::error("Could not search for packages. Are you connected to the internet?");
return;
}
log::info("{} package(s) found...", doc["total_items"].GetInt());
print_package_list(doc);
}
}
void list_installed_packages(){
using namespace rapidjson;
using namespace std::filesystem;
const path path{config.packages_dir};
std::vector<package_info> p_installed = cache::get_installed_packages();
if(p_installed.empty())
return;
log::println("Installed packages:");
print_package_list(p_installed);
}
void read_package_contents(const std::string& package_title){
}
void clean_temporary(const std::vector<std::string>& 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<std::string>& package_titles, const std::vector<std::string>& paths){
using namespace std::filesystem;
std::vector<package_info> p_found = {};
std::vector<package_info> 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);
}
}
/* 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("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());
}
}
}
}
void clone_packages(const std::vector<std::string>& package_titles, const std::vector<std::string>& paths){
using namespace std::filesystem;
std::vector<package_info> p_found = {};
std::vector<package_info> 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);
}
}
/* 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);
}
}
}
void add_remote_repository(const std::string& repository, ssize_t offset){
auto& s = config.remote_sources;
auto iter = (offset > 0) ? s.begin() + offset : s.end() - offset;
config.remote_sources.insert(iter, repository);
}
void delete_remote_repository(const std::string& repository){
auto& s = config.remote_sources;
std::erase(s, repository);
(void)std::remove_if(s.begin(), s.end(), [&repository](const std::string& rs){
return repository == rs;
});
}
void delete_remote_repository(size_t index){
auto& s = config.remote_sources;
// std::erase(s, index);
}
cxxargs parse_arguments(int argc, char **argv){
/* Parse command-line arguments using cxxopts */
cxxopts::Options options(
argv[0],
"Package manager made for managing Godot assets."
);
options.allow_unrecognised_options();
options.custom_help("This is a custom help string.");
options.add_options("Command")
("input", "", cxxopts::value<std::vector<std::string>>())
("install", "Install package or packages.", cxxopts::value<std::vector<std::string>>()->implicit_value(""), "<packages...>")
("remove", "Remove a package or packages.", cxxopts::value<std::vector<std::string>>()->implicit_value(""), "<packages...>")
("update", "Update a package or packages. This option will update all packages if no argument is supplied.", cxxopts::value<std::vector<std::string>>()->implicit_value(""), "<packages...>")
("search", "Search for a package or packages.", cxxopts::value<std::vector<std::string>>(), "<packages...>")
("list", "Show list of is_installed packages.")
("link", "Create a symlink (or shortcut) to target directory.", cxxopts::value<std::vector<std::string>>(), "<packages...>")
("clone", "Clone packages into target directory.", cxxopts::value<std::vector<std::string>>(), "<packages...>")
("clean", "Clean temporary downloaded files.")
("sync", "Sync local database with remote server.")
("add-remote", "Set a source repository.", cxxopts::value<std::string>()->default_value(constants::AssetRepo), "<url>")
("delete-remote", "Remove a source repository from list.", cxxopts::value<std::string>(), "<url>")
("h,help", "Print this message and exit.")
;
options.parse_positional({"input"});
options.add_options("Options")
("c,config", "Set the config file path.", cxxopts::value<std::string>())
("f,file", "Read file to install or remove packages.", cxxopts::value<std::string>(), "<path>")
("path", "Specify a path to use with a command", cxxopts::value<std::vector<std::string>>())
("type", "Set package type (any|addon|project).", cxxopts::value<std::string>())
("sort", "Sort packages in order (rating|cost|name|updated).", cxxopts::value<std::string>())
("support", "Set the support level for API (all|official|community|testing).")
("max-results", "Set the max results to return from search.", cxxopts::value<int>()->default_value("500"), "<int>")
("godot-version", "Set the Godot version to include in request.", cxxopts::value<std::string>())
("set-priority", "Set the priority for remote source. Lower values are used first (0...100).", cxxopts::value<int>())
("set-packages-directory", "Set the local package storage location.", cxxopts::value<std::string>())
("set-temporary-directory", "Set the local temporary storage location.", cxxopts::value<std::string>())
("timeout", "Set the amount of time to wait for a response.", cxxopts::value<size_t>())
("no-sync", "Disable synchronizing with remote.", cxxopts::value<bool>()->implicit_value("true")->default_value("false"))
("y,yes", "Bypass yes/no prompt for installing or removing packages.")
("v,verbose", "Show verbose output.", cxxopts::value<int>()->implicit_value("1")->default_value("0"), "0-5")
;
auto result = options.parse(argc, argv);
return {result, options};
}
void handle_arguments(const cxxargs& args){
auto _get_package_list = [](const cxxopts::ParseResult& result, const char *arg){
return result[arg].as<std::vector<std::string>>();
};
const auto& result = args.result;
const auto& options = args.options;
/* Set option variables first to be used in functions below. */
if(result.count("config")){
config.path = result["config"].as<std::string>();
config = config::load(config.path);
log::info("Config: {}", config.path);
}
if(result.count("add-remote")){
std::string repo = result["remote-add"].as<std::string>();
config.remote_sources.emplace_back(repo);
}
if(result.count("delete-remote")){
std::string repo = result["remote-add"].as<std::string>();
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("file")){
std::string path = result["file"].as<std::string>();
std::string contents = utils::readfile(path);
packages = utils::parse_lines(contents);
}
if(result.count("path")){
opts = result["path"].as<std::vector<std::string>>();
}
if(result.count("sort")){
rest_api::sort_e sort = rest_api::sort_e::none;
std::string r = result["sort"].as<std::string>();
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;
}
if(result.count("type")){
rest_api::type_e type = rest_api::type_e::any;
std::string r = result["type"].as<std::string>();
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;
}
if(result.count("support")){
rest_api::support_e support = rest_api::support_e::all;
std::string r = result["support"].as<std::string>();
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;
}
if(result.count("max-results")){
params.max_results = result["max-results"].as<int>();
}
if(result.count("godot-version")){
config.godot_version = result["godot-version"].as<std::string>();
}
if(result.count("timeout")){
config.timeout = result["timeout"].as<size_t>();
}
if(result.count("no-sync")){
config.enable_sync = false;
}
if(result.count("set-priority")){
priority = result["set-priority"].as<int>();
}
if(result.count("set-packages-directory")){
config.packages_dir = result["set-packages-directory"].as<std::string>();
}
if(result.count("set-temporary-directory")){
config.tmp_dir = result["set-temporary-directory"].as<std::string>();
}
if(result.count("yes")){
skip_prompt = true;
}
if(result.count("link")){
packages = result["link"].as<std::vector<std::string>>();
}
if(result.count("clone")){
packages = result["clone"].as<std::vector<std::string>>();
}
if(result.count("clean")){
clean_tmp_dir = true;
}
config.verbose = 0;
config.verbose += result["verbose"].as<int>();
std::string json = to_json(config);
if(config.verbose > 0){
log::println("Verbose set to level {}", config.verbose);
log::println("{}", json);
}
if(!result.count("input")){
log::error("Command required. See \"help\" for more information.");
return;
}
std::vector<std::string> argv = result["input"].as<std::vector<std::string>>();
std::vector<std::string> opts{argv.begin()+1, argv.end()};
if(packages.empty() && opts.size() > 0){
for(const auto& opt : opts){
packages.emplace_back(opt);
}
}
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] == "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] == "add-remote" || argv[0] == "--add-remote") command = add_remote;
else if(argv[0] == "delete-remote" || argv[0] == "--delete-remote") command = delete_remote;
else if(argv[0] == "help" || argv[0] == "-h" || argv[0] == "--help"){
log::println("{}", options.help());
}
}
/* Used to run the command AFTER parsing and setting all command line args. */
void run_command(command_e c, const std::vector<std::string>& package_titles, const std::vector<std::string>& opts){
switch(c){
case install: install_packages(package_titles); break;
case remove: remove_packages(package_titles); break;
case update: update_packages(package_titles); break;
case search: search_for_packages(package_titles); break;
case list: list_installed_packages(); break;
/* ...opts are the paths here */
case link: link_packages(package_titles, opts); break;
case clone: clone_packages(package_titles, opts); break;
case clean: clean_temporary(package_titles); break;
case sync: synchronize_database(package_titles); break;
case add_remote: add_remote_repository(opts[0], priority); break;
case delete_remote: delete_remote_repository(opts[0]); break;
case help: /* ...runs in handle_arguments() */ break;
case none: /* ...here to run with no command */ break;
}
}
void print_package_list(const std::vector<package_info>& packages){
for(const auto& p : packages){
log::println("{}/{} {} id={}\n\t{}, Godot {}, {}, {}, Last Modified: {}",
p.support_level,
p.title,
p.version,
p.asset_id,
p.author,
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\t{}, Godot {}, {}, {}, Last Modified: {}",
o["support_level"] .GetString(),
o["title"] .GetString(),
o["version_string"] .GetString(),
o["asset_id"] .GetString(),
o["author"] .GetString(),
o["godot_version"] .GetString(),
o["cost"] .GetString(),
o["category"] .GetString(),
o["modify_date"] .GetString()
);
}
}
std::vector<package_info> synchronize_database(const std::vector<std::string>& 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;
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<package_info> 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);
return cache::get_package_info_by_title(package_titles);
}
} // namespace gdpm::package_manager

189
src/rest_api.cpp Normal file
View file

@ -0,0 +1,189 @@
#include "rest_api.hpp"
#include "constants.hpp"
#include "http.hpp"
#include "log.hpp"
#include "utils.hpp"
#include <curl/curl.h>
#include <list>
#include <string>
#include <ostream>
#include <curlpp/cURLpp.hpp>
#include <curlpp/Easy.hpp>
#include <curlpp/Options.hpp>
#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;
}
asset_list_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){
asset_list_context params{
.type = type,
.category = category,
.support = support,
.filter = filter,
.user = user,
.godot_version = godot_version,
.max_results = max_results,
.page = page,
.sort = sort,
.reverse = reverse,
.verbose = verbose
};
return params;
}
rapidjson::Document _parse_json(const std::string& r, int verbose){
using namespace rapidjson;
Document d;
d.Parse(r.c_str());
StringBuffer buffer;
PrettyWriter<StringBuffer> writer(buffer);
d.Accept(writer);
if(verbose > 1)
log::info("JSON Response: \n{}", buffer.GetString());
return d;
}
std::string _get_type_string(type_e type){
std::string _s{"type="};
switch(type){
case any: _s += "any"; break;
case addon: _s += "addon"; break;
case project: _s += "project"; break;
}
return _s;
}
std::string _get_support_string(support_e support){
std::string _s{"support="};
switch(support){
case all: _s += "official+community+testing"; break;
case official: _s += "official"; break;
case community: _s += "community"; break;
case testing: _s += "testing"; break;
}
return _s;
}
std::string _get_sort_string(sort_e sort){
std::string _s{"sort="};
switch(sort){
case none: _s += ""; break;
case rating: _s += "rating"; break;
case cost: _s += "cost"; break;
case name: _s += "name"; break;
case updated: _s += "updated"; break;
}
return _s;
}
void _print_params(const asset_list_context& params){
log::println("params: \n"
"\ttype: {}\n"
"\tcategory: {}\n"
"\tsupport: {}\n"
"\tfilter: {}\n"
"\tgodot version: {}\n"
"\tmax results: {}\n",
params.type,
params.category,
params.support,
params.filter,
params.godot_version,
params.max_results
);
}
rapidjson::Document configure(const std::string& url, type_e type, int verbose){
std::string request_url{url};
request_url += _get_type_string(type);
http::response r = http::request_get(url);
if(verbose > 0)
log::info("URL: {}", url);
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){
std::string request_url{url};
request_url += _get_type_string(type);
request_url += (category <= 0) ? "&category=" : "&category="+fmt::to_string(category);
request_url += "&" + _get_support_string(support);
request_url += "&" + _get_sort_string(sort);
request_url += (!filter.empty()) ? "&filter="+filter : "";
request_url += (!godot_version.empty()) ? "&godot_version="+godot_version : "";
request_url += "&max_results=" + fmt::to_string(max_results);
request_url += "&page=" + fmt::to_string(page);
request_url += (reverse) ? "&reverse" : "";
http::response r = http::request_get(request_url);
if(verbose > 0)
log::info("URL: {}", request_url);
return _parse_json(r.body, verbose);
}
rapidjson::Document get_assets_list(const std::string& url, const asset_list_context& params){
return get_assets_list(
url, params.type, params.category, params.support, params.filter, params.user, params.godot_version, params.max_results, params.page, params.sort, params.reverse, params.verbose
);
}
rapidjson::Document get_asset(const std::string& url, int asset_id, int verbose){
std::string request_url{url};
request_url = utils::replace_all(request_url, "{id}", fmt::to_string(asset_id));
http::response r = http::request_get(request_url.c_str());
if(verbose > 0)
log::info("URL: {}", request_url);
return _parse_json(r.body);
}
bool delete_asset(int asset_id){
return false;
}
bool undelete_asset(int asset_id){
return false;
}
bool set_support_level(int asset_id){
return false;
}
namespace edits{
void edit_asset(){
}
void get_asset_edit(int asset_id){
}
std::string review_asset_edit(int asset_id){
return std::string();
}
std::string accept_asset_edit(int asset_id){
return std::string();
}
std::string reject_asset_edit(int asset_id){
return std::string();
}
} // namespace edits
}

182
src/utils.cpp Normal file
View file

@ -0,0 +1,182 @@
#include "utils.hpp"
#include "config.hpp"
#include "log.hpp"
#include <asm-generic/errno-base.h>
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <iostream>
#include <fstream>
#include <fcntl.h>
#include <rapidjson/ostreamwrapper.h>
#include <rapidjson/writer.h>
#include <readline/readline.h>
#include <thread>
#include <zip.h>
namespace gdpm::utils{
#if (GDPM_READFILE_IMPL == 0)
std::string readfile(const std::string& path){
constexpr auto read_size = std::size_t{4096};
auto stream = std::ifstream{path.data()};
stream.exceptions(std::ios_base::badbit);
auto out = std::string{};
auto buf = std::string(read_size, '\0');
while (stream.read(& buf[0], read_size)) {
out.append(buf, 0, stream.gcount());
}
out.append(buf, 0, stream.gcount());
return out;
}
#elif(GDPM_READFILE_IMPL == 1)
std::string readfile(const std::string& path){
std::ifstream ifs(path);
return std::string(
(std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>())
);
}
#elif(GDPM_READFILE_IMPL == 2)
std::string readfile(const std::string& path){
std::ifstream ifs(path);
std::stringstream buffer;
buffer << ifs.rdbuf();
return buffer.str();
}
#endif
void to_lower(std::string& s){
std::transform(s.begin(), s.end(), s.begin(), tolower);
}
std::vector<std::string> parse_lines(const std::string &s){
std::string line;
std::vector<std::string> result;
std::stringstream ss(s);
while(std::getline(ss, line)){
result.emplace_back(line);
}
return result;
}
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){
size_t pos = 0;
while((pos = s.find(from, pos)) != std::string::npos){
s.replace(pos, s.length(), to);
pos += to.length();
}
return s;
}
/* Ref: https://gist.github.com/mobius/1759816 */
int extract_zip(const char *archive, const char *dest, int verbose){
const char *prog = "gpdm";
struct zip *za;
struct zip_file *zf;
struct zip_stat sb;
char buf[100];
int err;
int i, len, fd;
zip_uint64_t sum;
log::info_n("Extracting package contents to '{}'...", dest);
if((za = zip_open(archive, 0, &err)) == nullptr){
zip_error_to_str(buf, sizeof(buf), err, errno);
log::error("{}: can't open zip archive {}: {}", prog, archive, buf);
return 1;
}
for(i = 0; i < zip_get_num_entries(za, 0); i++){
if(zip_stat_index(za, i, 0, &sb) == 0){
len = strlen(sb.name);
if(verbose > 1){
log::print("{}, ", sb.name);
log::println("size: {}, ", sb.size);
}
std::string path{dest};
path += sb.name;
if(sb.name[len-1] == '/'){
// safe_create_dir(sb.name);
std::filesystem::create_directory(path);
} else {
zf = zip_fopen_index(za, i, 0);
if(!zf){
log::error("extract_zip: zip_fopen_index() failed.");
return 100;
}
#ifdef _WIN32
fd = open(sb.name, O_RDWR | O_TRUNC | O_CREAT | O_BINARY, 0644);
#else
fd = open(path.c_str(), O_RDWR | O_TRUNC | O_CREAT, 0644);
#endif
if(fd < 0){
log::error("extract_zip: open() failed. (path: {}, fd={})", path, fd);
return 101;
}
sum = 0;
while(sum != sb.size){
len = zip_fread(zf, buf, 100);
if(len < 0){
log::error("extract_zip: zip_fread() returned len < 0 (len={})", len);
return 102;
}
write(fd, buf, len);
sum += len;
}
close(fd);
zip_fclose(zf);
}
} else {
log::println("File[{}] Line[{}]\n", __FILE__, __LINE__);
}
}
if(zip_close(za) == -1){
log::error("{}: can't close zip archive '{}'", prog, archive);
return 1;
}
log::println("Done.");
return 0;
}
std::string prompt_user(const char *message){
log::print("{} ", message);
std::string input;
std::cin >> input;
return input;
}
bool prompt_user_yn(const char *message){
std::string input{"y"};
do{
input = utils::prompt_user(message);
to_lower(input);
input = (input == "\0" || input == "\n" || input.empty()) ? "y" : input;
}while( !std::cin.fail() && input != "y" && input != "n");
return input == "y";
}
void delay(std::chrono::milliseconds millis){
using namespace std::this_thread;
using namespace std::chrono_literals;
using std::chrono::system_clock;
sleep_for(millis);
// sleep_until(system_clock::now() + millis);
}
} // namespace towk::utils