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
```
This commit is contained in:
Ellie Huxtable 2024-02-02 15:05:07 +00:00
parent 3c420f85f6
commit c9a453289e
7 changed files with 94 additions and 2 deletions

View File

@ -287,6 +287,17 @@ impl<'a> Client<'a> {
Ok(()) 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<()> { pub async fn post_records(&self, records: &[Record<EncryptedData>]) -> Result<()> {
let url = format!("{}/api/v0/record", self.sync_addr); let url = format!("{}/api/v0/record", self.sync_addr);
let url = Url::parse(url.as_str())?; let url = Url::parse(url.as_str())?;

View File

@ -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(&self, username: &str) -> DbResult<User>;
async fn get_user_session(&self, u: &User) -> DbResult<Session>; async fn get_user_session(&self, u: &User) -> DbResult<Session>;
async fn add_user(&self, user: &NewUser) -> DbResult<i64>; 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 update_user_password(&self, u: &User) -> DbResult<()>;
async fn total_history(&self) -> DbResult<i64>; async fn total_history(&self) -> DbResult<i64>;
async fn count_history(&self, user: &User) -> DbResult<i64>; async fn count_history(&self, user: &User) -> DbResult<i64>;
async fn count_history_cached(&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 delete_history(&self, user: &User, id: String) -> DbResult<()>;
async fn deleted_history(&self, user: &User) -> DbResult<Vec<String>>; 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 add_records(&self, user: &User, record: &[Record<EncryptedData>]) -> DbResult<()>;
async fn next_records( async fn next_records(

View File

@ -133,6 +133,19 @@ impl Database for Postgres {
Ok(res.0 as i64) 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<()> { async fn delete_history(&self, user: &User, id: String) -> DbResult<()> {
sqlx::query( sqlx::query(
"update history "update history

View File

@ -1 +1,2 @@
pub(crate) mod record; pub(crate) mod record;
pub(crate) mod store;

View File

@ -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(())
}

View File

@ -127,7 +127,8 @@ pub fn router<DB: Database>(database: DB, settings: Settings<DB::Settings>) -> R
.route("/record/next", get(handlers::record::next)) .route("/record/next", get(handlers::record::next))
.route("/api/v0/record", post(handlers::v0::record::post)) .route("/api/v0/record", post(handlers::v0::record::post))
.route("/api/v0/record", get(handlers::v0::record::index)) .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(); let path = settings.path.as_str();
if path.is_empty() { if path.is_empty() {

View File

@ -4,6 +4,7 @@ use eyre::Result;
use uuid::Uuid; use uuid::Uuid;
use atuin_client::{ use atuin_client::{
api_client::Client,
record::sync::Operation, record::sync::Operation,
record::{sqlite_store::SqliteStore, sync}, record::{sqlite_store::SqliteStore, sync},
settings::Settings, 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. /// The host to push, in the form of a UUID host ID. Defaults to the current host.
#[arg(long)] #[arg(long)]
pub host: Option<Uuid>, 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 { impl Push {
pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> { pub async fn run(&self, settings: &Settings, store: SqliteStore) -> Result<()> {
let host_id = Settings::host_id().expect("failed to get host_id"); 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 // We can actually just use the existing diff/etc to push
// 1. Diff // 1. Diff
// 2. Get operations // 2. Get operations
@ -40,6 +64,10 @@ impl Push {
// push, so yes plz to uploads! // push, so yes plz to uploads!
Operation::Upload { host, tag, .. } => { Operation::Upload { host, tag, .. } => {
if self.force {
return true;
}
if let Some(h) = self.host { if let Some(h) = self.host {
if HostId(h) != *host { if HostId(h) != *host {
return false; return false;