192 lines · 5.6 KB · Rust
RawDownload
use crate::{HttpClient, HttpRequestExt};use anyhow::{Context as _, Result, anyhow, bail};use futures::AsyncReadExt;use http::Request;use serde::Deserialize;use std::sync::Arc;use url::Url; const GITHUB_API_URL: &str = "https://api.github.com"; pub struct GitHubLspBinaryVersion {    pub name: String,    pub url: String,    pub digest: Option<String>,} #[derive(Deserialize, Debug)]pub struct GithubRelease {    pub tag_name: String,    #[serde(rename = "prerelease")]    pub pre_release: bool,    pub assets: Vec<GithubReleaseAsset>,    pub tarball_url: String,    pub zipball_url: String,} #[derive(Deserialize, Debug)]pub struct GithubReleaseAsset {    pub name: String,    pub browser_download_url: String,    pub digest: Option<String>,} pub async fn latest_github_release(    repo_name_with_owner: &str,    require_assets: bool,    pre_release: bool,    http: Arc<dyn HttpClient>,) -> anyhow::Result<GithubRelease> {    let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases");     let request = Request::get(&url)        .follow_redirects(crate::RedirectPolicy::FollowAll)        .when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {            builder.header("Authorization", format!("Bearer {}", token))        })        .body(Default::default())?;     let mut response = http        .send(request)        .await        .context("error fetching latest release")?;     let mut body = Vec::new();    response        .body_mut()        .read_to_end(&mut body)        .await        .context("error reading latest release")?;     if response.status().is_client_error() {        let text = String::from_utf8_lossy(body.as_slice());        bail!(            "status error {}, response: {text:?}",            response.status().as_u16()        );    }     let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {        Ok(releases) => releases,         Err(err) => {            log::error!("Error deserializing: {err:?}");            log::error!(                "GitHub API response text: {:?}",                String::from_utf8_lossy(body.as_slice())            );            anyhow::bail!("error deserializing latest release: {err:?}");        }    };     let mut release = releases        .into_iter()        .filter(|release| !require_assets || !release.assets.is_empty())        .find(|release| release.pre_release == pre_release)        .context("finding a prerelease")?;    release.assets.iter_mut().for_each(|asset| {        if let Some(digest) = &mut asset.digest            && let Some(stripped) = digest.strip_prefix("sha256:")        {            *digest = stripped.to_owned();        }    });    Ok(release)} pub async fn get_release_by_tag_name(    repo_name_with_owner: &str,    tag: &str,    http: Arc<dyn HttpClient>,) -> anyhow::Result<GithubRelease> {    let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases/tags/{tag}");     let request = Request::get(&url)        .follow_redirects(crate::RedirectPolicy::FollowAll)        .when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {            builder.header("Authorization", format!("Bearer {}", token))        })        .body(Default::default())?;     let mut response = http        .send(request)        .await        .context("error fetching latest release")?;     let mut body = Vec::new();    let status = response.status();    response        .body_mut()        .read_to_end(&mut body)        .await        .context("error reading latest release")?;     if status.is_client_error() {        let text = String::from_utf8_lossy(body.as_slice());        bail!(            "status error {}, response: {text:?}",            response.status().as_u16()        );    }     let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {        log::error!("Error deserializing: {err:?}");        log::error!(            "GitHub API response text: {:?}",            String::from_utf8_lossy(body.as_slice())        );        anyhow!("error deserializing GitHub release: {err:?}")    })?;     Ok(release)} #[derive(Debug, PartialEq, Eq, Clone, Copy)]pub enum AssetKind {    TarGz,    Gz,    Zip,} pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> Result<String> {    let mut url = Url::parse(&format!(        "https://github.com/{repo_name_with_owner}/archive/refs/tags",    ))?;    // We're pushing this here, because tags may contain `/` and other characters    // that need to be escaped.    let asset_filename = format!(        "{tag}.{extension}",        extension = match kind {            AssetKind::TarGz => "tar.gz",            AssetKind::Gz => "gz",            AssetKind::Zip => "zip",        }    );    url.path_segments_mut()        .map_err(|()| anyhow!("cannot modify url path segments"))?        .push(&asset_filename);    Ok(url.to_string())} #[cfg(test)]mod tests {    use crate::github::{AssetKind, build_asset_url};     #[test]    fn test_build_asset_url() {        let tag = "release/2.3.5";        let repo_name_with_owner = "microsoft/vscode-eslint";         let tarball = build_asset_url(repo_name_with_owner, tag, AssetKind::TarGz).unwrap();        assert_eq!(            tarball,            "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"        );         let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap();        assert_eq!(            zip,            "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip"        );    }}