From 6cc40e78764661e2d24f96e7c2d6d0ef0f3216ea Mon Sep 17 00:00:00 2001 From: Tom Lawton Date: Tue, 20 Feb 2024 22:39:28 +0000 Subject: [PATCH 1/2] feat: self update `sprout update` --- Cargo.lock | 259 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 +- src/cli/clap.rs | 2 + src/cli/commands.rs | 93 +++++++++++++++- src/engine.rs | 57 ++++++++++ 5 files changed, 412 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23f6d93..3be9e4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -583,6 +583,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam" version = "0.8.4" @@ -664,6 +673,34 @@ dependencies = [ "cipher", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + [[package]] name = "darling" version = "0.14.4" @@ -915,6 +952,31 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + [[package]] name = "edit" version = "0.1.5" @@ -1010,6 +1072,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fiat-crypto" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" + [[package]] name = "filetime" version = "0.2.23" @@ -1028,6 +1096,16 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a7e408202050813e6f1d9addadcaafef3dca7530c7ddfb005d4081cce6779" +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -1043,6 +1121,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1355,6 +1448,19 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -1652,6 +1758,24 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nix" version = "0.26.4" @@ -1919,12 +2043,50 @@ dependencies = [ "vec-strings", ] +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.49", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae94056a791d0e1217d18b6cbdccb02c61e3054fc69893607f4067e3bb0b1fd1" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2093,6 +2255,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "platforms" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" + [[package]] name = "poly1305" version = "0.8.0" @@ -2177,6 +2345,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.30.0" @@ -2395,10 +2572,12 @@ dependencies = [ "http-body", "hyper", "hyper-rustls", + "hyper-tls", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -2411,6 +2590,7 @@ dependencies = [ "sync_wrapper", "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls", "tokio-util", "tower-service", @@ -2720,6 +2900,40 @@ dependencies = [ "libc", ] +[[package]] +name = "self-replace" +version = "1.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525db198616b2bcd0f245daf7bfd8130222f7ee6af9ff9984c19a61bf1160c55" +dependencies = [ + "fastrand 1.9.0", + "tempfile", + "windows-sys 0.48.0", +] + +[[package]] +name = "self_update" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a34ad8e4a86884ab42e9b8690e9343abdcfe5fa38a0318cfe1565ba9ad437b4" +dependencies = [ + "either", + "flate2", + "hyper", + "indicatif", + "log", + "quick-xml 0.23.1", + "regex", + "reqwest", + "self-replace", + "semver", + "serde_json", + "tar", + "tempfile", + "urlencoding", + "zipsign-api", +] + [[package]] name = "semver" version = "1.0.22" @@ -2973,6 +3187,7 @@ dependencies = [ "regex", "rustic_backend", "rustic_core", + "self_update", "serde", "serde_json", "serde_yaml", @@ -3105,6 +3320,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.10.0" @@ -3262,6 +3488,16 @@ dependencies = [ "syn 2.0.49", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-pipe" version = "0.2.12" @@ -3425,6 +3661,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8parse" version = "0.2.1" @@ -3441,6 +3683,12 @@ dependencies = [ "serde", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec-strings" version = "0.4.8" @@ -3871,6 +4119,17 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +[[package]] +name = "zipsign-api" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ba5aa1827d6b1a35a29b3413ec69ce5f796e4d897e3e5b38f461bef41d225ea" +dependencies = [ + "base64", + "ed25519-dalek", + "thiserror", +] + [[package]] name = "zstd" version = "0.13.0" diff --git a/Cargo.toml b/Cargo.toml index c17c375..36313d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ passwords = "3.1.16" regex = "1.10.3" rustic_backend = "0.1.1" rustic_core = "0.2.0" +self_update = { version = "0.39.0", features = ["archive-tar", "compression-flate2"] } serde = "1.0.196" serde_json = "1.0.113" serde_yaml = "0.9.31" @@ -64,4 +65,4 @@ overflow-checks = true lto = false incremental = true codegen-units = 256 -rpath = false \ No newline at end of file +rpath = false diff --git a/src/cli/clap.rs b/src/cli/clap.rs index 4bea944..f83cb77 100644 --- a/src/cli/clap.rs +++ b/src/cli/clap.rs @@ -35,6 +35,8 @@ pub enum SubCommand { Stash(StashArgs), /// List available remote snapshots Ls, + /// Update Sprout to latest release + Update, } #[derive(Args, Debug)] diff --git a/src/cli/commands.rs b/src/cli/commands.rs index aa39707..5ca2633 100644 --- a/src/cli/commands.rs +++ b/src/cli/commands.rs @@ -7,7 +7,8 @@ use log::{info, warn}; use passwords::PasswordGenerator; use rustic_backend::BackendOptions; use rustic_core::{ConfigOptions, Id, KeyOptions, Progress, ProgressBars, RepositoryOptions}; -use std::io::Write; +use self_update::cargo_crate_version; +use std::{io::Write, time::SystemTime}; use crate::{ cli::clap::{CliResponse, Options, RepoCommand, StashCommand, SubCommand}, @@ -18,6 +19,7 @@ use crate::{ repo::{definition::RepositoryDefinition, ProjectRepository}, stash::Stash, theme::CliTheme, + CFG_OS, CFG_TARGET_ARCH, }; /// The main entrypoint for our CLI. Returns a CliResponse in the result @@ -91,6 +93,20 @@ pub fn run(engine: &Engine) -> anyhow::Result { let sprout_home = engine.get_home(); engine.ensure_home()?; + match engine.get_update_version() { + Ok(version) => { + if let Some(version) = version { + warn!( + "An update is available - {}. Run `sprout update` to update.", + version.bold().green() + ) + } + } + Err(e) => { + warn!("Could not check for updates: {}", e); + } + }; + std::env::set_current_dir(&options.path).map_err(|_| { anyhow::anyhow!( "Unable to set path to {}. Does it exist?", @@ -505,5 +521,80 @@ pub fn run(engine: &Engine) -> anyhow::Result { } }, }, + SubCommand::Update => { + let current_version = cargo_crate_version!(); + + info!("Checking for updates..."); + + let status = self_update::backends::github::Update::configure() + .repo_owner("talss89") + .repo_name("sprout") + .bin_name("sprout") + .target(&format!("{}-{}", CFG_OS, CFG_TARGET_ARCH)) + .current_version(current_version) + .no_confirm(true) + .show_download_progress(true) + .show_output(false) + .set_progress_style("{spinner:^9.green} [{elapsed_precise:}] {wide_bar:.green/cyan.dim} {bytes:.bold}/{total_bytes:} ({eta:})".to_string(), "▰▶▱".to_string()) + .build()?; + + let latest = status.get_latest_release()?; + + if latest.version == current_version { + return Ok(CliResponse { + msg: "Already up to date".to_string(), + data: None, + }); + } + + info!("Upgrading to {}...", latest.version.bold().green()); + + if !self_update::version::bump_is_compatible(current_version, &latest.version)? { + warn!("{}", format!("{:-^72}", "Warning!").red().bold()); + warn!( + "{}", + "This update may contain breaking changes. Please read the release notes." + .yellow() + .bold() + ); + warn!("{}", format!("{:-^72}", "").red().bold()); + } + + let confirmation = Confirm::with_theme(&CliTheme::default()) + .with_prompt(format!( + "This will upgrade Sprout from {} to {} Do you want to continue?", + current_version.dimmed(), + latest.version.green().bold() + )) + .interact() + .unwrap(); + + if !confirmation { + return Ok(CliResponse { + msg: "User aborted update".to_string(), + data: None, + }); + } + + info!("Starting update..."); + + let msg = match status.update()? { + self_update::Status::UpToDate(_) => "Already up to date!".to_string(), + self_update::Status::Updated(version) => { + format!("Updated to version {}", version.italic()) + } + }; + + info!("Update complete."); + + let mut config = engine.get_config()?; + + config.last_update_check = SystemTime::now(); + config.update_available = None; + + engine.write_config(&config)?; + + Ok(CliResponse { msg, data: None }) + } } } diff --git a/src/engine.rs b/src/engine.rs index 5c90767..8849ed3 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,16 +1,28 @@ +use std::time::SystemTime; use std::{borrow::Cow, env, fs, path::PathBuf}; use log::info; +use self_update::cargo_crate_version; use serde::{Deserialize, Serialize}; use regex::Captures; use regex::Regex; +use crate::{CFG_OS, CFG_TARGET_ARCH}; + +fn unix_epoch() -> SystemTime { + SystemTime::UNIX_EPOCH +} + /// Describes the sprout-config.yaml file, which stores information on how the current user has configured Sprout. #[derive(Serialize, Deserialize)] pub struct SproutConfig { pub stash_key: String, pub default_repo: String, + #[serde(default = "unix_epoch")] + pub last_update_check: SystemTime, + #[serde(default)] + pub update_available: Option, } /// Represents core Sprout state and helper functions @@ -51,6 +63,8 @@ impl Engine { self.write_config(&SproutConfig { stash_key: "".to_string(), default_repo: "".to_string(), + last_update_check: SystemTime::UNIX_EPOCH, // We haven't ever checked! + update_available: None, })?; } @@ -69,6 +83,49 @@ impl Engine { serde_yaml::to_string(config)?, )?) } + + pub fn should_check_for_updates(&self) -> anyhow::Result { + let config = self.get_config()?; + + if config.last_update_check.elapsed().unwrap().as_secs() >= 86400 { + Ok(true) + } else { + Ok(false) + } + } + + pub fn check_for_updates(&self) -> anyhow::Result> { + let releases = self_update::backends::github::ReleaseList::configure() + .repo_owner("talss89") + .repo_name("sprout") + .with_target(&format!("{}-{}", CFG_OS, CFG_TARGET_ARCH)) + .build()? + .fetch()?; + + Ok(match releases.first() { + Some(release) => { + if self_update::version::bump_is_greater(cargo_crate_version!(), &release.version)? + { + Some(release.version.clone()) + } else { + None + } + } + None => None, + }) + } + + pub fn get_update_version(&self) -> anyhow::Result> { + let mut config = self.get_config()?; + + if self.should_check_for_updates()? { + config.update_available = self.check_for_updates()?; + config.last_update_check = SystemTime::now(); + self.write_config(&config)?; + } + + Ok(config.update_available) + } } // (c) Joe_Jingyu - https://stackoverflow.com/questions/62888154/rust-load-environment-variables-into-log4rs-yml-file From 83133bb4924e1da4183ee657cc1af60812e4a275 Mon Sep 17 00:00:00 2001 From: Tom Lawton Date: Tue, 20 Feb 2024 23:17:14 +0000 Subject: [PATCH 2/2] minor: update ui improvements --- src/cli/commands.rs | 49 ++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/cli/commands.rs b/src/cli/commands.rs index 5ca2633..0f697e2 100644 --- a/src/cli/commands.rs +++ b/src/cli/commands.rs @@ -27,26 +27,50 @@ use crate::{ pub fn run(engine: &Engine) -> anyhow::Result { let options = Options::parse(); + let sprout_home = engine.get_home(); + engine.ensure_home()?; + let logo = format!( - r" + r" +++++ ++ ++ +++++ +++++ +++++ {} ++ ++ + ++ ++ {} ++++++ + ++++++ {} - +++ + +++ + +++ + +++ +++++++ ++ ++ +++++ ", - format!("Sprout {}", crate::PKG_VERSION).bold().green(), + format!( + "{} {}", + format!("Sprout {}", crate::PKG_VERSION).bold().green(), + match engine.get_update_version() { + Ok(version) => { + if let Some(_) = version { + "update available via `sprout update`" + .italic() + .yellow() + .dimmed() + } else { + "".to_string().normal() + } + } + Err(e) => { + format!("Could not check for updates: {}", e) + .red() + .italic() + .dimmed() + } + } + ), "Content and database seeding for WordPress" .white() .bold() .dimmed(), format!("{} | https://github.com/talss89/sprout", crate::TARGET) .white() - .dimmed(), + .dimmed() ); eprintln!("{:^26}", logo.green()); @@ -90,23 +114,6 @@ pub fn run(engine: &Engine) -> anyhow::Result { }) .init(); - let sprout_home = engine.get_home(); - engine.ensure_home()?; - - match engine.get_update_version() { - Ok(version) => { - if let Some(version) = version { - warn!( - "An update is available - {}. Run `sprout update` to update.", - version.bold().green() - ) - } - } - Err(e) => { - warn!("Could not check for updates: {}", e); - } - }; - std::env::set_current_dir(&options.path).map_err(|_| { anyhow::anyhow!( "Unable to set path to {}. Does it exist?",