diff options
Diffstat (limited to 'crates/atuin-client/src')
| -rw-r--r-- | crates/atuin-client/src/database.rs | 17 | ||||
| -rw-r--r-- | crates/atuin-client/src/history/store.rs | 2 | ||||
| -rw-r--r-- | crates/atuin-client/src/lib.rs | 1 | ||||
| -rw-r--r-- | crates/atuin-client/src/login.rs | 11 | ||||
| -rw-r--r-- | crates/atuin-client/src/logout.rs | 11 | ||||
| -rw-r--r-- | crates/atuin-client/src/meta.rs | 365 | ||||
| -rw-r--r-- | crates/atuin-client/src/record/sync.rs | 2 | ||||
| -rw-r--r-- | crates/atuin-client/src/register.rs | 7 | ||||
| -rw-r--r-- | crates/atuin-client/src/settings.rs | 176 | ||||
| -rw-r--r-- | crates/atuin-client/src/settings/meta.rs | 17 | ||||
| -rw-r--r-- | crates/atuin-client/src/sync.rs | 6 |
11 files changed, 466 insertions, 149 deletions
diff --git a/crates/atuin-client/src/database.rs b/crates/atuin-client/src/database.rs index 408e8e52..28d6c0f0 100644 --- a/crates/atuin-client/src/database.rs +++ b/crates/atuin-client/src/database.rs @@ -54,25 +54,22 @@ pub struct OptFilters { pub include_duplicates: bool, } -pub fn current_context() -> Context { - let Ok(session) = env::var("ATUIN_SESSION") else { - eprintln!( - "ERROR: Failed to find $ATUIN_SESSION in the environment. Check that you have correctly set up your shell." - ); - std::process::exit(1); - }; +pub async fn current_context() -> eyre::Result<Context> { + let session = env::var("ATUIN_SESSION").map_err(|_| { + eyre::eyre!("Failed to find $ATUIN_SESSION in the environment. Check that you have correctly set up your shell.") + })?; let hostname = get_host_user(); let cwd = utils::get_current_dir(); - let host_id = Settings::host_id().expect("failed to load host ID"); + let host_id = Settings::host_id().await?; let git_root = utils::in_git_repo(cwd.as_str()); - Context { + Ok(Context { session, hostname, cwd, git_root, host_id: host_id.0.as_simple().to_string(), - } + }) } fn get_session_start_time(session_id: &str) -> Option<i64> { diff --git a/crates/atuin-client/src/history/store.rs b/crates/atuin-client/src/history/store.rs index b8ac6ff4..041d90ce 100644 --- a/crates/atuin-client/src/history/store.rs +++ b/crates/atuin-client/src/history/store.rs @@ -302,7 +302,7 @@ impl HistoryStore { pb.set_message("Fetching history from old database"); - let context = current_context(); + let context = current_context().await?; let history = db.list(&[], &context, None, false, true).await?; pb.set_message("Fetching history already in store"); diff --git a/crates/atuin-client/src/lib.rs b/crates/atuin-client/src/lib.rs index 78819548..160d4529 100644 --- a/crates/atuin-client/src/lib.rs +++ b/crates/atuin-client/src/lib.rs @@ -14,6 +14,7 @@ pub mod history; pub mod import; pub mod login; pub mod logout; +pub mod meta; pub mod ordering; pub mod plugin; pub mod record; diff --git a/crates/atuin-client/src/login.rs b/crates/atuin-client/src/login.rs index 78168c7e..ab265928 100644 --- a/crates/atuin-client/src/login.rs +++ b/crates/atuin-client/src/login.rs @@ -54,7 +54,7 @@ pub async fn login( bail!("the specified key was invalid"); } - let mut file = File::create(key_path).await?; + let mut file = File::create(&key_path).await?; file.write_all(key.as_bytes()).await?; } else { // we now know that the user has logged in specifying a key, AND that the key path @@ -75,7 +75,7 @@ pub async fn login( store.re_encrypt(¤t_key, &new_key).await?; println!("Writing new key"); - let mut file = File::create(key_path).await?; + let mut file = File::create(&key_path).await?; file.write_all(encoded.as_bytes()).await?; } } @@ -86,9 +86,10 @@ pub async fn login( ) .await?; - let session_path = settings.session_path.as_str(); - let mut file = File::create(session_path).await?; - file.write_all(session.session.as_bytes()).await?; + Settings::meta_store() + .await? + .save_session(&session.session) + .await?; Ok(session.session) } diff --git a/crates/atuin-client/src/logout.rs b/crates/atuin-client/src/logout.rs index fe1a4d23..f720b302 100644 --- a/crates/atuin-client/src/logout.rs +++ b/crates/atuin-client/src/logout.rs @@ -1,13 +1,12 @@ -use eyre::{Context, Result}; -use fs_err::remove_file; +use eyre::Result; use crate::settings::Settings; -pub fn logout(settings: &Settings) -> Result<()> { - let session_path = settings.session_path.as_str(); +pub async fn logout() -> Result<()> { + let meta = Settings::meta_store().await?; - if settings.logged_in() { - remove_file(session_path).context("Failed to remove session file")?; + if meta.logged_in().await? { + meta.delete_session().await?; println!("You have logged out!"); } else { println!("You are not logged in"); diff --git a/crates/atuin-client/src/meta.rs b/crates/atuin-client/src/meta.rs new file mode 100644 index 00000000..870f36d0 --- /dev/null +++ b/crates/atuin-client/src/meta.rs @@ -0,0 +1,365 @@ +use std::path::Path; +use std::str::FromStr; +use std::time::Duration; + +use atuin_common::record::HostId; +use eyre::{Result, eyre}; +use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions}; +use time::{OffsetDateTime, format_description::well_known::Rfc3339}; +use tokio::sync::OnceCell; +use uuid::Uuid; + +// Filenames for the legacy plain-text files that we migrate from. +const LEGACY_HOST_ID_FILENAME: &str = "host_id"; +const LEGACY_LAST_SYNC_FILENAME: &str = "last_sync_time"; +const LEGACY_LAST_VERSION_CHECK_FILENAME: &str = "last_version_check_time"; +const LEGACY_LATEST_VERSION_FILENAME: &str = "latest_version"; +const LEGACY_SESSION_FILENAME: &str = "session"; + +const KEY_HOST_ID: &str = "host_id"; +const KEY_LAST_SYNC: &str = "last_sync_time"; +const KEY_LAST_VERSION_CHECK: &str = "last_version_check_time"; +const KEY_LATEST_VERSION: &str = "latest_version"; +const KEY_SESSION: &str = "session"; +const KEY_FILES_MIGRATED: &str = "files_migrated"; + +pub struct MetaStore { + pool: SqlitePool, + cached_host_id: OnceCell<HostId>, +} + +impl MetaStore { + pub async fn new(path: impl AsRef<Path>, timeout: f64) -> Result<Self> { + let path = path.as_ref(); + let path_str = path + .as_os_str() + .to_str() + .ok_or_else(|| eyre!("meta database path is not valid UTF-8: {path:?}"))?; + debug!("opening meta sqlite database at {path:?}"); + + let is_memory = path_str.contains(":memory:"); + + if !is_memory + && !path.exists() + && let Some(dir) = path.parent() + { + fs_err::create_dir_all(dir)?; + } + + // Use DELETE journal mode instead of WAL. This is a small, infrequently- + // written KV store — WAL's concurrency benefits aren't needed, and DELETE + // mode avoids creating auxiliary -wal/-shm files that complicate + // permission handling. + let opts = SqliteConnectOptions::from_str(path_str)? + .journal_mode(SqliteJournalMode::Delete) + .optimize_on_close(true, None) + .create_if_missing(true); + + let pool = SqlitePoolOptions::new() + .acquire_timeout(Duration::from_secs_f64(timeout)) + .connect_with(opts) + .await?; + + sqlx::migrate!("./meta-migrations").run(&pool).await?; + + // Session tokens are stored in this database, so restrict permissions. + #[cfg(unix)] + if !is_memory { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; + } + + let store = Self { + pool, + cached_host_id: OnceCell::const_new(), + }; + + if !is_memory { + store.migrate_files().await?; + } + + Ok(store) + } + + // Generic key-value operations + + pub async fn get(&self, key: &str) -> Result<Option<String>> { + let row: Option<(String,)> = sqlx::query_as("SELECT value FROM meta WHERE key = ?1") + .bind(key) + .fetch_optional(&self.pool) + .await?; + + Ok(row.map(|r| r.0)) + } + + pub async fn set(&self, key: &str, value: &str) -> Result<()> { + sqlx::query( + "INSERT INTO meta (key, value, updated_at) VALUES (?1, ?2, strftime('%s', 'now')) + ON CONFLICT(key) DO UPDATE SET value = ?2, updated_at = strftime('%s', 'now')", + ) + .bind(key) + .bind(value) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn delete(&self, key: &str) -> Result<()> { + sqlx::query("DELETE FROM meta WHERE key = ?1") + .bind(key) + .execute(&self.pool) + .await?; + + Ok(()) + } + + // Typed accessors + + pub async fn host_id(&self) -> Result<HostId> { + self.cached_host_id + .get_or_try_init(|| async { + if let Some(id) = self.get(KEY_HOST_ID).await? { + let parsed = Uuid::from_str(id.as_str()) + .map_err(|e| eyre!("failed to parse host ID: {e}"))?; + return Ok(HostId(parsed)); + } + + let uuid = atuin_common::utils::uuid_v7(); + self.set(KEY_HOST_ID, uuid.as_simple().to_string().as_ref()) + .await?; + + Ok(HostId(uuid)) + }) + .await + .copied() + } + + pub async fn last_sync(&self) -> Result<OffsetDateTime> { + match self.get(KEY_LAST_SYNC).await? { + Some(v) => Ok(OffsetDateTime::parse(v.as_str(), &Rfc3339)?), + None => Ok(OffsetDateTime::UNIX_EPOCH), + } + } + + pub async fn save_sync_time(&self) -> Result<()> { + self.set( + KEY_LAST_SYNC, + OffsetDateTime::now_utc().format(&Rfc3339)?.as_str(), + ) + .await + } + + pub async fn last_version_check(&self) -> Result<OffsetDateTime> { + match self.get(KEY_LAST_VERSION_CHECK).await? { + Some(v) => Ok(OffsetDateTime::parse(v.as_str(), &Rfc3339)?), + None => Ok(OffsetDateTime::UNIX_EPOCH), + } + } + + pub async fn save_version_check_time(&self) -> Result<()> { + self.set( + KEY_LAST_VERSION_CHECK, + OffsetDateTime::now_utc().format(&Rfc3339)?.as_str(), + ) + .await + } + + pub async fn latest_version(&self) -> Result<Option<String>> { + self.get(KEY_LATEST_VERSION).await + } + + pub async fn save_latest_version(&self, version: &str) -> Result<()> { + self.set(KEY_LATEST_VERSION, version).await + } + + pub async fn session_token(&self) -> Result<Option<String>> { + self.get(KEY_SESSION).await + } + + pub async fn save_session(&self, token: &str) -> Result<()> { + self.set(KEY_SESSION, token).await + } + + pub async fn delete_session(&self) -> Result<()> { + self.delete(KEY_SESSION).await + } + + pub async fn logged_in(&self) -> Result<bool> { + Ok(self.session_token().await?.is_some()) + } + + // File migration: on first open, migrate old plain-text files into the database. + // Old files are left in place for safe downgrades. + + async fn migrate_files(&self) -> Result<()> { + if self.get(KEY_FILES_MIGRATED).await?.is_some() { + return Ok(()); + } + + let data_dir = crate::settings::Settings::effective_data_dir(); + + // host_id — validate as UUID + let host_id_path = data_dir.join(LEGACY_HOST_ID_FILENAME); + if host_id_path.exists() + && let Ok(value) = fs_err::read_to_string(&host_id_path) + { + let value = value.trim(); + if !value.is_empty() { + if Uuid::from_str(value).is_ok() { + self.set(KEY_HOST_ID, value).await?; + } else { + warn!("skipping migration of host_id: invalid UUID {value:?}"); + } + } + } + + // last_sync_time — validate as RFC3339 + let sync_path = data_dir.join(LEGACY_LAST_SYNC_FILENAME); + if sync_path.exists() + && let Ok(value) = fs_err::read_to_string(&sync_path) + { + let value = value.trim(); + if !value.is_empty() { + if OffsetDateTime::parse(value, &Rfc3339).is_ok() { + self.set(KEY_LAST_SYNC, value).await?; + } else { + warn!("skipping migration of last_sync_time: invalid RFC3339 {value:?}"); + } + } + } + + // last_version_check_time — validate as RFC3339 + let version_check_path = data_dir.join(LEGACY_LAST_VERSION_CHECK_FILENAME); + if version_check_path.exists() + && let Ok(value) = fs_err::read_to_string(&version_check_path) + { + let value = value.trim(); + if !value.is_empty() { + if OffsetDateTime::parse(value, &Rfc3339).is_ok() { + self.set(KEY_LAST_VERSION_CHECK, value).await?; + } else { + warn!( + "skipping migration of last_version_check_time: invalid RFC3339 {value:?}" + ); + } + } + } + + // latest_version — no strict validation, just non-empty + let latest_version_path = data_dir.join(LEGACY_LATEST_VERSION_FILENAME); + if latest_version_path.exists() + && let Ok(value) = fs_err::read_to_string(&latest_version_path) + { + let value = value.trim(); + if !value.is_empty() { + self.set(KEY_LATEST_VERSION, value).await?; + } + } + + // session token — no strict validation, just non-empty + let session_path = data_dir.join(LEGACY_SESSION_FILENAME); + if session_path.exists() + && let Ok(value) = fs_err::read_to_string(&session_path) + { + let value = value.trim(); + if !value.is_empty() { + self.set(KEY_SESSION, value).await?; + } + } + + self.set(KEY_FILES_MIGRATED, "true").await?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + async fn new_test_store() -> MetaStore { + MetaStore::new("sqlite::memory:", 2.0).await.unwrap() + } + + #[tokio::test] + async fn test_get_set_delete() { + let store = new_test_store().await; + + assert_eq!(store.get("foo").await.unwrap(), None); + + store.set("foo", "bar").await.unwrap(); + assert_eq!(store.get("foo").await.unwrap(), Some("bar".to_string())); + + store.set("foo", "baz").await.unwrap(); + assert_eq!(store.get("foo").await.unwrap(), Some("baz".to_string())); + + store.delete("foo").await.unwrap(); + assert_eq!(store.get("foo").await.unwrap(), None); + } + + #[tokio::test] + async fn test_host_id_generation_and_stability() { + let store = new_test_store().await; + + let id1 = store.host_id().await.unwrap(); + let id2 = store.host_id().await.unwrap(); + + assert_eq!(id1, id2, "host_id should be stable across calls"); + } + + #[tokio::test] + async fn test_sync_time() { + let store = new_test_store().await; + + let t = store.last_sync().await.unwrap(); + assert_eq!(t, OffsetDateTime::UNIX_EPOCH); + + store.save_sync_time().await.unwrap(); + let t = store.last_sync().await.unwrap(); + assert!(t > OffsetDateTime::UNIX_EPOCH); + } + + #[tokio::test] + async fn test_version_check_time() { + let store = new_test_store().await; + + let t = store.last_version_check().await.unwrap(); + assert_eq!(t, OffsetDateTime::UNIX_EPOCH); + + store.save_version_check_time().await.unwrap(); + let t = store.last_version_check().await.unwrap(); + assert!(t > OffsetDateTime::UNIX_EPOCH); + } + + #[tokio::test] + async fn test_session_crud() { + let store = new_test_store().await; + + assert!(!store.logged_in().await.unwrap()); + assert_eq!(store.session_token().await.unwrap(), None); + + store.save_session("tok123").await.unwrap(); + assert!(store.logged_in().await.unwrap()); + assert_eq!( + store.session_token().await.unwrap(), + Some("tok123".to_string()) + ); + + store.delete_session().await.unwrap(); + assert!(!store.logged_in().await.unwrap()); + } + + #[tokio::test] + async fn test_latest_version() { + let store = new_test_store().await; + + assert_eq!(store.latest_version().await.unwrap(), None); + + store.save_latest_version("1.2.3").await.unwrap(); + assert_eq!( + store.latest_version().await.unwrap(), + Some("1.2.3".to_string()) + ); + } +} diff --git a/crates/atuin-client/src/record/sync.rs b/crates/atuin-client/src/record/sync.rs index bd357b79..52c34a50 100644 --- a/crates/atuin-client/src/record/sync.rs +++ b/crates/atuin-client/src/record/sync.rs @@ -57,6 +57,7 @@ pub async fn diff( &settings.sync_address, settings .session_token() + .await .map_err(|e| SyncError::RemoteRequestError { msg: e.to_string() })? .as_str(), settings.network_connect_timeout, @@ -282,6 +283,7 @@ pub async fn sync_remote( &settings.sync_address, settings .session_token() + .await .map_err(|e| SyncError::RemoteRequestError { msg: e.to_string() })? .as_str(), settings.network_connect_timeout, diff --git a/crates/atuin-client/src/register.rs b/crates/atuin-client/src/register.rs index dae01efd..b0c80dc4 100644 --- a/crates/atuin-client/src/register.rs +++ b/crates/atuin-client/src/register.rs @@ -1,6 +1,4 @@ use eyre::Result; -use tokio::fs::File; -use tokio::io::AsyncWriteExt; use crate::{api_client, settings::Settings}; @@ -13,9 +11,8 @@ pub async fn register( let session = api_client::register(settings.sync_address.as_str(), &username, &email, &password).await?; - let path = settings.session_path.as_str(); - let mut file = File::create(path).await?; - file.write_all(session.session.as_bytes()).await?; + let meta = Settings::meta_store().await?; + meta.save_session(&session.session).await?; let _key = crate::encryption::load_key(settings)?; diff --git a/crates/atuin-client/src/settings.rs b/crates/atuin-client/src/settings.rs index a988e145..df629664 100644 --- a/crates/atuin-client/src/settings.rs +++ b/crates/atuin-client/src/settings.rs @@ -1,7 +1,5 @@ -use std::{ - collections::HashMap, convert::TryFrom, fmt, io::prelude::*, path::PathBuf, str::FromStr, - sync::OnceLock, -}; +use std::{collections::HashMap, fmt, io::prelude::*, path::PathBuf, str::FromStr, sync::OnceLock}; +use tokio::sync::OnceCell; use atuin_common::record::HostId; use atuin_common::utils; @@ -16,24 +14,18 @@ use regex::RegexSet; use semver::Version; use serde::{Deserialize, Serialize}; use serde_with::DeserializeFromStr; -use time::{ - OffsetDateTime, UtcOffset, - format_description::{FormatItem, well_known::Rfc3339}, - macros::format_description, -}; -use uuid::Uuid; +use time::{OffsetDateTime, UtcOffset, format_description::FormatItem, macros::format_description}; 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"; -pub const HOST_ID_FILENAME: &str = "host_id"; static EXAMPLE_CONFIG: &str = include_str!("../config.toml"); static DATA_DIR: OnceLock<PathBuf> = OnceLock::new(); +static META_CONFIG: OnceLock<(String, f64)> = OnceLock::new(); +static META_STORE: OnceCell<crate::meta::MetaStore> = OnceCell::const_new(); mod dotfiles; mod kv; +pub(crate) mod meta; mod scripts; #[derive(Clone, Debug, Deserialize, Copy, ValueEnum, PartialEq, Serialize)] @@ -673,7 +665,6 @@ pub struct Settings { pub db_path: String, pub record_store_path: String, pub key_path: String, - pub session_path: String, pub search_mode: SearchMode, pub filter_mode: Option<FilterMode>, pub filter_mode_shell_up_key_binding: Option<FilterMode>, @@ -751,6 +742,9 @@ pub struct Settings { #[serde(default)] pub tmux: Tmux, + + #[serde(default)] + pub meta: meta::Settings, } impl Settings { @@ -765,92 +759,48 @@ impl Settings { .expect("Could not deserialize config") } - fn effective_data_dir() -> PathBuf { + pub(crate) fn effective_data_dir() -> PathBuf { DATA_DIR .get() .cloned() .unwrap_or_else(atuin_common::utils::data_dir) } - fn save_to_data_dir(filename: &str, value: &str) -> Result<()> { - let data_dir = Self::effective_data_dir(); - let data_dir = data_dir.as_path(); - - let path = data_dir.join(filename); - - fs_err::write(path, value)?; - - Ok(()) - } - - fn read_from_data_dir(filename: &str) -> Option<String> { - let data_dir = Self::effective_data_dir(); - let data_dir = data_dir.as_path(); - - 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, - OffsetDateTime::now_utc().format(&Rfc3339)?.as_str(), - )?; + // -- Meta store: lazily initialized on first access -- - Ok(()) - } - - fn load_time_from_file(filename: &str) -> Result<OffsetDateTime> { - let value = Settings::read_from_data_dir(filename); - - match value { - Some(v) => Ok(OffsetDateTime::parse(v.as_str(), &Rfc3339)?), - None => Ok(OffsetDateTime::UNIX_EPOCH), - } + pub async fn meta_store() -> Result<&'static crate::meta::MetaStore> { + META_STORE + .get_or_try_init(|| async { + let (db_path, timeout) = META_CONFIG.get().ok_or_else(|| { + eyre!("meta store config not set — Settings::new() has not been called") + })?; + crate::meta::MetaStore::new(db_path, *timeout).await + }) + .await } - pub fn save_sync_time() -> Result<()> { - Settings::save_current_time(LAST_SYNC_FILENAME) + pub async fn host_id() -> Result<HostId> { + Self::meta_store().await?.host_id().await } - pub fn save_version_check_time() -> Result<()> { - Settings::save_current_time(LAST_VERSION_CHECK_FILENAME) + pub async fn last_sync() -> Result<OffsetDateTime> { + Self::meta_store().await?.last_sync().await } - pub fn last_sync() -> Result<OffsetDateTime> { - Settings::load_time_from_file(LAST_SYNC_FILENAME) + pub async fn save_sync_time() -> Result<()> { + Self::meta_store().await?.save_sync_time().await } - pub fn last_version_check() -> Result<OffsetDateTime> { - Settings::load_time_from_file(LAST_VERSION_CHECK_FILENAME) + pub async fn last_version_check() -> Result<OffsetDateTime> { + Self::meta_store().await?.last_version_check().await } - pub fn host_id() -> Option<HostId> { - let id = Settings::read_from_data_dir(HOST_ID_FILENAME); - - if let Some(id) = id { - let parsed = - Uuid::from_str(id.as_str()).expect("failed to parse host ID from local directory"); - return Some(HostId(parsed)); - } - - let uuid = atuin_common::utils::uuid_v7(); - - Settings::save_to_data_dir(HOST_ID_FILENAME, uuid.as_simple().to_string().as_ref()) - .expect("Could not write host ID to data dir"); - - Some(HostId(uuid)) + pub async fn save_version_check_time() -> Result<()> { + Self::meta_store().await?.save_version_check_time().await } - pub fn should_sync(&self) -> Result<bool> { - if !self.auto_sync || !PathBuf::from(self.session_path.as_str()).exists() { + pub async fn should_sync(&self) -> Result<bool> { + if !self.auto_sync || !Self::meta_store().await?.logged_in().await? { return Ok(false); } @@ -861,30 +811,26 @@ impl Settings { match parse_duration(self.sync_frequency.as_str()) { Ok(d) => { let d = time::Duration::try_from(d)?; - Ok(OffsetDateTime::now_utc() - Settings::last_sync()? >= d) + Ok(OffsetDateTime::now_utc() - Settings::last_sync().await? >= d) } Err(e) => Err(eyre!("failed to check sync: {}", e)), } } - pub fn logged_in(&self) -> bool { - let session_path = self.session_path.as_str(); - - PathBuf::from(session_path).exists() + pub async fn logged_in(&self) -> Result<bool> { + Self::meta_store().await?.logged_in().await } - pub fn session_token(&self) -> Result<String> { - if !self.logged_in() { - return Err(eyre!("Tried to load session; not logged in")); + pub async fn session_token(&self) -> Result<String> { + match Self::meta_store().await?.session_token().await? { + Some(token) => Ok(token), + None => Err(eyre!("Tried to load session; not logged in")), } - - let session_path = self.session_path.as_str(); - Ok(fs_err::read_to_string(session_path)?) } #[cfg(feature = "check-update")] - fn needs_update_check(&self) -> Result<bool> { - let last_check = Settings::last_version_check()?; + async fn needs_update_check(&self) -> Result<bool> { + let last_check = Settings::last_version_check().await?; let diff = OffsetDateTime::now_utc() - last_check; // Check a max of once per hour @@ -898,16 +844,9 @@ impl Settings { 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 = tokio::task::spawn_blocking(|| { - Settings::read_from_data_dir(LATEST_VERSION_FILENAME) - }) - .await - .expect("file task panicked"); - - let version = match version { + if !self.needs_update_check().await? { + let meta = Self::meta_store().await?; + let version = match meta.latest_version().await? { Some(v) => Version::parse(&v).unwrap_or(current), None => current, }; @@ -921,14 +860,9 @@ impl Settings { #[cfg(not(feature = "sync"))] let latest = current; - let latest_encoded = latest.to_string(); - tokio::task::spawn_blocking(move || { - Settings::save_version_check_time()?; - Settings::save_to_data_dir(LATEST_VERSION_FILENAME, &latest_encoded)?; - Ok::<(), eyre::Report>(()) - }) - .await - .expect("file task panicked")?; + let meta = Self::meta_store().await?; + Settings::save_version_check_time().await?; + meta.save_latest_version(&latest.to_string()).await?; Ok(latest) } @@ -992,14 +926,13 @@ impl Settings { let socket_path = atuin_common::utils::runtime_dir().join("atuin.sock"); let key_path = data_dir.join("key"); - let session_path = data_dir.join("session"); + let meta_path = data_dir.join("meta.db"); Ok(Config::builder() .set_default("history_format", "{time}\t{command}\t{duration}")? .set_default("db_path", db_path.to_str())? .set_default("record_store_path", record_store_path.to_str())? .set_default("key_path", key_path.to_str())? - .set_default("session_path", session_path.to_str())? .set_default("dialect", "us")? .set_default("timezone", "local")? .set_default("auto_sync", true)? @@ -1058,6 +991,7 @@ impl Settings { .set_default("daemon.tcp_port", 8889)? .set_default("kv.db_path", kv_path.to_str())? .set_default("scripts.db_path", scripts_path.to_str())? + .set_default("meta.db_path", meta_path.to_str())? .set_default( "search.filters", vec![ @@ -1170,12 +1104,16 @@ impl Settings { settings.db_path = Self::expand_path(settings.db_path)?; settings.record_store_path = Self::expand_path(settings.record_store_path)?; settings.key_path = Self::expand_path(settings.key_path)?; - settings.session_path = Self::expand_path(settings.session_path)?; settings.daemon.socket_path = Self::expand_path(settings.daemon.socket_path)?; // Validate UI settings settings.ui.validate()?; + // Register meta store config for lazy initialization on first access + META_CONFIG + .set((settings.meta.db_path.clone(), settings.local_timeout)) + .ok(); + Ok(settings) } @@ -1194,7 +1132,7 @@ impl Settings { &self.db_path, &self.record_store_path, &self.key_path, - &self.session_path, + &self.meta.db_path, ]; paths.iter().all(|p| !utils::broken_symlink(p)) } @@ -1325,14 +1263,13 @@ mod tests { let db_path: String = config.get("db_path")?; let key_path: String = config.get("key_path")?; - let session_path: String = config.get("session_path")?; let record_store_path: String = config.get("record_store_path")?; let kv_db_path: String = config.get("kv.db_path")?; let scripts_db_path: String = config.get("scripts.db_path")?; + let meta_db_path: String = config.get("meta.db_path")?; assert_eq!(db_path, custom_dir.join("history.db").to_str().unwrap()); assert_eq!(key_path, custom_dir.join("key").to_str().unwrap()); - assert_eq!(session_path, custom_dir.join("session").to_str().unwrap()); assert_eq!( record_store_path, custom_dir.join("records.db").to_str().unwrap() @@ -1342,6 +1279,7 @@ mod tests { scripts_db_path, custom_dir.join("scripts.db").to_str().unwrap() ); + assert_eq!(meta_db_path, custom_dir.join("meta.db").to_str().unwrap()); Ok(()) } diff --git a/crates/atuin-client/src/settings/meta.rs b/crates/atuin-client/src/settings/meta.rs new file mode 100644 index 00000000..108d74ec --- /dev/null +++ b/crates/atuin-client/src/settings/meta.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Settings { + pub db_path: String, +} + +impl Default for Settings { + fn default() -> Self { + let dir = atuin_common::utils::data_dir(); + let path = dir.join("meta.db"); + + Self { + db_path: path.to_string_lossy().to_string(), + } + } +} diff --git a/crates/atuin-client/src/sync.rs b/crates/atuin-client/src/sync.rs index 57ac18e4..4c236de4 100644 --- a/crates/atuin-client/src/sync.rs +++ b/crates/atuin-client/src/sync.rs @@ -53,7 +53,7 @@ async fn sync_download( let mut last_sync = if force { OffsetDateTime::UNIX_EPOCH } else { - Settings::last_sync()? + Settings::last_sync().await? }; let mut last_timestamp = OffsetDateTime::UNIX_EPOCH; @@ -194,12 +194,12 @@ async fn sync_upload( pub async fn sync(settings: &Settings, force: bool, db: &impl Database) -> Result<()> { let client = api_client::Client::new( &settings.sync_address, - settings.session_token()?.as_str(), + settings.session_token().await?.as_str(), settings.network_connect_timeout, settings.network_timeout, )?; - Settings::save_sync_time()?; + Settings::save_sync_time().await?; let key = load_key(settings)?; // encryption key |
