From e4ba2af100db09f490412eb1b6ad7ffb1654d600 Mon Sep 17 00:00:00 2001 From: Rabindra Dhakal Date: Mon, 25 Nov 2024 20:15:19 +0545 Subject: [PATCH] feat(self): add self update --- src/cli.rs | 16 +++++++++ src/lib.rs | 65 ++++++++++++++++++++++++++++++++++--- src/misc/download/github.rs | 26 ++++++++------- src/misc/download/gitlab.rs | 24 +++++--------- src/misc/download/mod.rs | 2 +- 5 files changed, 102 insertions(+), 31 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index f0c6994..78c4938 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -26,6 +26,14 @@ pub struct Args { pub command: Commands, } +#[derive(Subcommand)] +pub enum SelfAction { + /// Update soar + Update, + /// Uninstall soar + Uninstall, +} + #[derive(Subcommand)] pub enum Commands { /// Install packages @@ -208,4 +216,12 @@ pub enum Commands { /// Build #[clap(name = "build")] Build { files: Vec }, + + /// Modify the soar installation + #[command(arg_required_else_help = true)] + #[clap(name = "self")] + SelfCmd { + #[clap(subcommand)] + action: SelfAction, + }, } diff --git a/src/lib.rs b/src/lib.rs index 2114b24..bb0779e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,14 @@ use anyhow::Result; use clap::Parser; -use cli::{Args, Commands}; -use misc::{download::download_and_save, health::check_health}; +use cli::{Args, Commands, SelfAction}; +use misc::{ + download::{download, download_and_save, github::fetch_github_releases, ApiType}, + health::check_health, +}; use package::build; use registry::PackageRegistry; -use tracing::{debug, error, trace, warn}; +use tokio::fs; +use tracing::{debug, error, info, trace, warn}; use core::{ color::{Color, ColorExt}, @@ -13,7 +17,11 @@ use core::{ log::setup_logging, util::{cleanup, print_env, setup_required_paths}, }; -use std::{env, io::Read, path::Path}; +use std::{ + env::{self, consts::ARCH}, + io::Read, + path::Path, +}; mod cli; pub mod core; @@ -23,6 +31,8 @@ mod registry; async fn handle_cli() -> Result<()> { let mut args = env::args().collect::>(); + let self_bin = args.get(0).unwrap().clone(); + let self_version = env!("CARGO_PKG_VERSION"); let mut i = 0; while i < args.len() { @@ -173,6 +183,53 @@ async fn handle_cli() -> Result<()> { build::init(&file).await?; } } + Commands::SelfCmd { action } => { + match action { + SelfAction::Update => { + let is_nightly = self_version.starts_with("nightly"); + let gh_releases = + fetch_github_releases(&ApiType::PkgForge, "pkgforge/soar").await?; + + let release = gh_releases.iter().find(|rel| { + if is_nightly { + return rel.name.starts_with("nightly") && rel.name != self_version; + } else { + rel.tag_name + .trim_start_matches('v') + .parse::() + .map(|v| v > self_version.parse::().unwrap()) + .unwrap_or(false) + } + }); + if let Some(release) = release { + let asset = release + .assets + .iter() + .find(|a| { + a.name.contains(ARCH) + && !a.name.contains("tar") + && !a.name.contains("sum") + }) + .unwrap(); + download(&asset.browser_download_url, Some(self_bin)).await?; + println!("Soar updated to {}", release.tag_name); + } else { + eprintln!("No updates found."); + } + } + SelfAction::Uninstall => { + match fs::remove_file(self_bin).await { + Ok(_) => { + info!("Soar has been uninstalled successfully."); + info!("You should remove soar config and data files manually."); + } + Err(err) => { + error!("{}\nFailed to uninstall soar.", err.to_string()); + } + }; + } + }; + } }; Ok(()) diff --git a/src/misc/download/github.rs b/src/misc/download/github.rs index 299e8b2..46ddcb2 100644 --- a/src/misc/download/github.rs +++ b/src/misc/download/github.rs @@ -20,19 +20,20 @@ use crate::{ use super::{should_fallback, ApiType}; #[derive(Clone, Debug, Deserialize, Serialize)] -struct GithubAsset { - name: String, - size: u64, - browser_download_url: String, +pub struct GithubAsset { + pub name: String, + pub size: u64, + pub browser_download_url: String, } #[derive(Debug, Deserialize, Serialize)] -struct GithubRelease { - tag_name: String, - draft: bool, - prerelease: bool, - published_at: String, - assets: Vec, +pub struct GithubRelease { + pub name: String, + pub tag_name: String, + pub draft: bool, + pub prerelease: bool, + pub published_at: String, + pub assets: Vec, } pub static GITHUB_URL_REGEX: &str = @@ -64,7 +65,10 @@ async fn call_github_api(gh_api: &ApiType, user_repo: &str) -> Result .context("Failed to fetch GitHub releases") } -async fn fetch_github_releases(gh_api: &ApiType, user_repo: &str) -> Result> { +pub async fn fetch_github_releases( + gh_api: &ApiType, + user_repo: &str, +) -> Result> { let response = match call_github_api(gh_api, user_repo).await { Ok(resp) => { let status = resp.status(); diff --git a/src/misc/download/gitlab.rs b/src/misc/download/gitlab.rs index fbb4608..88daffa 100644 --- a/src/misc/download/gitlab.rs +++ b/src/misc/download/gitlab.rs @@ -17,7 +17,7 @@ use crate::{ misc::download::download, }; -use super::should_fallback; +use super::{should_fallback, ApiType}; #[derive(Clone, Debug, Deserialize, Serialize)] struct GitlabAsset { @@ -41,25 +41,19 @@ struct GitlabRelease { pub static GITLAB_URL_REGEX: &str = r"^(?i)(?:https?://)?(?:gitlab(?:\.com)?[:/])([^/@]+/[^/@]+)(?:@([^/\s]*)?)?$"; -#[derive(Debug)] -enum GitlabApi { - PkgForge, - Gitlab, -} - -async fn call_gitlab_api(gh_api: &GitlabApi, user_repo: &str) -> Result { +async fn call_gitlab_api(gh_api: &ApiType, user_repo: &str) -> Result { let client = reqwest::Client::new(); let url = format!( "{}/api/v4/projects/{}/releases", match gh_api { - GitlabApi::PkgForge => "https://api.gl.pkgforge.dev", - GitlabApi::Gitlab => "https://gitlab.com", + ApiType::PkgForge => "https://api.gl.pkgforge.dev", + ApiType::Primary => "https://gitlab.com", }, user_repo.replace("/", "%2F") ); let mut headers = HeaderMap::new(); headers.insert(USER_AGENT, "pkgforge/soar".parse()?); - if matches!(gh_api, GitlabApi::Gitlab) { + if matches!(gh_api, ApiType::Primary) { if let Ok(token) = env::var("GITLAB_TOKEN") { trace!("Using Gitlab token: {}", token); headers.insert(AUTHORIZATION, format!("Bearer {}", token).parse()?); @@ -73,13 +67,13 @@ async fn call_gitlab_api(gh_api: &GitlabApi, user_repo: &str) -> Result Result> { +async fn fetch_gitlab_releases(gh_api: &ApiType, user_repo: &str) -> Result> { let response = match call_gitlab_api(gh_api, user_repo).await { Ok(resp) => { let status = resp.status(); - if should_fallback(status) && matches!(gh_api, GitlabApi::PkgForge) { + if should_fallback(status) && matches!(gh_api, ApiType::PkgForge) { debug!("Failed to fetch Gitlab asset using pkgforge API. Retrying request using Gitlab API."); - call_gitlab_api(&GitlabApi::Gitlab, user_repo).await? + call_gitlab_api(&ApiType::Primary, user_repo).await? } else { resp } @@ -138,7 +132,7 @@ pub async fn handle_gitlab_download( .filter(|&tag| !tag.is_empty()); info!("Fetching releases for {}...", user_repo); - let releases = fetch_gitlab_releases(&GitlabApi::PkgForge, user_repo).await?; + let releases = fetch_gitlab_releases(&ApiType::PkgForge, user_repo).await?; let release = if let Some(tag_name) = tag { releases diff --git a/src/misc/download/mod.rs b/src/misc/download/mod.rs index 2d1adae..46499d9 100644 --- a/src/misc/download/mod.rs +++ b/src/misc/download/mod.rs @@ -14,7 +14,7 @@ use tokio::{ }; use tracing::{error, info}; -mod github; +pub mod github; mod gitlab; use crate::{