diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-06-12 01:54:21 +0200 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-06-12 01:54:21 +0200 |
| commit | bbdf38018b47328b5faa2cef635c37095045be72 (patch) | |
| tree | 8983817d547551ae12508a8ae8731b622d990af4 /crates/turtle/src/atuin_client/settings.rs | |
| parent | feat(server): Make user stuff stateless (diff) | |
| download | atuin-bbdf38018b47328b5faa2cef635c37095045be72.zip | |
feat(server): Really make users stateless (with tests)
This commit also remove another load of unneeded features.
Diffstat (limited to 'crates/turtle/src/atuin_client/settings.rs')
| -rw-r--r-- | crates/turtle/src/atuin_client/settings.rs | 225 |
1 files changed, 89 insertions, 136 deletions
diff --git a/crates/turtle/src/atuin_client/settings.rs b/crates/turtle/src/atuin_client/settings.rs index e8ff98ee..d84e2eb0 100644 --- a/crates/turtle/src/atuin_client/settings.rs +++ b/crates/turtle/src/atuin_client/settings.rs @@ -1,8 +1,13 @@ -use std::{collections::HashMap, fmt, io::prelude::*, path::PathBuf, str::FromStr, sync::OnceLock}; +use crypto_secretbox::Key; +use std::{ + collections::HashMap, fmt, fs::read_to_string, io::prelude::Write, path::PathBuf, str::FromStr, + sync::OnceLock, +}; use tokio::sync::OnceCell; +use uuid::Uuid; -use crate::atuin_common::record::HostId; use crate::atuin_common::utils; +use crate::{atuin_client::encryption::decode_key, atuin_common::record::HostId}; use clap::ValueEnum; use config::{ Config, ConfigBuilder, Environment, File as ConfigFile, FileFormat, builder::DefaultState, @@ -217,16 +222,6 @@ pub(crate) enum KeymapMode { Auto, } -impl KeymapMode { - pub(crate) fn as_str(&self) -> &'static str { - match self { - KeymapMode::Emacs => "EMACS", - KeymapMode::VimNormal => "VIMNORMAL", - KeymapMode::VimInsert => "VIMINSERT", - KeymapMode::Auto => "AUTO", - } - } -} // We want to translate the config to crossterm::cursor::SetCursorStyle, but // the original type does not implement trait serde::Deserialize unfortunately. @@ -257,19 +252,6 @@ pub(crate) enum CursorStyle { SteadyBar, } -impl CursorStyle { - pub(crate) fn as_str(&self) -> &'static str { - match self { - CursorStyle::DefaultUserShape => "DEFAULT", - CursorStyle::BlinkingBlock => "BLINKBLOCK", - CursorStyle::SteadyBlock => "STEADYBLOCK", - CursorStyle::BlinkingUnderScore => "BLINKUNDERLINE", - CursorStyle::SteadyUnderScore => "STEADYUNDERLINE", - CursorStyle::BlinkingBar => "BLINKBAR", - CursorStyle::SteadyBar => "STEADYBAR", - } - } -} #[derive(Clone, Debug, Deserialize, Serialize)] pub(crate) struct Stats { @@ -330,36 +312,6 @@ impl Default for Stats { } } -/// Resolved authentication state for sync operations. -/// -/// Determined at runtime by examining which tokens are available and what -/// server the client is configured to talk to. Operations use this to pick -/// the right auth header and endpoint style. -#[cfg(feature = "sync")] -#[derive(Debug, Clone)] -pub(crate) enum SyncAuth { - /// Self-hosted Rust server. Uses `Authorization: Token <session>` and - /// legacy endpoints. - Legacy { token: String }, - - /// Not authenticated at all. Contains an actionable user-facing message. - NotLoggedIn { reason: String }, -} - -#[cfg(feature = "sync")] -impl SyncAuth { - /// Convert into the auth token type used by the API client. - /// - /// Returns an error with an actionable message for `NotLoggedIn`. - pub(crate) fn into_auth_token(self) -> Result<crate::atuin_client::api_client::AuthToken> { - use crate::atuin_client::api_client::AuthToken; - match self { - SyncAuth::Legacy { token } => Ok(AuthToken(token)), - SyncAuth::NotLoggedIn { reason } => Err(eyre!(reason)), - } - } -} - #[derive(Clone, Debug, Deserialize, Default, Serialize)] #[expect(clippy::struct_excessive_bools)] pub(crate) struct Keys { @@ -781,14 +733,6 @@ impl UiColumn { column_type, } } - - pub(crate) fn with_width(column_type: UiColumnType, width: u16) -> Self { - Self { - column_type, - width, - expand: column_type == UiColumnType::Command, - } - } } // Custom deserialize to handle both string and object formats: @@ -902,6 +846,82 @@ impl Default for Ui { } } +/// Sync-specific settings. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub(crate) struct Sync { + /// The sync address for atuin. + pub(crate) address: String, + + #[serde(default)] + pub(crate) frequency: String, + + #[serde(default)] + pub(crate) auto: bool, + + #[serde(default)] + pub(crate) user_id_path: Option<PathBuf>, + + #[serde(default)] + pub(crate) encryption_key_path: Option<PathBuf>, +} + +impl Sync { + fn try_read_file(file: Option<&PathBuf>) -> Result<Option<String>> { + if let Some(path) = file { + if path.try_exists()? { + let user = read_to_string(path)?; + + if user.is_empty() { + Ok(None) + } else { + Ok(Some(user)) + } + } else { + // It's okay that the file doesn't exist. + // The important part is to error out if we can't access it (e.g. Because of missing + // permissions). + Ok(None) + } + } else { + Ok(None) + } + } + + pub(crate) fn have_sync_user(&self) -> Result<bool> { + let sa = self.user_id()?; + Ok(sa.is_some()) + } + + pub(crate) fn user_id(&self) -> Result<Option<Uuid>> { + Self::try_read_file(self.user_id_path.as_ref())? + .map(|file| Uuid::try_parse(&file).map_err(Into::into)) + .transpose() + } + pub(crate) fn encryption_key(&self) -> Result<Option<Key>> { + Self::try_read_file(self.encryption_key_path.as_ref())? + .map(decode_key) + .transpose() + } + + pub(crate) async fn should_sync(&self) -> Result<bool> { + if !self.auto || !self.have_sync_user()? { + return Ok(false); + } + + if self.frequency == "0" || self.frequency.is_empty() { + return Ok(true); + } + + match parse_duration(self.frequency.as_str()) { + Ok(d) => { + let d = time::Duration::try_from(d)?; + Ok(OffsetDateTime::now_utc() - Settings::last_sync().await? >= d) + } + Err(e) => Err(eyre!("failed to check sync: {}", e)), + } + } +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[expect(clippy::struct_excessive_bools)] pub(crate) struct Settings { @@ -909,15 +929,9 @@ pub(crate) struct Settings { pub(crate) dialect: Dialect, pub(crate) timezone: Timezone, pub(crate) style: Style, - pub(crate) auto_sync: bool, - - /// The sync address for atuin. - pub(crate) sync_address: String, - pub(crate) sync_frequency: String, pub(crate) db_path: String, pub(crate) record_store_path: String, - pub(crate) key_path: String, pub(crate) search_mode: SearchMode, pub(crate) filter_mode: Option<FilterMode>, pub(crate) filter_mode_shell_up_key_binding: Option<FilterMode>, @@ -963,6 +977,9 @@ pub(crate) struct Settings { pub(crate) command_chaining: bool, #[serde(default)] + pub(crate) sync: Sync, + + #[serde(default)] pub(crate) stats: Stats, #[serde(default)] @@ -994,24 +1011,6 @@ pub(crate) struct Settings { } impl Settings { - pub(crate) fn utc() -> Self { - Self::builder() - .expect("Could not build default") - .set_override("timezone", "0") - .expect("failed to override timezone with UTC") - .build() - .expect("Could not build config") - .try_deserialize() - .expect("Could not deserialize config") - } - - pub(crate) fn effective_data_dir() -> PathBuf { - DATA_DIR - .get() - .cloned() - .unwrap_or_else(crate::atuin_common::utils::data_dir) - } - // -- Meta store: lazily initialized on first access -- pub(crate) async fn meta_store() -> Result<&'static crate::atuin_client::meta::MetaStore> { @@ -1037,34 +1036,6 @@ impl Settings { Self::meta_store().await?.save_sync_time().await } - pub(crate) async fn should_sync(&self) -> Result<bool> { - if !self.auto_sync || !self.have_sync_key().await? { - return Ok(false); - } - - if self.sync_frequency == "0" { - return Ok(true); - } - - match parse_duration(self.sync_frequency.as_str()) { - Ok(d) => { - let d = time::Duration::try_from(d)?; - Ok(OffsetDateTime::now_utc() - Settings::last_sync().await? >= d) - } - Err(e) => Err(eyre!("failed to check sync: {}", e)), - } - } - - pub(crate) async fn have_sync_key(&self) -> Result<bool> { - let sa = self.sync_auth().await?; - Ok(matches!(sa, SyncAuth::Legacy { .. })) - } - - pub(crate) async fn sync_auth(&self) -> Result<SyncAuth> { - // TODO(@bpeetz): Add this <2026-06-11> - todo!() - } - pub(crate) fn default_filter_mode(&self, git_root: bool) -> FilterMode { self.filter_mode .filter(|x| self.search.filters.contains(x)) @@ -1213,7 +1184,7 @@ impl Settings { let config_dir = crate::atuin_common::utils::config_dir(); create_dir_all(&config_dir) - .wrap_err_with(|| format!("could not create dir {config_dir:?}"))?; + .wrap_err_with(|| format!("could not create dir {}", config_dir.display()))?; let mut config_file = if let Ok(p) = std::env::var("ATUIN_CONFIG_DIR") { PathBuf::from(p) @@ -1414,12 +1385,8 @@ impl Settings { } pub(crate) fn paths_ok(&self) -> bool { - let paths = [ - &self.db_path, - &self.record_store_path, - &self.key_path, - &self.meta.db_path, - ]; + // TODO(@bpeetz): Add the `sync.*` paths <2026-06-11> + let paths = [&self.db_path, &self.record_store_path, &self.meta.db_path]; paths.iter().all(|p| !utils::broken_symlink(p)) } } @@ -1437,20 +1404,6 @@ impl Default for Settings { } } -/// Initialize the meta store configuration for testing. -/// -/// This should only be used in tests. It allows tests to bypass the normal -/// Settings::new() flow while still being able to use Settings::host_id() -/// and other meta store dependent functions. -/// -/// # Safety -/// This function is not thread-safe with concurrent calls to Settings::new() -/// or other meta store initialization. Only call from tests. -#[doc(hidden)] -pub(crate) fn init_meta_config_for_testing(meta_db_path: impl Into<String>, local_timeout: f64) { - META_CONFIG.set((meta_db_path.into(), local_timeout)).ok(); -} - #[cfg(test)] pub(crate) fn test_local_timeout() -> f64 { std::env::var("ATUIN_TEST_LOCAL_TIMEOUT") |
