summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@elliehuxtable.com>2024-02-02 15:05:07 +0000
committerEllie Huxtable <ellie@elliehuxtable.com>2024-02-02 18:01:09 +0000
commitc9a453289e2fea97b5ac17e265f99332edcdcd4c (patch)
treee51eb1f7d2fd3b44fabf2c1260802232fa0367b2
parent3c420f85f69771db269c015ead8e1678a4ad6899 (diff)
feat: add `store push --force`
This will 1. Wipe the remote store 2. Upload all of the local store to remote Imagine the scenario where you end up with some mixed keys locally :( You confirm this with ``` atuin store verify ``` You then fix it locally with ``` atuin store purge ``` Ensure that your local changes are reflected remotely with ``` atuin store push --force ``` and then (another PR, coming soon), update all other hosts with ``` atuin store pull --force ```
-rw-r--r--atuin-client/src/api_client.rs11
-rw-r--r--atuin-server-database/src/lib.rs3
-rw-r--r--atuin-server-postgres/src/lib.rs13
-rw-r--r--atuin-server/src/handlers/v0/mod.rs1
-rw-r--r--atuin-server/src/handlers/v0/store.rs37
-rw-r--r--atuin-server/src/router.rs3
-rw-r--r--atuin/src/command/client/store/push.rs28
7 files changed, 94 insertions, 2 deletions
diff --git a/atuin-client/src/api_client.rs b/atuin-client/src/api_client.rs
index d53c9a36..709627b6 100644
--- a/atuin-client/src/api_client.rs
+++ b/atuin-client/src/api_client.rs
@@ -287,6 +287,17 @@ impl<'a> Client<'a> {
Ok(())
}
+ pub async fn delete_store(&self) -> Result<()> {
+ let url = format!("{}/api/v0/store", self.sync_addr);
+ let url = Url::parse(url.as_str())?;
+
+ let resp = self.client.delete(url).send().await?;
+
+ handle_resp_error(resp).await?;
+
+ Ok(())
+ }
+
pub async fn post_records(&self, records: &[Record<EncryptedData>]) -> Result<()> {
let url = format!("{}/api/v0/record", self.sync_addr);
let url = Url::parse(url.as_str())?;
diff --git a/atuin-server-database/src/lib.rs b/atuin-server-database/src/lib.rs
index dff1204d..d2c16b3d 100644
--- a/atuin-server-database/src/lib.rs
+++ b/atuin-server-database/src/lib.rs
@@ -53,15 +53,16 @@ pub trait Database: Sized + Clone + Send + Sync + 'static {
async fn get_user(&self, username: &str) -> DbResult<User>;
async fn get_user_session(&self, u: &User) -> DbResult<Session>;
async fn add_user(&self, user: &NewUser) -> DbResult<i64>;
- async fn delete_user(&self, u: &User) -> DbResult<()>;
async fn update_user_password(&self, u: &User) -> DbResult<()>;
async fn total_history(&self) -> DbResult<i64>;
async fn count_history(&self, user: &User) -> DbResult<i64>;
async fn count_history_cached(&self, user: &User) -> DbResult<i64>;
+ async fn delete_user(&self, u: &User) -> DbResult<()>;
async fn delete_history(&self, user: &User, id: String) -> DbResult<()>;
async fn deleted_history(&self, user: &User) -> DbResult<Vec<String>>;
+ async fn delete_store(&self, user: &User) -> DbResult<()>;
async fn add_records(&self, user: &User, record: &[Record<EncryptedData>]) -> DbResult<()>;
async fn next_records(
diff --git a/atuin-server-postgres/src/lib.rs b/atuin-server-postgres/src/lib.rs
index 1f7cf47a..0ad33076 100644
--- a/atuin-server-postgres/src/lib.rs
+++ b/atuin-server-postgres/src/lib.rs
@@ -133,6 +133,19 @@ impl Database for Postgres {
Ok(res.0 as i64)
}
+ async fn delete_store(&self, user: &User) -> DbResult<()> {
+ sqlx::query(
+ "delete from store
+ where user_id = $1",
+ )
+ .bind(user.id)
+ .execute(&self.pool)
+ .await
+ .map_err(fix_error)?;
+
+ Ok(())
+ }
+
async fn delete_history(&self, user: &User, id: String) -> DbResult<()> {
sqlx::query(
"update history
diff --git a/atuin-server/src/handlers/v0/mod.rs b/atuin-server/src/handlers/v0/mod.rs
index 78fb47b8..2d6745cf 100644
--- a/atuin-server/src/handlers/v0/mod.rs
+++ b/atuin-server/src/handlers/v0/mod.rs
@@ -1 +1,2 @@
pub(crate) mod record;
+pub(crate) mod store;
diff --git a/atuin-server/src/handlers/v0/store.rs b/atuin-server/src/handlers/v0/store.rs
new file mode 100644
index 00000000..941f2487
--- /dev/null
+++ b/atuin-server/src/handlers/v0/store.rs
@@ -0,0 +1,37 @@
+use axum::{extract::Query, extract::State, http::StatusCode};
+use metrics::counter;
+use serde::Deserialize;
+use tracing::{error, instrument};
+
+use crate::{
+ handlers::{ErrorResponse, ErrorResponseStatus, RespExt},
+ router::{AppState, UserAuth},
+};
+use atuin_server_database::Database;
+
+#[derive(Deserialize)]
+pub struct DeleteParams {}
+
+#[instrument(skip_all, fields(user.id = user.id))]
+pub async fn delete<DB: Database>(
+ _params: Query<DeleteParams>,
+ UserAuth(user): UserAuth,
+ state: State<AppState<DB>>,
+) -> Result<(), ErrorResponseStatus<'static>> {
+ let State(AppState {
+ database,
+ settings: _,
+ }) = state;
+
+ if let Err(e) = database.delete_store(&user).await {
+ counter!("atuin_store_delete_failed", 1);
+ error!("failed to delete store {e:?}");
+
+ return Err(ErrorResponse::reply("failed to delete store")
+ .with_status(StatusCode::INTERNAL_SERVER_ERROR));
+ }
+
+ counter!("atuin_store_deleted", 1);
+
+ Ok(())
+}
diff --git a/atuin-server/src/router.rs b/atuin-server/src/router.rs
index 74df229a..52fc1484 100644
--- a/atuin-server/src/router.rs
+++ b/atuin-server/src/router.rs
@@ -127,7 +127,8 @@ pub fn router<DB: Database>(database: DB, settings: Settings<DB::Settings>) -> R
.route("/record/next", get(handlers::record::next))
.route("/api/v0/record", post(handlers::v0::record::post))
.route("/api/v0/record", get(handlers::v0::record::index))
- .route("/api/v0/record/next", get(handlers::v0::record::next));
+ .route("/api/v0/record/next", get(handlers::v0::record::next))
+ .route("/api/v0/store", delete(handlers::v0::store::delete));
let path = settings.path.as_str();
if path.is_empty() {
diff --git a/atuin/src/command/client/store/push.rs b/atuin/src/command/client/store/push.rs
index a5bd2dd0..17a72f2a 100644
--- a/atuin/src/command/client/store/push.rs
+++ b/atuin/src/command/client/store/push.rs
@@ -4,6 +4,7 @@ use eyre::Result;
use uuid::Uuid;
use atuin_client::{
+ api_client::Client,
record::sync::Operation,
record::{sqlite_store::SqliteStore, sync},
settings::Settings,
@@ -18,11 +19,34 @@ pub struct Push {
/// 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,
}
impl Push {
pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> {
let host_id = Settings::host_id().expect("failed to get host_id");
+
+ if self.force {
+ println!("Forcing remote store overwrite!");
+ println!("Clearing remote store");
+
+ let client = Client::new(
+ &settings.sync_address,
+ &settings.session_token,
+ 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
@@ -40,6 +64,10 @@ impl Push {
// 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;