217 lines
6.5 KiB
C++
217 lines
6.5 KiB
C++
#include "download.h"
|
|
|
|
#include <regex>
|
|
|
|
#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>& collection,
|
|
const std::shared_ptr<Logger>& 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<RepoInfo> repo = testRepo();
|
|
if (repo.has_value()) {
|
|
std::optional<std::filesystem::path> path = downloadAsRepo(repo.value());
|
|
}
|
|
}
|
|
|
|
std::optional<nlohmann::json> 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<std::filesystem::path> Download::downloadAsRepo(const RepoInfo& repo) {
|
|
info("Trying Gitea v1 API");
|
|
std::optional<nlohmann::json> 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<std::filesystem::path> path = downloadRepoGiteaApi1(repo, branchName);
|
|
}
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<std::filesystem::path> 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<std::string_view>& 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<std::string_view>& 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<FILE, FileDeleter> 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<std::string_view>& 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<CURL, CurlDeleter>(crl);
|
|
}
|
|
|
|
|
|
std::optional<Download::RepoInfo> 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;
|
|
}
|
|
|
|
|