diff options
Diffstat (limited to 'atuin-client/src')
| -rw-r--r-- | atuin-client/src/api_client.rs | 26 | ||||
| -rw-r--r-- | atuin-client/src/settings.rs | 121 |
2 files changed, 135 insertions, 12 deletions
diff --git a/atuin-client/src/api_client.rs b/atuin-client/src/api_client.rs index 5692fea0..b20d9378 100644 --- a/atuin-client/src/api_client.rs +++ b/atuin-client/src/api_client.rs @@ -9,9 +9,10 @@ use reqwest::{ use sodiumoxide::crypto::secretbox; use atuin_common::api::{ - AddHistoryRequest, CountResponse, ErrorResponse, LoginRequest, LoginResponse, RegisterResponse, - SyncHistoryResponse, + AddHistoryRequest, CountResponse, ErrorResponse, IndexResponse, LoginRequest, LoginResponse, + RegisterResponse, SyncHistoryResponse, }; +use semver::Version; use crate::{ encryption::{decode_key, decrypt}, @@ -86,6 +87,27 @@ pub async fn login(address: &str, req: LoginRequest) -> Result<LoginResponse> { Ok(session) } +pub async fn latest_version() -> Result<Version> { + let url = "https://api.atuin.sh"; + let client = reqwest::Client::new(); + + let resp = client + .get(url) + .header(USER_AGENT, APP_USER_AGENT) + .send() + .await?; + + if resp.status() != reqwest::StatusCode::OK { + let error = resp.json::<ErrorResponse>().await?; + bail!("failed to check latest version: {}", error.reason); + } + + let index = resp.json::<IndexResponse>().await?; + let version = Version::parse(index.version.as_str())?; + + Ok(version) +} + impl<'a> Client<'a> { pub fn new(sync_addr: &'a str, session_token: &'a str, key: String) -> Result<Self> { let mut headers = HeaderMap::new(); 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<chrono::DateTime<Utc>> { + fn read_from_data_dir(filename: &str) -> Option<String> { 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 !sync_time_path.exists() { - return Ok(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)); + if !path.exists() { + return None; } - let time = fs_err::read_to_string(sync_time_path)?; - let time = chrono::DateTime::parse_from_rfc3339(time.as_str())?; + 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<chrono::DateTime<Utc>> { + let value = Settings::read_from_data_dir(filename); - Ok(time.with_timezone(&Utc)) + match value { + Some(v) => { + let time = chrono::DateTime::parse_from_rfc3339(v.as_str())?; + + Ok(time.with_timezone(&Utc)) + } + None => Ok(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), + } + } + + 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<chrono::DateTime<Utc>> { + Settings::load_time_from_file(LAST_SYNC_FILENAME) + } + + pub fn last_version_check() -> Result<chrono::DateTime<Utc>> { + Settings::load_time_from_file(LAST_VERSION_CHECK_FILENAME) } pub fn should_sync(&self) -> Result<bool> { @@ -142,6 +181,67 @@ impl Settings { } } + fn needs_update_check(&self) -> Result<bool> { + 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<Version> { + // 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<Version> { + 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<Self> { 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")? |
