aboutsummaryrefslogtreecommitdiffstats
path: root/crates/turtle/src/command/client/store
diff options
context:
space:
mode:
Diffstat (limited to 'crates/turtle/src/command/client/store')
-rw-r--r--crates/turtle/src/command/client/store/pull.rs94
-rw-r--r--crates/turtle/src/command/client/store/purge.rs26
-rw-r--r--crates/turtle/src/command/client/store/push.rs112
-rw-r--r--crates/turtle/src/command/client/store/rebuild.rs58
-rw-r--r--crates/turtle/src/command/client/store/rekey.rs41
-rw-r--r--crates/turtle/src/command/client/store/verify.rs26
6 files changed, 357 insertions, 0 deletions
diff --git a/crates/turtle/src/command/client/store/pull.rs b/crates/turtle/src/command/client/store/pull.rs
new file mode 100644
index 00000000..c9c9c379
--- /dev/null
+++ b/crates/turtle/src/command/client/store/pull.rs
@@ -0,0 +1,94 @@
+use clap::Args;
+use eyre::Result;
+
+use crate::atuin_client::{
+ database::Database,
+ encryption::load_key,
+ record::store::Store,
+ record::sync::Operation,
+ record::{sqlite_store::SqliteStore, sync},
+ settings::Settings,
+};
+
+#[derive(Args, Debug)]
+pub struct Pull {
+ /// The tag to push (eg, 'history'). Defaults to all tags
+ #[arg(long, short)]
+ pub tag: Option<String>,
+
+ /// Force push records
+ /// This will first wipe the local store, and then download all records from the remote
+ #[arg(long, default_value = "false")]
+ pub force: bool,
+
+ /// Page Size
+ /// How many records to download at once. Defaults to 100
+ #[arg(long, default_value = "100")]
+ pub page: u64,
+}
+
+impl Pull {
+ pub async fn run(
+ &self,
+ settings: &Settings,
+ store: SqliteStore,
+ db: &dyn Database,
+ ) -> Result<()> {
+ if self.force {
+ println!("Forcing local overwrite!");
+ println!("Clearing local store");
+
+ store.delete_all().await?;
+ }
+
+ // We can actually just use the existing diff/etc to push
+ // 1. Diff
+ // 2. Get operations
+ // 3. Filter operations by
+ // a) are they a download op?
+ // b) are they for the host/tag we are pushing here?
+ let client = sync::build_client(settings).await?;
+ let (diff, remote_index) = sync::diff(&client, &store).await?;
+
+ // Skip on --force: local was already wiped above, mismatch is the user's call.
+ if !self.force {
+ let key: [u8; 32] = load_key(settings)?.into();
+ sync::check_encryption_key(&client, &remote_index, &key)
+ .await
+ .map_err(crate::print_error::format_sync_error)?;
+ }
+
+ let operations = sync::operations(diff, &store).await?;
+
+ let operations = operations
+ .into_iter()
+ .filter(|op| match op {
+ // No noops or downloads thx
+ Operation::Noop { .. } | Operation::Upload { .. } => false,
+
+ // pull, so yes plz to downloads!
+ Operation::Download { tag, .. } => {
+ if self.force {
+ return true;
+ }
+
+ if let Some(t) = self.tag.clone()
+ && t != *tag
+ {
+ return false;
+ }
+
+ true
+ }
+ })
+ .collect();
+
+ let (_, downloaded) = sync::sync_remote(&client, operations, &store, self.page).await?;
+
+ println!("Downloaded {} records", downloaded.len());
+
+ crate::sync::build(settings, &store, db, Some(&downloaded)).await?;
+
+ Ok(())
+ }
+}
diff --git a/crates/turtle/src/command/client/store/purge.rs b/crates/turtle/src/command/client/store/purge.rs
new file mode 100644
index 00000000..f7996c4b
--- /dev/null
+++ b/crates/turtle/src/command/client/store/purge.rs
@@ -0,0 +1,26 @@
+use clap::Args;
+use eyre::Result;
+
+use crate::atuin_client::{
+ encryption::load_key,
+ record::{sqlite_store::SqliteStore, store::Store},
+ settings::Settings,
+};
+
+#[derive(Args, Debug)]
+pub struct Purge {}
+
+impl Purge {
+ pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> {
+ println!("Purging local records that cannot be decrypted");
+
+ let key = load_key(settings)?;
+
+ match store.purge(&key.into()).await {
+ Ok(()) => println!("Local store purge completed OK"),
+ Err(e) => println!("Failed to purge local store: {e:?}"),
+ }
+
+ Ok(())
+ }
+}
diff --git a/crates/turtle/src/command/client/store/push.rs b/crates/turtle/src/command/client/store/push.rs
new file mode 100644
index 00000000..724dfbef
--- /dev/null
+++ b/crates/turtle/src/command/client/store/push.rs
@@ -0,0 +1,112 @@
+use crate::atuin_common::record::HostId;
+use clap::Args;
+use eyre::Result;
+use uuid::Uuid;
+
+use crate::atuin_client::{
+ api_client::Client,
+ encryption::load_key,
+ record::sync::Operation,
+ record::{sqlite_store::SqliteStore, sync},
+ settings::Settings,
+};
+
+#[derive(Args, Debug)]
+pub struct Push {
+ /// The tag to push (eg, 'history'). Defaults to all tags
+ #[arg(long, short)]
+ pub tag: Option<String>,
+
+ /// The host to push, in the form of a UUID host ID. Defaults to the current host.
+ #[arg(long)]
+ pub host: Option<Uuid>,
+
+ /// Force push records
+ /// This will override both host and tag, to be all hosts and all tags. First clear the remote store, then upload all of the
+ /// local store
+ #[arg(long, default_value = "false")]
+ pub force: bool,
+
+ /// Page Size
+ /// How many records to upload at once. Defaults to 100
+ #[arg(long, default_value = "100")]
+ pub page: u64,
+}
+
+impl Push {
+ pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> {
+ let host_id = Settings::host_id().await?;
+
+ if self.force {
+ println!("Forcing remote store overwrite!");
+ println!("Clearing remote store");
+
+ let client = Client::new(
+ &settings.sync_address,
+ settings.sync_auth_token().await?,
+ settings.network_connect_timeout,
+ settings.network_timeout * 10, // we may be deleting a lot of data... so up the
+ // timeout
+ )
+ .expect("failed to create client");
+
+ client.delete_store().await?;
+ }
+
+ // We can actually just use the existing diff/etc to push
+ // 1. Diff
+ // 2. Get operations
+ // 3. Filter operations by
+ // a) are they an upload op?
+ // b) are they for the host/tag we are pushing here?
+ let client = sync::build_client(settings).await?;
+ let (diff, remote_index) = sync::diff(&client, &store).await?;
+
+ // Skip on --force: that path intentionally replaces remote with local.
+ if !self.force {
+ let key: [u8; 32] = load_key(settings)?.into();
+ sync::check_encryption_key(&client, &remote_index, &key)
+ .await
+ .map_err(crate::print_error::format_sync_error)?;
+ }
+
+ let operations = sync::operations(diff, &store).await?;
+
+ let operations = operations
+ .into_iter()
+ .filter(|op| match op {
+ // No noops or downloads thx
+ Operation::Noop { .. } | Operation::Download { .. } => false,
+
+ // push, so yes plz to uploads!
+ Operation::Upload { host, tag, .. } => {
+ if self.force {
+ return true;
+ }
+
+ if let Some(h) = self.host {
+ if HostId(h) != *host {
+ return false;
+ }
+ } else if *host != host_id {
+ return false;
+ }
+
+ if let Some(t) = self.tag.clone()
+ && t != *tag
+ {
+ return false;
+ }
+
+ true
+ }
+ })
+ .collect();
+
+ let (uploaded, _) = sync::sync_remote(&client, operations, &store, self.page).await?;
+
+ println!("Uploaded {uploaded} records");
+
+ Ok(())
+ }
+}
diff --git a/crates/turtle/src/command/client/store/rebuild.rs b/crates/turtle/src/command/client/store/rebuild.rs
new file mode 100644
index 00000000..80e201c2
--- /dev/null
+++ b/crates/turtle/src/command/client/store/rebuild.rs
@@ -0,0 +1,58 @@
+use clap::Args;
+use eyre::{Result, bail};
+
+#[cfg(feature = "daemon")]
+use crate::command::client::daemon as daemon_cmd;
+
+use crate::atuin_client::{
+ database::Database, encryption, history::store::HistoryStore,
+ record::sqlite_store::SqliteStore, settings::Settings,
+};
+
+#[derive(Args, Debug)]
+pub struct Rebuild {
+ pub tag: String,
+}
+
+impl Rebuild {
+ pub async fn run(
+ &self,
+ settings: &Settings,
+ store: SqliteStore,
+ database: &dyn Database,
+ ) -> Result<()> {
+ // keep it as a string and not an enum atm
+ // would be super cool to build this dynamically in the future
+ // eg register handles for rebuilding various tags without having to make this part of the
+ // binary big
+ match self.tag.as_str() {
+ "history" => {
+ self.rebuild_history(settings, store.clone(), database)
+ .await?;
+ }
+
+ tag => bail!("unknown tag: {tag}"),
+ }
+
+ Ok(())
+ }
+
+ async fn rebuild_history(
+ &self,
+ settings: &Settings,
+ store: SqliteStore,
+ database: &dyn Database,
+ ) -> Result<()> {
+ let encryption_key: [u8; 32] = encryption::load_key(settings)?.into();
+
+ let host_id = Settings::host_id().await?;
+ let history_store = HistoryStore::new(store, host_id, encryption_key);
+
+ history_store.build(database).await?;
+
+ #[cfg(feature = "daemon")]
+ daemon_cmd::emit_event(settings, crate::atuin_daemon::DaemonEvent::HistoryRebuilt).await;
+
+ Ok(())
+ }
+}
diff --git a/crates/turtle/src/command/client/store/rekey.rs b/crates/turtle/src/command/client/store/rekey.rs
new file mode 100644
index 00000000..e63be447
--- /dev/null
+++ b/crates/turtle/src/command/client/store/rekey.rs
@@ -0,0 +1,41 @@
+use clap::Args;
+use eyre::Result;
+use tokio::{fs::File, io::AsyncWriteExt};
+
+use crate::atuin_client::{
+ encryption::{decode_key, generate_encoded_key, load_key},
+ record::sqlite_store::SqliteStore,
+ record::store::Store,
+ settings::Settings,
+};
+
+#[derive(Args, Debug)]
+pub struct Rekey {
+ /// The new key to use for encryption. Omit for a randomly-generated key
+ key: Option<String>,
+}
+
+impl Rekey {
+ pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> {
+ let key = if let Some(key) = self.key.clone() {
+ println!("Re-encrypting store with specified key");
+
+ key
+ } else {
+ println!("Re-encrypting store with freshly-generated key");
+ let (_, encoded) = generate_encoded_key()?;
+ encoded
+ };
+
+ let current_key: [u8; 32] = load_key(settings)?.into();
+ let new_key: [u8; 32] = decode_key(key.clone())?.into();
+
+ store.re_encrypt(&current_key, &new_key).await?;
+
+ println!("Store rewritten. Saving new key");
+ let mut file = File::create(settings.key_path.clone()).await?;
+ file.write_all(key.as_bytes()).await?;
+
+ Ok(())
+ }
+}
diff --git a/crates/turtle/src/command/client/store/verify.rs b/crates/turtle/src/command/client/store/verify.rs
new file mode 100644
index 00000000..5aa1dc70
--- /dev/null
+++ b/crates/turtle/src/command/client/store/verify.rs
@@ -0,0 +1,26 @@
+use clap::Args;
+use eyre::Result;
+
+use crate::atuin_client::{
+ encryption::load_key,
+ record::{sqlite_store::SqliteStore, store::Store},
+ settings::Settings,
+};
+
+#[derive(Args, Debug)]
+pub struct Verify {}
+
+impl Verify {
+ pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> {
+ println!("Verifying local store can be decrypted with the current key");
+
+ let key = load_key(settings)?;
+
+ match store.verify(&key.into()).await {
+ Ok(()) => println!("Local store encryption verified OK"),
+ Err(e) => println!("Failed to verify local store encryption: {e:?}"),
+ }
+
+ Ok(())
+ }
+}