use aide_axum_typed_multipart::TypedMultipart; use anyhow::Context as _; use axum::extract::{Path, State}; use axum_extra::{ TypedHeader, headers::{Authorization, authorization::Bearer}, }; use axum_jsonschema::Json; use chrono::{DateTime, Utc}; use log::info; use schemars::JsonSchema; use serde::Deserialize; use sync_lib::{base64_to_bytes, merge}; use super::{ app_state::AppState, auth::auth, requests::{CreateDocumentVersion, CreateDocumentVersionMultipart}, responses::DocumentUpdateResponse, }; use crate::{ database::models::{StoredDocumentVersion, VaultId}, errors::{SyncServerError, client_error, server_error}, utils::sanitize_path, }; // This is required for aide to infer the path parameter types and names #[derive(Deserialize, JsonSchema)] 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_multipart( TypedHeader(auth_header): TypedHeader>, Path(PathParams { vault_id }): Path, State(state): State, TypedMultipart(axum_typed_multipart::TypedMultipart(request)): TypedMultipart< CreateDocumentVersionMultipart, >, ) -> Result, SyncServerError> { internal_create_document( auth_header, state, vault_id, request.relative_path, request.created_date, request.content.contents.to_vec(), ) .await } /// 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_json( TypedHeader(auth_header): TypedHeader>, Path(PathParams { vault_id }): Path, State(state): State, Json(request): Json, ) -> Result, SyncServerError> { let content_bytes = base64_to_bytes(&request.content_base64) .context("Failed to decode base64 content in request") .map_err(client_error)?; internal_create_document( auth_header, state, vault_id, request.relative_path, request.created_date, content_bytes, ) .await } async fn internal_create_document( auth_header: Authorization, state: AppState, vault_id: VaultId, relative_path: String, created_date: DateTime, content: Vec, ) -> Result, SyncServerError> { auth(&state, auth_header.token())?; let mut transaction = state .database .create_write_transaction() .await .map_err(server_error)?; let last_update_id = state .database .get_max_update_id_in_vault(&vault_id, Some(&mut transaction)) .await .map_err(server_error)?; let sanitized_relative_path = sanitize_path(&relative_path); let maybe_existing_version = state .database .get_latest_document_by_path(&vault_id, &sanitized_relative_path, Some(&mut transaction)) .await .map_err(server_error)? .and_then(|doc| if doc.is_deleted { None } else { Some(doc) }); let response = if let Some(existing_version) = maybe_existing_version { if content == existing_version.content { info!( "Content of the new version is the same as the existing version. Not creating a \ new version." ); transaction .rollback() .await .context("Failed to roll back unecceseary transaction") .map_err(server_error)?; return Ok(Json(DocumentUpdateResponse::FastForwardUpdate( existing_version.into(), ))); } let merged_content = merge( &[], // the empty string is the first common parent of the two documents, &existing_version.content, &content, ); let new_version = StoredDocumentVersion { vault_id, vault_update_id: last_update_id + 1, relative_path: sanitized_relative_path, document_id: existing_version.document_id, content: merged_content, created_date, updated_date: chrono::Utc::now(), is_deleted: false, }; state .database .insert_document_version(&new_version, Some(&mut transaction)) .await .map_err(server_error)?; DocumentUpdateResponse::MergingUpdate(new_version.into()) } else { let new_version = StoredDocumentVersion { vault_id, vault_update_id: last_update_id + 1, document_id: uuid::Uuid::new_v4(), relative_path: sanitized_relative_path, content, created_date, updated_date: chrono::Utc::now(), is_deleted: false, }; state .database .insert_document_version(&new_version, Some(&mut transaction)) .await .map_err(server_error)?; DocumentUpdateResponse::FastForwardUpdate(new_version.into()) }; transaction .commit() .await .context("Failed to commit successful transaction") .map_err(server_error)?; Ok(Json(response)) }