Line data Source code
1 : // SPDX-FileCopyrightText: 2025 Pairinteraction Developers 2 : // SPDX-License-Identifier: LGPL-3.0-or-later 3 : 4 : #include "pairinteraction/database/GitHubDownloader.hpp" 5 : 6 : #include "pairinteraction/utils/paths.hpp" 7 : 8 : #include <filesystem> 9 : #include <fmt/core.h> 10 : #include <fstream> 11 : #include <future> 12 : #include <httplib.h> 13 : #include <stdexcept> 14 : 15 : namespace pairinteraction { 16 : 17 5 : GitHubDownloader::GitHubDownloader() : client(std::make_unique<httplib::SSLClient>(host)) { 18 5 : std::filesystem::path configdir = paths::get_config_directory(); 19 5 : if (!std::filesystem::exists(configdir)) { 20 0 : std::filesystem::create_directories(configdir); 21 5 : } else if (!std::filesystem::is_directory(configdir)) { 22 0 : throw std::filesystem::filesystem_error("Cannot access config directory ", 23 0 : configdir.string(), 24 0 : std::make_error_code(std::errc::not_a_directory)); 25 : } 26 : 27 5 : std::filesystem::path cert_path = configdir / "ca-bundle.crt"; 28 5 : if (!std::filesystem::exists(cert_path)) { 29 1 : std::ofstream out(cert_path); 30 1 : if (!out) { 31 0 : throw std::runtime_error("Failed to create certificate file at " + cert_path.string()); 32 : } 33 1 : out << cert; 34 1 : out.close(); 35 1 : } 36 : 37 5 : client->set_follow_location(true); 38 5 : client->set_connection_timeout(5, 0); // seconds 39 5 : client->set_read_timeout(60, 0); // seconds 40 5 : client->set_write_timeout(1, 0); // seconds 41 5 : client->set_ca_cert_path(cert_path.string()); 42 5 : } 43 : 44 7 : GitHubDownloader::~GitHubDownloader() = default; 45 : 46 : std::future<GitHubDownloader::Result> 47 0 : GitHubDownloader::download(const std::string &remote_url, const std::string &if_modified_since, 48 : bool use_octet_stream) const { 49 : return std::async( 50 0 : std::launch::async, [this, remote_url, if_modified_since, use_octet_stream]() -> Result { 51 : // Prepare headers 52 : httplib::Headers headers{ 53 : {"X-GitHub-Api-Version", "2022-11-28"}, 54 : {"Accept", 55 0 : use_octet_stream ? "application/octet-stream" : "application/vnd.github+json"}}; 56 : 57 0 : if (!if_modified_since.empty()) { 58 0 : headers.emplace("if-modified-since", if_modified_since); 59 : } 60 : 61 : // Use the GitHub token if available; otherwise, if we have a conditional request, 62 : // insert a dummy authorization header to avoid increasing rate limits 63 0 : if (auto *token = std::getenv("GITHUB_TOKEN"); token) { 64 0 : headers.emplace("Authorization", fmt::format("Bearer {}", token)); 65 0 : } else if (!if_modified_since.empty()) { 66 0 : headers.emplace("Authorization", 67 : "avoids-an-increase-in-ratelimits-used-if-304-is-returned"); 68 : } 69 : 70 0 : auto response = client->Get(remote_url, headers); 71 : 72 : // Handle if the response is null 73 0 : if (!response) { 74 : // Defensive handling: if response is null and the error is unknown, 75 : // treat this as a 304 Not Modified 76 0 : if (response.error() == httplib::Error::Unknown) { 77 0 : return Result{304, "", "", {}}; 78 : } 79 0 : throw std::runtime_error(fmt::format("Error downloading '{}': {}", remote_url, 80 0 : httplib::to_string(response.error()))); 81 : } 82 : 83 : // Parse the response 84 0 : Result result; 85 0 : if (response->has_header("x-ratelimit-remaining")) { 86 0 : result.rate_limit.remaining = 87 0 : std::stoi(response->get_header_value("x-ratelimit-remaining")); 88 : } 89 0 : if (response->has_header("x-ratelimit-reset")) { 90 0 : result.rate_limit.reset_time = 91 0 : std::stoi(response->get_header_value("x-ratelimit-reset")); 92 : } 93 0 : if (response->has_header("last-modified")) { 94 0 : result.last_modified = response->get_header_value("last-modified"); 95 : } 96 0 : result.body = response->body; 97 0 : result.status_code = response->status; 98 0 : return result; 99 0 : }); 100 0 : } 101 : 102 1 : GitHubDownloader::RateLimit GitHubDownloader::get_rate_limit() const { 103 : // This call now either returns valid rate limit data or throws an exception on error 104 1 : Result result = download("/rate_limit", "", false).get(); 105 1 : if (result.status_code != 200) { 106 0 : throw std::runtime_error( 107 0 : fmt::format("Failed obtaining the rate limit: status code {}.", result.status_code)); 108 : } 109 1 : return result.rate_limit; 110 1 : } 111 : 112 1 : std::string GitHubDownloader::get_host() const { return "https://" + host; } 113 : 114 : } // namespace pairinteraction