aboutsummaryrefslogtreecommitdiffstats
path: root/crates/turtle/src/atuin_client/meta.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-09 21:43:23 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-09 21:43:23 +0200
commit3223d93cb3c77ab02aa0a35f2a8314e447cee9a4 (patch)
tree3971a1f37f5fe3cadb747c5229a0d54e868e45e3 /crates/turtle/src/atuin_client/meta.rs
parentfix(client/sync): Pass through precise error on `SyncError::WrongKey` (diff)
downloadatuin-3223d93cb3c77ab02aa0a35f2a8314e447cee9a4.zip
chore: Separate daemon, client, server, and lib into crates
Diffstat (limited to 'crates/turtle/src/atuin_client/meta.rs')
-rw-r--r--crates/turtle/src/atuin_client/meta.rs182
1 files changed, 0 insertions, 182 deletions
diff --git a/crates/turtle/src/atuin_client/meta.rs b/crates/turtle/src/atuin_client/meta.rs
deleted file mode 100644
index 079c9926..00000000
--- a/crates/turtle/src/atuin_client/meta.rs
+++ /dev/null
@@ -1,182 +0,0 @@
-use std::path::Path;
-use std::str::FromStr;
-use std::time::Duration;
-
-use crate::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 tracing::debug;
-use uuid::Uuid;
-
-const KEY_HOST_ID: &str = "host_id";
-const KEY_LAST_SYNC: &str = "last_sync_time";
-
-pub(crate) struct MetaStore {
- pool: SqlitePool,
- cached_host_id: OnceCell<HostId>,
-}
-
-impl MetaStore {
- pub(crate) 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!("./db/client-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(),
- };
-
- Ok(store)
- }
-
- // Generic key-value operations
-
- pub(crate) 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(crate) 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(())
- }
-
- // Typed accessors
-
- pub(crate) 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 = crate::atuin_common::utils::uuid_v7();
- self.set(KEY_HOST_ID, uuid.as_simple().to_string().as_ref())
- .await?;
-
- Ok(HostId(uuid))
- })
- .await
- .copied()
- }
-
- pub(crate) 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(crate) async fn save_sync_time(&self) -> Result<()> {
- self.set(
- KEY_LAST_SYNC,
- OffsetDateTime::now_utc().format(&Rfc3339)?.as_str(),
- )
- .await
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{MetaStore, OffsetDateTime};
-
- 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);
- }
-}