diff --git a/README.md b/README.md index 0be09835..848373b6 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ - add clap - add auth middleware - add request logs +- the is deleted logic is bad and we'll always read the previous version instead of the deleted - CI for: - publish reconcile - cross-platform build server diff --git a/backend/sync_server/src/database.rs b/backend/sync_server/src/database.rs index 9a92c36e..901c844c 100644 --- a/backend/sync_server/src/database.rs +++ b/backend/sync_server/src/database.rs @@ -129,6 +129,42 @@ impl Database { .context("Cannot fetch latest document version") } + pub async fn get_latest_document_version_by_path( + &self, + vault: &VaultId, + relative_path: &str, + transaction: Option<&mut Transaction<'_>>, + ) -> Result> { + let query = sqlx::query_as!( + StoredDocumentVersion, + r#" + select + vault_id, + document_id as "document_id: uuid::Uuid", + version_id, + created_date as "created_date: chrono::DateTime", + updated_date as "updated_date: chrono::DateTime", + relative_path, + content, + is_binary, + is_deleted + from documents + where vault_id = ? and relative_path = ? and is_deleted = false + ORDER BY version_id DESC + LIMIT 1 + "#, + vault, + relative_path + ); + + if let Some(transaction) = transaction { + query.fetch_optional(&mut **transaction).await + } else { + query.fetch_optional(&self.connection_pool).await + } + .context("Cannot fetch latest document version by path") + } + pub async fn get_document_version( &self, vault: &VaultId, diff --git a/backend/sync_server/src/server/create_document.rs b/backend/sync_server/src/server/create_document.rs index 3dd5a89f..479969f1 100644 --- a/backend/sync_server/src/server/create_document.rs +++ b/backend/sync_server/src/server/create_document.rs @@ -9,12 +9,12 @@ use axum_extra::{ }; use schemars::JsonSchema; use serde::Deserialize; -use sync_lib::base64_to_bytes; +use sync_lib::{base64_to_bytes, base64_to_string}; use super::{auth::auth, requests::CreateDocumentVersion}; use crate::{ app_state::AppState, - database::models::{DocumentVersionWithoutContent, StoredDocumentVersion, VaultId}, + database::models::{DocumentVersion, StoredDocumentVersion, VaultId}, errors::{client_error, server_error, SyncServerError}, }; @@ -24,34 +24,88 @@ pub struct PathParams { vault_id: VaultId, } +/// Create a new document in case a document with the same doesn't exist +/// already. If a document with the same path exists, a new version is created +/// with their content merged. #[axum::debug_handler] pub async fn create_document( TypedHeader(auth_header): TypedHeader>, Path(PathParams { vault_id }): Path, State(state): State, Json(request): Json, -) -> Result, SyncServerError> { +) -> Result, SyncServerError> { auth(&state, auth_header.token())?; - let new_version = StoredDocumentVersion { - vault_id, - document_id: uuid::Uuid::new_v4(), - version_id: 0, - content: base64_to_bytes(&request.content_base64) - .context("Cannot convert base64 encoded content to bytes") - .map_err(client_error)?, - created_date: request.created_date, - relative_path: request.relative_path, - updated_date: chrono::Utc::now(), - is_binary: request.is_binary, - is_deleted: false, + let mut transaction = state + .database + .create_transaction() + .await + .map_err(server_error)?; + + let maybe_existing_version = state + .database + .get_latest_document_version_by_path( + &vault_id, + &request.relative_path, + Some(&mut transaction), + ) + .await + .map_err(server_error)?; + + let new_version = if let Some(existing_version) = maybe_existing_version { + let merged_content = if request.is_binary { + base64_to_bytes(&request.content_base64) + .context("Failed to decode base64 content in request") + .map_err(client_error)? + } else { + reconcile::reconcile( + "", // the empty string is the first common parent of the two documents + &existing_version.content_as_string(), + &base64_to_string(&request.content_base64) + .context("Failed to decode base64 content in request") + .map_err(client_error)?, + ) + .into_bytes() + }; + + StoredDocumentVersion { + vault_id, + document_id: existing_version.document_id, + version_id: existing_version.version_id + 1, + content: merged_content, + created_date: request.created_date, + relative_path: request.relative_path, + updated_date: chrono::Utc::now(), + is_binary: request.is_binary, + is_deleted: false, + } + } else { + StoredDocumentVersion { + vault_id, + document_id: uuid::Uuid::new_v4(), + version_id: 0, + content: base64_to_bytes(&request.content_base64) + .context("Cannot convert base64 encoded content to bytes") + .map_err(client_error)?, + created_date: request.created_date, + relative_path: request.relative_path, + updated_date: chrono::Utc::now(), + is_binary: request.is_binary, + is_deleted: false, + } }; state .database - .insert_document_version(&new_version, None) + .insert_document_version(&new_version, Some(&mut transaction)) .await .map_err(server_error)?; + transaction + .commit() + .await + .context("Failed to commit successful transaction") + .map_err(server_error)?; + Ok(Json(new_version.into())) }