Skip to main content

modde_sources/github/
mod.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use anyhow::Result;
5use reqwest::Client;
6use serde::Deserialize;
7use tracing::info;
8
9use modde_core::manifest::wabbajack::DownloadDirective;
10
11use crate::common::{simple_download, with_retry};
12use crate::traits::{DownloadHandle, DownloadSource, ProgressCallback, VerifiedFile};
13
14/// GitHub Releases download source.
15pub struct GitHubSource {
16    client: Client,
17    token: Option<String>,
18}
19
20#[derive(Debug, Deserialize)]
21struct Release {
22    assets: Vec<ReleaseAsset>,
23}
24
25#[derive(Debug, Deserialize)]
26struct ReleaseAsset {
27    name: String,
28    browser_download_url: String,
29    size: u64,
30}
31
32impl GitHubSource {
33    pub fn new(client: Client) -> Self {
34        let token = std::env::var("GITHUB_TOKEN").ok();
35        Self { client, token }
36    }
37}
38
39impl DownloadSource for GitHubSource {
40    fn can_handle(&self, directive: &DownloadDirective) -> bool {
41        matches!(directive, DownloadDirective::GitHub { .. })
42    }
43
44    async fn resolve(&self, directive: &DownloadDirective) -> Result<DownloadHandle> {
45        let DownloadDirective::GitHub {
46            user,
47            repo,
48            tag,
49            asset,
50            hash,
51        } = directive
52        else {
53            anyhow::bail!("not a GitHub directive");
54        };
55
56        let url = format!(
57            "https://api.github.com/repos/{user}/{repo}/releases/tags/{tag}"
58        );
59
60        let mut req = self.client.get(&url).header("User-Agent", "modde");
61        if let Some(token) = &self.token {
62            req = req.header("Authorization", format!("Bearer {token}"));
63        }
64
65        let release: Release = req.send().await?.error_for_status()?.json().await?;
66
67        let found = release
68            .assets
69            .iter()
70            .find(|a| a.name == *asset)
71            .ok_or_else(|| anyhow::anyhow!("asset '{asset}' not found in release {tag}"))?;
72
73        info!(repo = %format!("{user}/{repo}"), tag, asset, "resolved GitHub release asset");
74
75        Ok(DownloadHandle {
76            url: found.browser_download_url.clone(),
77            headers: HashMap::new(),
78            expected_hash: *hash,
79            size_hint: Some(found.size),
80        })
81    }
82
83    async fn download_with_progress(
84        &self,
85        handle: DownloadHandle,
86        dest: &Path,
87        progress: ProgressCallback,
88    ) -> Result<VerifiedFile> {
89        let client = self.client.clone();
90        let handle_ref = &handle;
91        let dest_ref = dest;
92        let progress_ref = &progress;
93
94        with_retry("GitHub download", || async {
95            simple_download(&client, handle_ref, dest_ref, progress_ref).await
96        })
97        .await
98    }
99}