#include "download.h" #include #include "collection.h" constexpr std::string_view downloads("downloads"); constexpr std::string_view headerAcceptJson("accept: application/json"); static const std::regex repo("(^https?):\\/\\/([\\w\\d\\.\\-\\_]+)\\/([\\w\\d\\-\\_]+)\\/([\\w\\d\\-\\_]+)"); unsigned int Download::instances(0); AtomicMutex Download::amx{}; void Download::CurlDeleter::operator()(CURL* curl) const { curl_easy_cleanup(curl); } void Download::FileDeleter::operator()(FILE* file) const { fclose(file); } Download::Download( const std::string& url, const std::filesystem::path& destination, const std::shared_ptr& collection, const std::shared_ptr& logger ) : Loggable(logger), curl(), collection(collection), url(url), destination(destination) { std::lock_guard lock(amx); if (instances == 0) { CURLcode res = curl_global_init(CURL_GLOBAL_ALL); if (res != CURLE_OK) throw CurlError(std::string("Error initializing curl global ") + curl_easy_strerror(res)); } ++instances; createCurl(); } Download::~Download() { std::lock_guard lock(amx); --instances; if (instances == 0) curl_global_cleanup(); } void Download::proceed() { bool success = false; std::optional repo = testRepo(); if (repo.has_value()) { std::optional path = downloadAsRepo(repo.value()); } } std::optional Download::repoInfoGiteaApi1(const RepoInfo& repo) { std::string url = repo.origin() + "/api/v1/repos/" + repo.project(); std::string data; CURLcode code = httpGet(url, data, {headerAcceptJson}); if (code == CURLE_OK) { try { return nlohmann::json::parse(data); } catch (const nlohmann::json::exception& e) { warn(e.what()); } } return std::nullopt; } std::optional Download::downloadAsRepo(const RepoInfo& repo) { info("Trying Gitea v1 API"); std::optional repoInfo = repoInfoGiteaApi1(repo); if (repoInfo.has_value()) { nlohmann::json::const_iterator itr = repoInfo.value().find("default_branch"); if (itr != repoInfo.value().end() && itr->is_string()) { std::string branchName = *itr; info("Gitea v1 API seem to have worked"); info("Default branch is " + branchName); std::optional path = downloadRepoGiteaApi1(repo, branchName); } } return std::nullopt; } std::optional Download::downloadRepoGiteaApi1(const RepoInfo& repo, const std::string& branch) { std::string fileName = branch + ".tar.gz"; std::string url = repo.origin() + "/api/v1/repos/" + repo.project() + "/archive/" + fileName; std::filesystem::path path = destination/downloads/fileName; CURLcode code = httpDownload(url, path); if (code == CURLE_OK) return path; warn("Couldn't download file " + url + ": " + curl_easy_strerror(code)); minor("Removing " + path.string()); if (std::filesystem::exists(path)) std::filesystem::remove_all(path); return std::nullopt; } CURLcode Download::httpGet( const std::string& url, std::string& result, const std::vector& headers) { setHeaders(headers); curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, writeString); curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &result); return curl_easy_perform(curl.get()); } CURLcode Download::httpDownload(const std::string& url, const std::filesystem::path& path, const std::vector& headers) { std::filesystem::create_directories(path.parent_path()); if (std::filesystem::exists(path)) { minor("File " + path.string() + " already exists, will be overwritten"); if (std::filesystem::is_directory(path)) std::filesystem::remove_all(path); } std::unique_ptr filePtr(fopen(path.c_str(), "wb")); if (!filePtr) throw FileError("Could not open file " + path.string() + " to write"); setHeaders(headers); curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, writeFile); curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, filePtr.get()); return curl_easy_perform(curl.get()); // return res; // if (res != CURLE_OK) { // Project::warn("Couldn't download file " + url + ": " + curl_easy_strerror(res)); // } else { // uint32_t code; // curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); // if (code == 200) // result = true; // else // Project::warn("Couldn't download file " + url + ": response code " + std::to_string(code)); // } // } else { // Project::error(std::string("Couldn't open output file ") + destination.c_str()); // } } void Download::setHeaders(const std::vector& headers) { struct curl_slist* curlHeaders = nullptr; for (const std::string_view& header : headers) curlHeaders = curl_slist_append(curlHeaders, header.data()); curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, curlHeaders); } void Download::createCurl() { CURL* crl = curl_easy_init(); if (!crl) throw CurlError("Error creating curl instalce"); curl_easy_setopt(crl, CURLOPT_USERAGENT, "libcurl-agent/1.0" ); curl_easy_setopt(crl, CURLOPT_VERBOSE, 1L); curl_easy_setopt(crl, CURLOPT_DEBUGFUNCTION, trace); curl_easy_setopt(crl, CURLOPT_DEBUGDATA, this); curl_easy_setopt(crl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(crl, CURLOPT_NOPROGRESS, 1L); curl = std::unique_ptr(crl); } std::optional Download::testRepo() const { std::smatch results; if (std::regex_search(url, results, repo)) { info(url + " appears to be a git repository"); return RepoInfo { results[1], results[2], results[3], results[4] }; } return std::nullopt; } Download::CurlError::CurlError(const std::string& message) : std::runtime_error(message) {} Download::FileError::FileError(const std::string& message) : std::runtime_error(message) {} std::string Download::RepoInfo::origin() const { return protocol + "://" + host; } std::string Download::RepoInfo::project() const { return owner + "/" + name; }