From f03f6e9ad74d8e1cf1fa33dc2c0c7c5dd7ae5c94 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Fri, 14 Oct 2022 10:59:21 +0100 Subject: Add automatic update checking (#555) * Add automatic update checking * Add setting to opt out of update checks * Document options * no * no * also no * Make clippy happy * Update atuin-client/src/settings.rs Co-authored-by: Conrad Ludgate * fix features Co-authored-by: Conrad Ludgate Co-authored-by: Conrad Ludgate --- atuin-client/src/settings.rs | 121 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 10 deletions(-) (limited to 'atuin-client/src/settings.rs') diff --git a/atuin-client/src/settings.rs b/atuin-client/src/settings.rs index f836ce02..b743a154 100644 --- a/atuin-client/src/settings.rs +++ b/atuin-client/src/settings.rs @@ -8,9 +8,13 @@ use config::{Config, Environment, File as ConfigFile, FileFormat}; use eyre::{eyre, Context, Result}; use fs_err::{create_dir_all, File}; use parse_duration::parse; +use semver::Version; use serde::Deserialize; pub const HISTORY_PAGE_SIZE: i64 = 100; +pub const LAST_SYNC_FILENAME: &str = "last_sync_time"; +pub const LAST_VERSION_CHECK_FILENAME: &str = "last_version_check_time"; +pub const LATEST_VERSION_FILENAME: &str = "latest_version"; #[derive(Clone, Debug, Deserialize, Copy)] pub enum SearchMode { @@ -86,6 +90,7 @@ pub struct Settings { pub dialect: Dialect, pub style: Style, pub auto_sync: bool, + pub update_check: bool, pub sync_address: String, pub sync_frequency: String, pub db_path: String, @@ -99,31 +104,65 @@ pub struct Settings { } impl Settings { - pub fn save_sync_time() -> Result<()> { + fn save_to_data_dir(filename: &str, value: &str) -> Result<()> { let data_dir = atuin_common::utils::data_dir(); let data_dir = data_dir.as_path(); - let sync_time_path = data_dir.join("last_sync_time"); + let path = data_dir.join(filename); - fs_err::write(sync_time_path, Utc::now().to_rfc3339())?; + fs_err::write(path, value)?; Ok(()) } - pub fn last_sync() -> Result> { + fn read_from_data_dir(filename: &str) -> Option { let data_dir = atuin_common::utils::data_dir(); let data_dir = data_dir.as_path(); - let sync_time_path = data_dir.join("last_sync_time"); + let path = data_dir.join(filename); + + if !path.exists() { + return None; + } + + let value = fs_err::read_to_string(path); + + value.ok() + } + + fn save_current_time(filename: &str) -> Result<()> { + Settings::save_to_data_dir(filename, Utc::now().to_rfc3339().as_str())?; + + Ok(()) + } + + fn load_time_from_file(filename: &str) -> Result> { + let value = Settings::read_from_data_dir(filename); + + match value { + Some(v) => { + let time = chrono::DateTime::parse_from_rfc3339(v.as_str())?; - if !sync_time_path.exists() { - return Ok(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)); + Ok(time.with_timezone(&Utc)) + } + None => Ok(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), } + } - let time = fs_err::read_to_string(sync_time_path)?; - let time = chrono::DateTime::parse_from_rfc3339(time.as_str())?; + pub fn save_sync_time() -> Result<()> { + Settings::save_current_time(LAST_SYNC_FILENAME) + } + + pub fn save_version_check_time() -> Result<()> { + Settings::save_current_time(LAST_VERSION_CHECK_FILENAME) + } + + pub fn last_sync() -> Result> { + Settings::load_time_from_file(LAST_SYNC_FILENAME) + } - Ok(time.with_timezone(&Utc)) + pub fn last_version_check() -> Result> { + Settings::load_time_from_file(LAST_VERSION_CHECK_FILENAME) } pub fn should_sync(&self) -> Result { @@ -142,6 +181,67 @@ impl Settings { } } + fn needs_update_check(&self) -> Result { + let last_check = Settings::last_version_check()?; + let diff = Utc::now() - last_check; + + // Check a max of once per hour + Ok(diff.num_hours() >= 1) + } + + async fn latest_version(&self) -> Result { + // Default to the current version, and if that doesn't parse, a version so high it's unlikely to ever + // suggest upgrading. + let current = + Version::parse(env!("CARGO_PKG_VERSION")).unwrap_or(Version::new(100000, 0, 0)); + + if !self.needs_update_check()? { + // Worst case, we don't want Atuin to fail to start because something funky is going on with + // version checking. + let version = match Settings::read_from_data_dir(LATEST_VERSION_FILENAME) { + Some(v) => Version::parse(&v).unwrap_or(current), + None => current, + }; + + return Ok(version); + } + + #[cfg(feature = "sync")] + let latest = crate::api_client::latest_version().await.unwrap_or(current); + + #[cfg(not(feature = "sync"))] + let latest = current; + + Settings::save_version_check_time()?; + Settings::save_to_data_dir(LATEST_VERSION_FILENAME, latest.to_string().as_str())?; + + Ok(latest) + } + + // Return Some(latest version) if an update is needed. Otherwise, none. + pub async fn needs_update(&self) -> Option { + if !self.update_check { + return None; + } + + let current = + Version::parse(env!("CARGO_PKG_VERSION")).unwrap_or(Version::new(100000, 0, 0)); + + let latest = self.latest_version().await; + + if latest.is_err() { + return None; + } + + let latest = latest.unwrap(); + + if latest > current { + return Some(latest); + } + + None + } + pub fn new() -> Result { let config_dir = atuin_common::utils::config_dir(); @@ -172,6 +272,7 @@ impl Settings { .set_default("session_path", session_path.to_str())? .set_default("dialect", "us")? .set_default("auto_sync", true)? + .set_default("update_check", true)? .set_default("sync_frequency", "1h")? .set_default("sync_address", "https://api.atuin.sh")? .set_default("search_mode", "prefix")? -- cgit v1.3.1