rustinsight/
github.rs

1use crate::app_info::AppInfo;
2use crate::built_info;
3use crate::RI_USER_AGENT;
4use anyhow::{anyhow as err, Error};
5use futures::StreamExt;
6use indicatif::ProgressBar;
7use reqwest::header::{CONTENT_LENGTH, USER_AGENT};
8use reqwest::Client;
9use semver::Version;
10use serde::Deserialize;
11use tempfile::tempfile;
12use tokio::fs::File;
13use tokio::io::{AsyncSeekExt, AsyncWriteExt, SeekFrom};
14
15pub struct GitHubApi {
16    client: Client,
17}
18
19impl GitHubApi {
20    pub fn new() -> Self {
21        Self {
22            client: Client::new(),
23        }
24    }
25
26    pub async fn releases(&mut self, url: &str) -> Result<Vec<Release>, Error> {
27        let releases = self
28            .client
29            .get(url)
30            .header(USER_AGENT, RI_USER_AGENT)
31            .send()
32            .await?
33            .json()
34            .await?;
35        Ok(releases)
36    }
37
38    pub async fn latest_release(&mut self, app_info: &AppInfo) -> Result<Release, Error> {
39        let latest_release = self
40            .releases(&app_info.link)
41            .await?
42            .into_iter()
43            .next()
44            .ok_or_else(|| Error::msg("No releases available"))?;
45        Ok(latest_release)
46    }
47
48    pub async fn download_assets(&mut self, url: &str) -> Result<File, Error> {
49        let resp = self
50            .client
51            .get(url)
52            .header(USER_AGENT, RI_USER_AGENT)
53            .send()
54            .await?;
55        let total = resp
56            .headers()
57            .get(CONTENT_LENGTH)
58            .ok_or_else(|| Error::msg("Can't detect size of assets"))?
59            .to_str()?
60            .parse()?;
61        let mut chunks = resp.bytes_stream();
62        let mut archive = File::from_std(tempfile()?);
63        let bar = ProgressBar::new(total);
64        while let Some(chunk) = chunks.next().await.transpose()? {
65            bar.inc(chunk.len() as u64);
66            archive.write_all(&chunk).await?;
67        }
68        bar.finish();
69        archive.seek(SeekFrom::Start(0)).await?;
70        Ok(archive)
71    }
72}
73
74#[derive(Debug, Deserialize)]
75pub struct Release {
76    /// Title
77    pub name: String,
78    pub html_url: String,
79    /// IMPORTANT: `Tag` has to be a valid semver
80    #[serde(rename = "tag_name")]
81    pub version: Version,
82    pub assets: Vec<Asset>,
83}
84
85impl Release {
86    pub fn get_asset_for_os(&self, app_info: &AppInfo, os: &str) -> Result<&str, Error> {
87        let arch = built_info::CFG_TARGET_ARCH;
88        let ver = &self.version;
89        let name = &app_info.name;
90        let expected_asset = format!("{name}-{ver}-{os}-{arch}.tar.gz");
91        for asset in &self.assets {
92            if asset.name == expected_asset {
93                return Ok(&asset.browser_download_url);
94            }
95        }
96        Err(err!("Assets for '{os}' system was not found"))
97    }
98}
99
100#[derive(Debug, Deserialize)]
101pub struct Asset {
102    /// The name of the file.
103    pub name: String,
104    pub browser_download_url: String,
105}