aboutsummaryrefslogtreecommitdiffstats
path: root/atuin-client/src
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@elliehuxtable.com>2022-10-14 10:59:21 +0100
committerGitHub <noreply@github.com>2022-10-14 10:59:21 +0100
commitf03f6e9ad74d8e1cf1fa33dc2c0c7c5dd7ae5c94 (patch)
treeaa03f60230b3bc96485806724383bfd6e4cc6b1d /atuin-client/src
parentFix ZSH import print (diff)
downloadatuin-f03f6e9ad74d8e1cf1fa33dc2c0c7c5dd7ae5c94.zip
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 <conradludgate@gmail.com> * fix features Co-authored-by: Conrad Ludgate <conradludgate@gmail.com> Co-authored-by: Conrad Ludgate <conrad.ludgate@truelayer.com>
Diffstat (limited to 'atuin-client/src')
-rw-r--r--atuin-client/src/api_client.rs26
-rw-r--r--atuin-client/src/settings.rs121
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")?