diff --git a/atuin-client/src/api_client.rs b/atuin-client/src/api_client.rs index cea78792..9369203f 100644 --- a/atuin-client/src/api_client.rs +++ b/atuin-client/src/api_client.rs @@ -8,11 +8,14 @@ use reqwest::{ StatusCode, Url, }; -use atuin_common::api::{ - AddHistoryRequest, CountResponse, DeleteHistoryRequest, ErrorResponse, IndexResponse, - LoginRequest, LoginResponse, RegisterResponse, StatusResponse, SyncHistoryResponse, -}; use atuin_common::record::Record; +use atuin_common::{ + api::{ + AddHistoryRequest, CountResponse, DeleteHistoryRequest, ErrorResponse, IndexResponse, + LoginRequest, LoginResponse, RegisterResponse, StatusResponse, SyncHistoryResponse, + }, + record::RecordIndex, +}; use semver::Version; use crate::{history::History, sync::hash_str}; @@ -205,6 +208,16 @@ impl<'a> Client<'a> { Ok(()) } + pub async fn record_index(&self) -> Result { + let url = format!("{}/record", self.sync_addr); + let url = Url::parse(url.as_str())?; + + let resp = self.client.get(url).send().await?; + let index = resp.json().await?; + + Ok(index) + } + pub async fn delete(&self) -> Result<()> { let url = format!("{}/account", self.sync_addr); let url = Url::parse(url.as_str())?; diff --git a/atuin-client/src/record/mod.rs b/atuin-client/src/record/mod.rs index 9ac2c541..1edb2747 100644 --- a/atuin-client/src/record/mod.rs +++ b/atuin-client/src/record/mod.rs @@ -1,3 +1,4 @@ pub mod encryption; pub mod sqlite_store; pub mod store; +pub mod sync; diff --git a/atuin-client/src/record/sqlite_store.rs b/atuin-client/src/record/sqlite_store.rs index fd056f70..9207129e 100644 --- a/atuin-client/src/record/sqlite_store.rs +++ b/atuin-client/src/record/sqlite_store.rs @@ -83,7 +83,7 @@ impl SqliteStore { // tbh at this point things are pretty fucked so just panic let id = Uuid::from_str(row.get("id")).expect("invalid id UUID format in sqlite DB"); let host = Uuid::from_str(row.get("host")).expect("invalid host UUID format in sqlite DB"); - let parent: Option<&str> = row.get("host"); + let parent: Option<&str> = row.get("parent"); let parent = if let Some(parent) = parent { Some(Uuid::from_str(parent).expect("invalid parent UUID format in sqlite DB")) @@ -125,7 +125,7 @@ impl Store for SqliteStore { async fn get(&self, id: Uuid) -> Result> { let res = sqlx::query("select * from records where id = ?1") - .bind(id) + .bind(id.as_simple().to_string()) .map(Self::query_row) .fetch_one(&self.pool) .await?; @@ -136,7 +136,7 @@ impl Store for SqliteStore { async fn len(&self, host: Uuid, tag: &str) -> Result { let res: (i64,) = sqlx::query_as("select count(1) from records where host = ?1 and tag = ?2") - .bind(host) + .bind(host.as_simple().to_string()) .bind(tag) .fetch_one(&self.pool) .await?; @@ -146,7 +146,7 @@ impl Store for SqliteStore { async fn next(&self, record: &Record) -> Result>> { let res = sqlx::query("select * from records where parent = ?1") - .bind(record.id.clone()) + .bind(record.id.as_simple().to_string()) .map(Self::query_row) .fetch_one(&self.pool) .await; @@ -162,7 +162,7 @@ impl Store for SqliteStore { let res = sqlx::query( "select * from records where host = ?1 and tag = ?2 and parent is null limit 1", ) - .bind(host) + .bind(host.as_simple().to_string()) .bind(tag) .map(Self::query_row) .fetch_optional(&self.pool) @@ -183,6 +183,23 @@ impl Store for SqliteStore { Ok(res) } + + async fn tail_records(&self) -> Result> { + let res = sqlx::query( + "select host, tag, id from records rp where (select count(1) from records where parent=rp.id) = 0;", + ) + .map(|row: SqliteRow| { + let host: Uuid= Uuid::from_str(row.get("host")).expect("invalid uuid in db host"); + let tag: String= row.get("tag"); + let id: Uuid= Uuid::from_str(row.get("id")).expect("invalid uuid in db id"); + + (host, tag, id) + }) + .fetch_all(&self.pool) + .await?; + + Ok(res) + } } #[cfg(test)] diff --git a/atuin-client/src/record/store.rs b/atuin-client/src/record/store.rs index f0c145b0..a5280bbb 100644 --- a/atuin-client/src/record/store.rs +++ b/atuin-client/src/record/store.rs @@ -31,4 +31,6 @@ pub trait Store { async fn first(&self, host: Uuid, tag: &str) -> Result>>; /// Get the last record for a given host and tag async fn last(&self, host: Uuid, tag: &str) -> Result>>; + + async fn tail_records(&self) -> Result>; } diff --git a/atuin-client/src/record/sync.rs b/atuin-client/src/record/sync.rs new file mode 100644 index 00000000..3eec7522 --- /dev/null +++ b/atuin-client/src/record/sync.rs @@ -0,0 +1,25 @@ +use atuin_common::record::RecordIndex; +// do a sync :O +use eyre::Result; +use uuid::Uuid; + +use crate::{api_client::Client, settings::Settings}; + +use super::store::Store; + +pub async fn diff( + settings: &Settings, + store: &mut impl Store, +) -> Result> { + let client = Client::new(&settings.sync_address, &settings.session_token)?; + + // First, build our own index + let local_tail = store.tail_records().await?; + let local_index = RecordIndex::from(local_tail); + + let remote_index = client.record_index().await?; + + let diff = local_index.diff(&remote_index); + + Ok(diff) +} diff --git a/atuin-common/src/record.rs b/atuin-common/src/record.rs index 5073ccf3..a9959d13 100644 --- a/atuin-common/src/record.rs +++ b/atuin-common/src/record.rs @@ -84,6 +84,17 @@ impl Default for RecordIndex { } } +impl From> for RecordIndex { + fn from(f: Vec<(Uuid, String, Uuid)>) -> RecordIndex { + let mut record_index = RecordIndex::new(); + + for row in f { + record_index.set_raw(row.0, row.1, row.2); + } + record_index + } +} + impl RecordIndex { pub fn new() -> RecordIndex { RecordIndex { @@ -114,7 +125,7 @@ impl RecordIndex { /// other machine has a different tail, it will be the differing tail. This is useful to /// check if the other index is ahead of us, or behind. /// If the other index does not have the (host, tag) pair, then the other value will be None. - pub fn diff(&self, other: &Self) -> Vec<(Uuid, String, Option)> { + pub fn diff(&self, other: &Self) -> Vec<(Uuid, String, Uuid)> { let mut ret = Vec::new(); // First, we check if other has everything that self has @@ -125,10 +136,10 @@ impl RecordIndex { Some(t) if t.eq(tail) => continue, // The other store does exist, but it is either ahead or behind us. A diff regardless - Some(t) => ret.push((host.clone(), tag.clone(), Some(t))), + Some(t) => ret.push((host.clone(), tag.clone(), t)), // The other store does not exist :O - None => ret.push((host.clone(), tag.clone(), None)), + None => ret.push((host.clone(), tag.clone(), tail.clone())), }; } } @@ -143,7 +154,7 @@ impl RecordIndex { // If we have this host/tag combo, the comparison and diff will have already happened above Some(_) => continue, - None => ret.push((host.clone(), tag.clone(), Some(tail.clone()))), + None => ret.push((host.clone(), tag.clone(), tail.clone())), }; } } @@ -311,7 +322,7 @@ mod tests { let diff = index1.diff(&index2); assert_eq!(1, diff.len(), "expected single diff"); - assert_eq!(diff[0], (record2.host, record2.tag, Some(record2.id))); + assert_eq!(diff[0], (record2.host, record2.tag, record2.id)); } #[test] diff --git a/atuin/src/command/client/sync.rs b/atuin/src/command/client/sync.rs index d2850898..feec9be0 100644 --- a/atuin/src/command/client/sync.rs +++ b/atuin/src/command/client/sync.rs @@ -1,7 +1,12 @@ use clap::Subcommand; use eyre::{Result, WrapErr}; -use atuin_client::{api_client, database::Database, record::store::Store, settings::Settings}; +use atuin_client::{ + api_client, + database::Database, + record::{store::Store, sync}, + settings::Settings, +}; mod status; @@ -73,11 +78,14 @@ async fn run( db: &mut impl Database, store: &mut impl Store, ) -> Result<()> { - let host = Settings::host_id().expect("No host ID found"); - // FOR TESTING ONLY! - let kv_tail = store.last(host, "kv").await?.expect("no kv found"); - let client = api_client::Client::new(&settings.sync_address, &settings.session_token)?; - client.post_records(&[kv_tail]).await?; + let diff = sync::diff(settings, store).await?; + println!("{:?}", diff); + atuin_client::sync::sync(settings, force, db).await?; + println!( + "Sync complete! {} items in database, force: {}", + db.history_count().await?, + force + ); Ok(()) }