diff options
author | Ellie Huxtable <ellie@elliehuxtable.com> | 2024-02-02 15:05:07 +0000 |
---|---|---|
committer | Ellie Huxtable <ellie@elliehuxtable.com> | 2024-02-02 18:01:09 +0000 |
commit | c9a453289e2fea97b5ac17e265f99332edcdcd4c (patch) | |
tree | e51eb1f7d2fd3b44fabf2c1260802232fa0367b2 | |
parent | 3c420f85f69771db269c015ead8e1678a4ad6899 (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.rs | 11 | ||||
-rw-r--r-- | atuin-server-database/src/lib.rs | 3 | ||||
-rw-r--r-- | atuin-server-postgres/src/lib.rs | 13 | ||||
-rw-r--r-- | atuin-server/src/handlers/v0/mod.rs | 1 | ||||
-rw-r--r-- | atuin-server/src/handlers/v0/store.rs | 37 | ||||
-rw-r--r-- | atuin-server/src/router.rs | 3 | ||||
-rw-r--r-- | atuin/src/command/client/store/push.rs | 28 |
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; |