LCOV - code coverage report
Current view: top level - src/database - GitHubDownloader.cpp (source / functions) Hit Total Coverage
Test: coverage.info Lines: 24 60 40.0 %
Date: 2025-04-29 15:56:08 Functions: 5 7 71.4 %

          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           8 : GitHubDownloader::GitHubDownloader() : client(std::make_unique<httplib::SSLClient>(host)) {
      18           8 :     std::filesystem::path configdir = paths::get_config_directory();
      19           8 :     if (!std::filesystem::exists(configdir)) {
      20           0 :         std::filesystem::create_directories(configdir);
      21           8 :     } 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           8 :     std::filesystem::path cert_path = configdir / "ca-bundle.crt";
      28           8 :     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           8 :     client->set_follow_location(true);
      38           8 :     client->set_connection_timeout(5, 0); // seconds
      39           8 :     client->set_read_timeout(60, 0);      // seconds
      40           8 :     client->set_write_timeout(1, 0);      // seconds
      41           8 :     client->set_ca_cert_path(cert_path.string());
      42           8 : }
      43             : 
      44          13 : 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

Generated by: LCOV version 1.16