diff --git a/express-install.sh b/express-install.sh new file mode 100644 index 0000000..0578f8d --- /dev/null +++ b/express-install.sh @@ -0,0 +1,79 @@ +#!/bin/bash +#set -x #uncomment for debug mode + +clear_term() { + #comment out for output + clear + #echo "debug -- clear" +} + +aborting_install() { + clear_term + echo "Aborting Install" +} + +path_check() { + path_check_var=$(which jhira) + if [ -x "$path_check_var" ] ; then + echo "JHIRA appears to be in your path! Continuing..." + sleep 2 + else + echo "JHIRA does not appear to be in your path," + echo "Would you like the installer to try to auto add JHIRA to your path? (y/n) " + read addToPath + if [[ $addToPath == "y" ]] ; then + touch ~/.zshrc + echo "export PATH=\"$PATH:/opt\"" >> ~/.zshrc + echo "Now we are going to refresh your terminal" + sleep 2 + source ~/.zshrc + else + echo "Skipping add to path step" + fi + fi +} + +exec_install() { + clear_term + echo "Beginning express download of JHIRA" + curl -L "https://github.com/wearejh/jhira/releases/download/v0.1.2/jhira" --output jhira-temp-binary-file + chmod +x ./jhira-temp-binary-file + echo "Download successful!" + echo "You may now be asked for your password to install the JHIRA binary" + sudo mkdir -p /opt + sudo chown -R $(whoami) /opt + mv ./jhira-temp-binary-file /opt/jhira + path_check + echo "Now the self-update function will run to get the latest version!" + sleep 2 + jhira self-update + echo "Thank you for using the express installer!" + echo "(You may need to run \"source ~/.zshrc\" - without the quotes - to see jhira)" +} + +clear_term +echo "Welcome to the JHIRA Express installer!" +echo "Would you like to install JHIRA? (y/n) " +read continueInstall +if [[ $continueInstall == "y" ]] ; then + echo "Ok, installing now" + path_to_executable=$(which jhira) + if [ -x "$path_to_executable" ] ; then + clear_term + echo "Looks like jhira is already installed" + echo "Would you like to reinstall?" + echo " - this will delete your existing installation (y/n) " + read reinstall + if [[ $reinstall == "y" ]] ; then + rm $(which jhira) + exec_install + else + aborting_install + fi + else + exec_install + fi +else + aborting_install +fi + diff --git a/jhira_core/Cargo.toml b/jhira_core/Cargo.toml index 325da9e..7282630 100644 --- a/jhira_core/Cargo.toml +++ b/jhira_core/Cargo.toml @@ -11,7 +11,7 @@ argh = "0.1.3" failure = "0.1.6" structopt = "0.3.9" chrono = "0.4.10" -reqwest = { version = "0.10.3", features = ["json"] } +reqwest = { version = "0.10.3", features = ["json", "blocking"] } serde = "1.0.104" serde_json = "1.0.48" serde_derive = "1.0.104" diff --git a/jhira_core/src/jhira.rs b/jhira_core/src/jhira.rs index a56c14d..cd73ed8 100644 --- a/jhira_core/src/jhira.rs +++ b/jhira_core/src/jhira.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use crate::epic::EpicCmd; use crate::jql::JqlCmd; +use crate::self_update; #[derive(Debug)] pub struct Jhira { @@ -61,6 +62,13 @@ pub enum Subcommands { #[structopt(subcommand)] cmd: Worklog, }, + /// Update to the latest version + #[structopt(name = "self-update", alias = "update")] + SelfUpdate { + /// Accept all prompts and update automatically + #[structopt(long = "yes", short = "y")] + yes: bool, + }, } #[derive(Debug, Fail)] @@ -106,6 +114,10 @@ impl Jhira { let auth = Auth { api, domain, email }; auth.login() } + SelfUpdate { yes } => { + let self_update = self_update::SelfUpdate { yes }; + self_update.into() + } }?; Ok((opt2, upcoming)) } diff --git a/jhira_core/src/lib.rs b/jhira_core/src/lib.rs index ed68c08..28fc61c 100644 --- a/jhira_core/src/lib.rs +++ b/jhira_core/src/lib.rs @@ -21,6 +21,7 @@ pub mod issues; pub mod jhira; pub mod jql; pub mod login; +pub mod self_update; pub mod task; pub mod worklog; diff --git a/jhira_core/src/self_update/mod.rs b/jhira_core/src/self_update/mod.rs new file mode 100644 index 0000000..270f679 --- /dev/null +++ b/jhira_core/src/self_update/mod.rs @@ -0,0 +1,186 @@ +use crate::async_task::{AsyncTask, Return, TaskOutput}; +use crate::task::TaskSequence; +use ansi_term::Colour::{Blue, Green, Red}; +use async_trait::async_trait; +use std::fs::File; +use std::process::Command; +use std::{env, io}; + +#[derive(Debug, Clone)] +pub struct SelfUpdate { + pub yes: bool, +} + +impl From for TaskSequence { + fn from(self_update: SelfUpdate) -> Self { + Ok(vec![Box::new(self_update)]) + } +} + +#[async_trait(?Send)] +impl AsyncTask for SelfUpdate { + async fn exec(&self) -> Return { + let _output = run_self_update(self.yes).await?; + Ok(TaskOutput::Done) + } +} + +#[derive(Debug, Fail)] +enum SelfUpdateError { + #[fail(display = "Cannot read path to executable")] + PermissionDenied, + #[fail(display = "Assets contained no items")] + NoItems, +} + +#[derive(Serialize, Deserialize, Debug)] +struct JhiraJson { + assets: Vec, + name: String, + tag_name: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct JhiraJsonAsset { + browser_download_url: String, + size: i32, + name: String, +} + +pub async fn run_self_update(is_auto_confirmed: bool) -> Result<(), failure::Error> { + let request_url = String::from("https://api.github.com/repos/wearejh/jhira/releases/latest"); + let response = reqwest::get(&request_url).await?; + let resp = response.text().await?; + + let jhira_path_cmd = env::current_exe()?; + + let jhira_path = jhira_path_cmd + .to_str() + .ok_or(SelfUpdateError::PermissionDenied)?; + + let jhira: JhiraJson = serde_json::from_str(&resp)?; + let url = jhira + .assets + .get(0) + .map(|asset| asset.browser_download_url.clone()) + .ok_or(SelfUpdateError::NoItems)?; + + let name = jhira + .assets + .get(0) + .map(|asset| asset.name.clone()) + .ok_or(SelfUpdateError::NoItems)?; + + let size = jhira + .assets + .get(0) + .map(|asset| asset.size) + .ok_or(SelfUpdateError::NoItems)?; + + clear_terminal(is_auto_confirmed); + let mut ok_to_proceed: bool = false; + if !is_auto_confirmed { + println!("{}", Green.paint("=====[Jhira Self Updater]=====")); + println!(); + println!("File name : {}", name); + println!("Description : {}", jhira.name); + println!("Url : {}", url); + println!("Version : {}", jhira.tag_name); + println!("Size : {}kb", size / 1024); + println!(); + println!( + "Current jhira directory is reported as: {}", + Blue.paint(jhira_path) + ); + println!(); + if jhira_path != "/opt/jhira" { + println!( + "{}", + Red.paint("Warning! Working directory is NOT the standard directory expected.") + ); + println!("{}", Red.paint("Expected directory to be /opt/jhira")); + println!( + "{}", + Red.paint("You can proceed with the update, but at your own risk!") + ); + println!(); + println!( + "{} {} {}", + Blue.paint("If you wish to fix this, exit out of this app and run 'sudo mv"), + Blue.paint(jhira_path), + Blue.paint("/opt/jhira'") + ); + println!( + "{}", + Blue.paint("More info here: https://github.com/WeareJH/jhira#manual") + ); + } else { + println!("{}", Green.paint("Working directory is ok!")); + } + println!(); + + loop { + println!("Ok to proceed? (y/n)"); + let mut user_input = String::new(); + + io::stdin() + .read_line(&mut user_input) + .expect("Failed to read line"); + + if let Some('\n') = user_input.chars().next_back() { + user_input.pop(); + } + if let Some('\r') = user_input.chars().next_back() { + user_input.pop(); + } + if user_input == "y" || user_input == "yes" { + ok_to_proceed = true; + break; + } else if user_input == "n" || user_input == "no" { + break; + } else { + clear_terminal(is_auto_confirmed); + println!("Unrecognised input: '{}'", user_input); + } + } + } else { + println!("Auto confirm flag passed, continuing..."); + ok_to_proceed = true; + } + + if ok_to_proceed { + clear_terminal(is_auto_confirmed); + println!("Starting update..."); + + let mut response = reqwest::blocking::get("https://www.rust-lang.org/")?; + + let current_path = std::path::PathBuf::from(jhira_path); + let mut current_dir = File::create(current_path)?; + + println!("Attempting to copy to {}", jhira_path); + + response.copy_to(&mut current_dir)?; + + clear_terminal(is_auto_confirmed); + let version = Command::new(jhira_path) + .arg("-V") + .output() + .expect("failed to execute process"); + println!("Success!"); + println!( + "You updated to {}", + std::str::from_utf8(&version.stdout).unwrap() + ); + } else { + clear_terminal(is_auto_confirmed); + println!("Aborted update"); + } + + Ok(()) +} + +fn clear_terminal(is_auto_confirmed: bool) { + if !is_auto_confirmed { + print!("{}[2J", 27 as char); + } +} diff --git a/pre-push.sh b/pre-push.sh index ac4f8e0..80cced7 100644 --- a/pre-push.sh +++ b/pre-push.sh @@ -1,5 +1,5 @@ set -eo pipefail -cargo fix --allow-dirty --allow-staged && cargo fmt +cargo fix --allow-dirty --allow-staged && cargo fmt --all cargo clippy cargo check cargo test