diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e7ef44..feafcbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ add_executable(ezremote_client source/clients/archiveorg.cpp source/clients/ftpclient.cpp source/clients/gdrive.cpp + source/clients/github.cpp source/clients/myrient.cpp source/clients/iis.cpp source/clients/nginx.cpp @@ -69,7 +70,7 @@ add_executable(ezremote_client add_self(ezremote_client) -add_pkg(ezremote_client ${CMAKE_SOURCE_DIR}/data "RMTC00001" "ezRemote Client" "01.34" 32 0) +add_pkg(ezremote_client ${CMAKE_SOURCE_DIR}/data "RMTC00001" "ezRemote Client" "01.35" 32 0) target_link_libraries(ezremote_client c diff --git a/source/actions.cpp b/source/actions.cpp index 888fe26..28e953a 100644 --- a/source/actions.cpp +++ b/source/actions.cpp @@ -12,6 +12,7 @@ #include "clients/webdav.h" #include "clients/apache.h" #include "clients/archiveorg.h" +#include "clients/github.h" #include "clients/myrient.h" #include "clients/nginx.h" #include "clients/npxserve.h" @@ -1314,6 +1315,8 @@ namespace Actions remoteclient = new ArchiveOrgClient(); else if (strcmp(remote_settings->http_server_type, HTTP_SERVER_MYRIENT) == 0) remoteclient = new MyrientClient(); + else if (strcmp(remote_settings->http_server_type, HTTP_SERVER_GITHUB) == 0) + remoteclient = new GithubClient(); } else if (strncmp(remote_settings->server, "webdavs://", 10) == 0 || strncmp(remote_settings->server, "webdav://", 9) == 0) { diff --git a/source/clients/github.cpp b/source/clients/github.cpp new file mode 100644 index 0000000..66dbd2d --- /dev/null +++ b/source/clients/github.cpp @@ -0,0 +1,228 @@ +#include +#include +#include +#include "common.h" +#include "clients/remote_client.h" +#include "clients/github.h" +#include "lang.h" +#include "util.h" +#include "windows.h" + +using httplib::Client; +using httplib::Headers; +using httplib::Result; + +int GithubClient::Connect(const std::string &url, const std::string &username, const std::string &password) +{ + if (url.find("https://github.com") == std::string::npos) + return 0; + + this->host_url = "https://api.github.com"; + this->base_path = "/repos" + url.substr(18); + Util::Rtrim(this->base_path, "/"); + this->base_path += "/releases"; + + client = new httplib::Client(this->host_url); + if (username.length() > 0) + client->set_basic_auth(username, password); + client->set_follow_location(true); + client->set_connection_timeout(10); + client->set_read_timeout(30); + client->enable_server_certificate_verification(false); + m_client.Connect("https://github.com", username, password); + + if (Ping()) + this->connected = true; + return 1; +} + +std::vector GithubClient::ListDir(const std::string &path) +{ + std::vector out; + DirEntry entry; + Util::SetupPreviousFolder(path, &entry); + out.push_back(entry); + + if (!releases_parsed) + { + if (auto res = client->Get(this->base_path + "?per_page=100&page=1")) + { + if (HTTP_SUCCESS(res->status)) + { + json_object *jobj = json_tokener_parse(res->body.c_str()); + struct array_list *areleases = json_object_get_array(jobj); + + for (size_t release_idx = 0; release_idx < areleases->length; ++release_idx) + { + GitRelease release_entry; + + json_object *release = (json_object *)array_list_get_idx(areleases, release_idx); + release_entry.name = std::string(json_object_get_string(json_object_object_get(release, "tag_name"))); + std::string date_time = std::string(json_object_get_string(json_object_object_get(release, "published_at"))); + + auto date_time_array = Util::Split(date_time, "T"); + auto date_array = Util::Split(date_time_array[0], "-"); + auto time_array = Util::Split(date_time_array[1], ":"); + release_entry.modified.year = std::atoi(date_array[0].c_str()); + release_entry.modified.month = std::atoi(date_array[1].c_str()); + release_entry.modified.day = std::atoi(date_array[2].c_str()); + release_entry.modified.hours = std::atoi(time_array[0].c_str()); + release_entry.modified.minutes = std::atoi(time_array[1].c_str()); + release_entry.modified.seconds = std::atoi(time_array[2].substr(0,2).c_str()); + + json_object *obj_assets = json_object_object_get(release, "assets"); + if (json_object_get_type(obj_assets) == json_type_array) + { + struct array_list *aassets = json_object_get_array(obj_assets); + std::map assets; + + for (size_t asset_idx = 0; asset_idx < aassets->length; ++asset_idx) + { + GitAsset asset_entry; + + json_object *asset = (json_object *)array_list_get_idx(aassets, asset_idx); + asset_entry.name = std::string(json_object_get_string(json_object_object_get(asset, "name"))); + asset_entry.size = json_object_get_uint64(json_object_object_get(asset, "size")); + std::string date_time = std::string(json_object_get_string(json_object_object_get(asset, "updated_at"))); + asset_entry.url = std::string(json_object_get_string(json_object_object_get(asset, "browser_download_url"))); + Util::ReplaceAll(asset_entry.url, "https://github.com", ""); + + auto date_time_array = Util::Split(date_time, "T"); + auto date_array = Util::Split(date_time_array[0], "-"); + auto time_array = Util::Split(date_time_array[1], ":"); + asset_entry.modified.year = std::atoi(date_array[0].c_str()); + asset_entry.modified.month = std::atoi(date_array[1].c_str()); + asset_entry.modified.day = std::atoi(date_array[2].c_str()); + asset_entry.modified.hours = std::atoi(time_array[0].c_str()); + asset_entry.modified.minutes = std::atoi(time_array[1].c_str()); + asset_entry.modified.seconds = std::atoi(time_array[2].substr(0,2).c_str()); + + assets.insert(std::make_pair(asset_entry.name, asset_entry)); + } + + m_assets.insert(std::make_pair(release_entry.name, assets)); + } + + m_releases.push_back(release_entry); + } + } + } + releases_parsed = true; + } + + if (path.compare("/") == 0) // return releases as folders + { + for (std::vector::iterator release = m_releases.begin(); release != m_releases.end();) + { + DirEntry entry; + entry.isDir = true; + entry.selectable = true; + entry.file_size = 0; + snprintf(entry.directory, 512, "%s", "/"); + snprintf(entry.name, 256, "%s", release->name.c_str()); + snprintf(entry.path, 768, "/%s", release->name.c_str()); + snprintf(entry.display_size, 48, "%s", lang_strings[STR_FOLDER]); + entry.modified = release->modified; + + out.push_back(entry); + release++; + } + } + else // return assets in the releases matching the path + { + std::string tag_name = path.substr(1); + std::map assets = m_assets[tag_name]; + for (std::map::iterator asset = assets.begin(); asset != assets.end();) + { + DirEntry entry; + memset(&entry, 0, sizeof(DirEntry)); + entry.isDir = false; + entry.selectable = true; + snprintf(entry.directory, 512, "%s", path.c_str()); + snprintf(entry.name, 256, "%s", asset->second.name.c_str()); + snprintf(entry.path, 768, "%s/%s", path.c_str(), asset->second.name.c_str()); + entry.file_size = asset->second.size; + entry.modified = asset->second.modified; + DirEntry::SetDisplaySize(&entry); + + out.push_back(entry); + asset++; + } + } + + return out; +} + +int GithubClient::Size(const std::string &path, int64_t *size) +{ + std::vector path_parts = Util::Split(path, "/"); + + if (path_parts.size() != 2) + { + return 0; + } + + *size = m_assets[path_parts[0]][path_parts[1]].size; + + return 1; +} + +int GithubClient::Head(const std::string &path, void *buffer, uint64_t len) +{ + std::vector path_parts = Util::Split(path, "/"); + + if (path_parts.size() != 2) + { + return 0; + } + + return m_client.Head(m_assets[path_parts[0]][path_parts[1]].url, buffer, len); +} + +int GithubClient::Get(const std::string &outputfile, const std::string &path, uint64_t offset) +{ + std::vector path_parts = Util::Split(path, "/"); + + if (path_parts.size() != 2) + { + return 0; + } + + return m_client.Get(outputfile, m_assets[path_parts[0]][path_parts[1]].url, offset); +} + +int GithubClient::Get(SplitFile *split_file, const std::string &path, uint64_t offset) +{ + std::vector path_parts = Util::Split(path, "/"); + + if (path_parts.size() != 2) + { + return 0; + } + + return m_client.Get(split_file, m_assets[path_parts[0]][path_parts[1]].url, offset); +} + +int GithubClient::GetRange(const std::string &path, void *buffer, uint64_t size, uint64_t offset) +{ + std::vector path_parts = Util::Split(path, "/"); + + if (path_parts.size() != 2) + { + return 0; + } + + return m_client.GetRange(m_assets[path_parts[0]][path_parts[1]].url, buffer, size, offset); +} + +int GithubClient::GetRange(const std::string &path, DataSink &sink, uint64_t size, uint64_t offset) +{ + std::vector path_parts = Util::Split(path, "/"); + + if (path_parts.size() != 2) + { + return 0; + } + + return m_client.GetRange(m_assets[path_parts[0]][path_parts[1]].url, sink, size, offset); +} diff --git a/source/clients/github.h b/source/clients/github.h new file mode 100644 index 0000000..5fdf318 --- /dev/null +++ b/source/clients/github.h @@ -0,0 +1,44 @@ +#ifndef EZ_GITHUB_H +#define EZ_GITHUB_H + +#include +#include +#include "http/httplib.h" +#include "clients/remote_client.h" +#include "clients/baseclient.h" +#include "common.h" + +class GithubClient : public BaseClient +{ +public: + int Connect(const std::string &url, const std::string &username, const std::string &password); + std::vector ListDir(const std::string &path); + int Size(const std::string &path, int64_t *size); + int Get(const std::string &outputfile, const std::string &path, uint64_t offset=0); + int Get(SplitFile *split_file, const std::string &path, uint64_t offset=0); + int GetRange(const std::string &path, void *buffer, uint64_t size, uint64_t offset); + int GetRange(const std::string &path, DataSink &sink, uint64_t size, uint64_t offset); + int Head(const std::string &path, void *buffer, uint64_t len); + +private: + struct GitAsset + { + std::string name; + std::string url; + DateTime modified; + uint64_t size; + }; + + struct GitRelease + { + std::string name; + DateTime modified; + }; + + std::vector m_releases; + std::map> m_assets; + bool releases_parsed = false; + BaseClient m_client; +}; + +#endif \ No newline at end of file diff --git a/source/config.cpp b/source/config.cpp index 2f73163..0a6a865 100644 --- a/source/config.cpp +++ b/source/config.cpp @@ -162,7 +162,7 @@ namespace CONFIG "Hungarian", "Indonesian", "Italiano", "Japanese", "Korean", "Norwegian", "Polish", "Portuguese_BR", "Russian", "Romanian", "Ryukyuan", "Spanish", "Turkish", "Simplified Chinese", "Traditional Chinese", "Thai", "Ukrainian", "Vietnamese"}; - http_servers = {HTTP_SERVER_APACHE, HTTP_SERVER_MS_IIS, HTTP_SERVER_NGINX, HTTP_SERVER_NPX_SERVE, HTTP_SERVER_RCLONE, HTTP_SERVER_ARCHIVEORG, HTTP_SERVER_MYRIENT}; + http_servers = {HTTP_SERVER_APACHE, HTTP_SERVER_MS_IIS, HTTP_SERVER_NGINX, HTTP_SERVER_NPX_SERVE, HTTP_SERVER_RCLONE, HTTP_SERVER_ARCHIVEORG, HTTP_SERVER_MYRIENT, HTTP_SERVER_GITHUB}; text_file_extensions = { ".txt", ".ini", ".log", ".json", ".xml", ".html", ".xhtml", ".conf", ".config" }; image_file_extensions = { ".bmp", ".jpg", ".jpeg", ".png", ".webp" }; diff --git a/source/config.h b/source/config.h index 271886c..4ea4707 100644 --- a/source/config.h +++ b/source/config.h @@ -84,6 +84,7 @@ #define HTTP_SERVER_RCLONE "RClone" #define HTTP_SERVER_ARCHIVEORG "Archive.org" #define HTTP_SERVER_MYRIENT "Myrient" +#define HTTP_SERVER_GITHUB "Github" #define MAX_EDIT_FILE_SIZE 32768 diff --git a/source/windows.cpp b/source/windows.cpp index 14e5327..bb70c42 100644 --- a/source/windows.cpp +++ b/source/windows.cpp @@ -2647,6 +2647,10 @@ namespace Windows { sprintf(remote_settings->http_server_type, "%s", HTTP_SERVER_MYRIENT); } + else if (strncasecmp(remote_settings->server, "https://github.com/", 19) == 0) + { + snprintf(remote_settings->http_server_type, 24, "%s", HTTP_SERVER_GITHUB); + } } }