From 1fe18c4d94cfc2d2bb7080be0de9922390c48898 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Mon, 23 Dec 2024 10:00:39 -0600 Subject: [PATCH] Add multi-site support to ubi crate --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/houseabsolute/ubi?shareId=XXXX-XXXX-XXXX-XXXX). --- Cargo.toml | 1 + ubi/src/forge.rs | 13 ++++++ ubi/src/{fetcher.rs => github.rs} | 9 ++-- ubi/src/gitlab.rs | 75 +++++++++++++++++++++++++++++++ ubi/src/lib.rs | 35 ++++++++++++--- 5 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 ubi/src/forge.rs rename ubi/src/{fetcher.rs => github.rs} (90%) create mode 100644 ubi/src/gitlab.rs diff --git a/Cargo.toml b/Cargo.toml index 940ccf8..83b2423 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,3 +45,4 @@ zip = { version = "2.2.1", default-features = false, features = [ "lzma", "zstd", ] } +async-trait = "0.1.50" diff --git a/ubi/src/forge.rs b/ubi/src/forge.rs new file mode 100644 index 0000000..224d6d5 --- /dev/null +++ b/ubi/src/forge.rs @@ -0,0 +1,13 @@ +use anyhow::Result; +use async_trait::async_trait; +use reqwest::Client; +use url::Url; + +use crate::release::{Asset, Release}; + +#[async_trait] +pub(crate) trait Forge { + fn new(project_name: String, tag: Option, url: Option, api_base: Option) -> Self; + async fn fetch_assets(&self, client: &Client) -> Result>; + async fn release_info(&self, client: &Client) -> Result; +} diff --git a/ubi/src/fetcher.rs b/ubi/src/github.rs similarity index 90% rename from ubi/src/fetcher.rs rename to ubi/src/github.rs index c8c5e89..e275a41 100644 --- a/ubi/src/fetcher.rs +++ b/ubi/src/github.rs @@ -1,5 +1,7 @@ +use crate::forge::Forge; use crate::release::{Asset, Release}; use anyhow::Result; +use async_trait::async_trait; use reqwest::{header::HeaderValue, header::ACCEPT, Client}; use url::Url; @@ -13,8 +15,9 @@ pub(crate) struct GitHubAssetFetcher { const GITHUB_API_BASE: &str = "https://api.github.com"; -impl GitHubAssetFetcher { - pub(crate) fn new( +#[async_trait] +impl Forge for GitHubAssetFetcher { + fn new( project_name: String, tag: Option, url: Option, @@ -28,7 +31,7 @@ impl GitHubAssetFetcher { } } - pub(crate) async fn fetch_assets(&self, client: &Client) -> Result> { + async fn fetch_assets(&self, client: &Client) -> Result> { if let Some(url) = &self.url { return Ok(vec![Asset { name: url.path().split('/').last().unwrap().to_string(), diff --git a/ubi/src/gitlab.rs b/ubi/src/gitlab.rs new file mode 100644 index 0000000..1d4ca13 --- /dev/null +++ b/ubi/src/gitlab.rs @@ -0,0 +1,75 @@ +use crate::forge::Forge; +use crate::release::{Asset, Release}; +use anyhow::Result; +use async_trait::async_trait; +use reqwest::{header::HeaderValue, header::ACCEPT, Client}; +use url::Url; + +#[derive(Debug)] +pub(crate) struct GitLabAssetFetcher { + project_name: String, + tag: Option, + url: Option, + gitlab_api_base: String, +} + +const GITLAB_API_BASE: &str = "https://gitlab.com/api/v4"; + +#[async_trait] +impl Forge for GitLabAssetFetcher { + fn new( + project_name: String, + tag: Option, + url: Option, + gitlab_api_base: Option, + ) -> Self { + Self { + project_name, + tag, + url, + gitlab_api_base: gitlab_api_base.unwrap_or(GITLAB_API_BASE.to_string()), + } + } + + async fn fetch_assets(&self, client: &Client) -> Result> { + if let Some(url) = &self.url { + return Ok(vec![Asset { + name: url.path().split('/').last().unwrap().to_string(), + url: url.clone(), + }]); + } + + Ok(self.release_info(client).await?.assets) + } + + async fn release_info(&self, client: &Client) -> Result { + let mut parts = self.project_name.split('/'); + let owner = parts.next().unwrap(); + let repo = parts.next().unwrap(); + + let url = match &self.tag { + Some(tag) => format!( + "{}/projects/{}/releases/{}", + self.gitlab_api_base, + urlencoding::encode(&format!("{}/{}", owner, repo)), + tag, + ), + None => format!( + "{}/projects/{}/releases", + self.gitlab_api_base, + urlencoding::encode(&format!("{}/{}", owner, repo)), + ), + }; + let req = client + .get(url) + .header(ACCEPT, HeaderValue::from_str("application/json")?) + .build()?; + let resp = client.execute(req).await?; + + if let Err(e) = resp.error_for_status_ref() { + return Err(anyhow::Error::new(e)); + } + + Ok(resp.json::().await?) + } +} diff --git a/ubi/src/lib.rs b/ubi/src/lib.rs index 1740182..8810e03 100644 --- a/ubi/src/lib.rs +++ b/ubi/src/lib.rs @@ -30,15 +30,17 @@ mod arch; mod extension; -mod fetcher; mod installer; mod os; mod picker; mod release; +mod forge; +mod github; +mod gitlab; use crate::{ - fetcher::GitHubAssetFetcher, installer::Installer, picker::AssetPicker, release::Asset, - release::Download, + installer::Installer, picker::AssetPicker, release::Asset, + release::Download, forge::Forge, github::GitHubAssetFetcher, gitlab::GitLabAssetFetcher, }; use anyhow::{anyhow, Result}; use log::debug; @@ -74,6 +76,7 @@ pub struct UbiBuilder<'a> { platform: Option<&'a Platform>, is_musl: Option, github_api_url_base: Option, + forge: Option, } impl<'a> UbiBuilder<'a> { @@ -174,6 +177,13 @@ impl<'a> UbiBuilder<'a> { self } + /// Set the forge type to use for fetching assets and release information. + #[must_use] + pub fn forge(mut self, forge: ForgeType) -> Self { + self.forge = Some(forge); + self + } + const TARGET: &'static str = env!("TARGET"); /// Builds a new [`Ubi`] instance and returns it. @@ -225,6 +235,7 @@ impl<'a> UbiBuilder<'a> { None => platform_is_musl(platform), }, self.github_api_url_base, + self.forge, ) } } @@ -251,7 +262,7 @@ fn platform_is_musl(platform: &Platform) -> bool { /// [`UbiBuilder`] struct to create a new `Ubi` instance. #[derive(Debug)] pub struct Ubi<'a> { - asset_fetcher: GitHubAssetFetcher, + asset_fetcher: Box, asset_picker: AssetPicker<'a>, installer: Installer, reqwest_client: Client, @@ -271,6 +282,7 @@ impl<'a> Ubi<'a> { platform: &'a Platform, is_musl: bool, github_api_url_base: Option, + forge: Option, ) -> Result> { let url = if let Some(u) = url { Some(Url::parse(u)?) @@ -280,13 +292,22 @@ impl<'a> Ubi<'a> { let project_name = Self::parse_project_name(project, url.as_ref())?; let exe = Self::exe_name(exe, &project_name, platform); let install_path = Self::install_path(install_dir, &exe)?; - Ok(Ubi { - asset_fetcher: GitHubAssetFetcher::new( + let asset_fetcher: Box = match forge { + Some(ForgeType::GitLab) => Box::new(GitLabAssetFetcher::new( project_name, tag.map(std::string::ToString::to_string), url, github_api_url_base, - ), + )), + _ => Box::new(GitHubAssetFetcher::new( + project_name, + tag.map(std::string::ToString::to_string), + url, + github_api_url_base, + )), + }; + Ok(Ubi { + asset_fetcher, asset_picker: AssetPicker::new(matching, platform, is_musl), installer: Installer::new(install_path, exe), reqwest_client: Self::reqwest_client(github_token)?,