Remove aide

This commit is contained in:
Andras Schmelczer 2025-06-07 17:15:16 +01:00
parent 0908a5b527
commit 4ca55768c5
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
15 changed files with 85 additions and 562 deletions

View file

@ -18,22 +18,18 @@ log = { version = "0.4.27" }
anyhow = { version = "1.0.98", features = ["backtrace"] }
axum = { version = "0.7.4", features = ["ws", "macros", "tracing", "multipart"]}
axum-extra = { version = "0.9.6", features = ["typed-header"] }
aide-axum-typed-multipart = "0.13.0"
axum_typed_multipart = "0.11.0"
tower-http = { version = "0.6.1", features = ["cors", "trace", "limit", "timeout"] }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["fmt", "env-filter"]}
serde_yaml = "0.9.34"
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "uuid", "chrono"] }
chrono = { version = "0.4.41", features = ["serde"] }
aide = { version = "0.13.5", features = ["axum", "axum-ws", "scalar", "axum-headers"] }
schemars = { version = "0.8.22", features = ["chrono", "uuid1", "bytes"] }
tracing = "0.1.41"
rand = "0.9.0"
sanitize-filename = "0.6.0"
axum-jsonschema = { version = "0.8.0", features = ["aide"] }
regex = "1.11.1"
clap = { version = "4.5.38", features = ["derive"] }
futures = "0.3.31"
serde_yaml = "0.9.34"
serde_json = "1.0.140"
clap-verbosity-flag = "3.0.3"
bimap = "0.6.3"

View file

@ -1,5 +1,4 @@
use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::Serialize;
use sync_lib::bytes_to_base64;
use ts_rs::TS;
@ -27,7 +26,7 @@ impl PartialEq<Self> for StoredDocumentVersion {
fn eq(&self, other: &Self) -> bool { self.vault_update_id == other.vault_update_id }
}
#[derive(TS, Debug, Clone, Serialize, JsonSchema)]
#[derive(TS, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentVersionWithoutContent {
#[ts(as = "i32")]
@ -59,7 +58,7 @@ impl From<StoredDocumentVersion> for DocumentVersionWithoutContent {
}
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[derive(TS, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentVersion {
pub vault_update_id: VaultUpdateId,

View file

@ -12,29 +12,20 @@ mod responses;
mod update_document;
mod websocket;
use std::{ffi::OsString, sync::Arc, time::Duration};
use std::{ffi::OsString, time::Duration};
use aide::{
axum::{
ApiRouter,
routing::{delete, get, post, put},
},
openapi::{Info, OpenApi},
scalar::Scalar,
transform::TransformOpenApi,
};
use anyhow::{Context as _, Result, anyhow};
use auth::auth_middleware;
use axum::{
Extension, Json,
Router,
extract::{DefaultBodyLimit, Request},
http::{self, HeaderValue, Method},
middleware,
response::IntoResponse,
routing::IntoMakeService,
routing::{IntoMakeService, delete, get, post, put},
};
use device_id_header::DEVICE_ID_HEADER_NAME;
use log::{error, info};
use log::info;
use tokio::signal;
use tower_http::{
LatencyUnit,
@ -51,26 +42,20 @@ use tracing::{Level, info_span};
use crate::{
app_state::AppState,
config::server_config::ServerConfig,
errors::{SerializedError, client_error, not_found_error},
errors::{client_error, not_found_error},
};
pub async fn create_server(config_path: Option<OsString>) -> Result<()> {
aide::r#gen::on_error(|err| error!("{err}"));
aide::r#gen::extract_schemas(true);
let app_state = AppState::try_new(config_path)
.await
.context("Failed to initialise app state")?;
let server_config = app_state.config.server.clone();
let mut api = create_open_api();
let app = ApiRouter::new()
let app = Router::new()
.nest("/", get_authed_routes(app_state.clone()))
.api_route("/vaults/:vault_id/ping", get(ping::ping))
.route("/vaults/:vault_id/ping", get(ping::ping))
.route("/vaults/:vault_id/ws", get(websocket::websocket_handler))
.route("/", Scalar::new("/api.json").axum_route())
.route("/api.json", axum::routing::get(serve_api))
.layer(DefaultBodyLimit::disable())
.layer(RequestBodyLimitLayer::new(
app_state.config.server.max_body_size_mb * 1024 * 1024,
@ -108,8 +93,6 @@ pub async fn create_server(config_path: Option<OsString>) -> Result<()> {
.on_failure(DefaultOnFailure::new().level(Level::ERROR)),
)
.with_state(app_state)
.finish_api_with(&mut api, add_api_docs_error_example)
.layer(Extension(Arc::new(api))) // https://github.com/tamasfe/aide/blob/507f4a8822bc0c13cbda0f589da1e0f4cbcdb812/examples/example-axum/src/main.rs#L39
.fallback(handle_404)
.fallback(handle_405)
.into_make_service();
@ -117,67 +100,33 @@ pub async fn create_server(config_path: Option<OsString>) -> Result<()> {
start_server(app, &server_config).await
}
async fn serve_api(Extension(api): Extension<Arc<OpenApi>>) -> impl IntoResponse { Json(api) }
fn create_open_api() -> OpenApi {
OpenApi {
info: Info {
title: "VaultLink sync server".to_owned(),
summary: Some(
"Simple API for syncing documents between concurrent clients.".to_owned(),
),
description: Some(include_str!("../README.md").to_owned()),
version: env!("CARGO_PKG_VERSION").to_owned(),
..Info::default()
},
..OpenApi::default()
}
}
fn add_api_docs_error_example(api: TransformOpenApi<'_>) -> TransformOpenApi<'_> {
api.default_response_with::<Json<SerializedError>, _>(|res| {
res.example(SerializedError {
message: "An error has occurred".to_owned(),
causes: vec![],
})
})
}
fn get_authed_routes(app_state: AppState) -> ApiRouter<AppState> {
ApiRouter::new()
.api_route(
fn get_authed_routes(app_state: AppState) -> Router<AppState> {
Router::new()
.route(
"/vaults/:vault_id/documents",
get(fetch_latest_documents::fetch_latest_documents),
)
.api_route(
.route(
"/vaults/:vault_id/documents",
post(create_document::create_document_multipart),
post(create_document::create_document),
)
.api_route(
"/vaults/:vault_id/documents/json",
post(create_document::create_document_json),
)
.api_route(
.route(
"/vaults/:vault_id/documents/:document_id",
get(fetch_latest_document_version::fetch_latest_document_version),
)
.api_route(
.route(
"/vaults/:vault_id/documents/:document_id",
put(update_document::update_document_multipart),
put(update_document::update_document),
)
.api_route(
"/vaults/:vault_id/documents/:document_id/json",
put(update_document::update_document_json),
)
.api_route(
.route(
"/vaults/:vault_id/documents/:document_id/versions/:version_id",
put(fetch_document_version::fetch_document_version),
)
.api_route(
.route(
"/vaults/:vault_id/documents/:document_id/versions/:version_id/content",
put(fetch_document_version_content::fetch_document_version_content),
)
.api_route(
.route(
"/vaults/:vault_id/documents/:document_id",
delete(delete_document::delete_document),
)

View file

@ -1,33 +1,24 @@
use aide_axum_typed_multipart::TypedMultipart;
use anyhow::Context as _;
use axum::{
Extension,
Extension, Json,
extract::{Path, State},
};
use axum_extra::TypedHeader;
use axum_jsonschema::Json;
use schemars::JsonSchema;
use axum_typed_multipart::TypedMultipart;
use serde::Deserialize;
use sync_lib::base64_to_bytes;
use super::{
device_id_header::DeviceIdHeader,
requests::{CreateDocumentVersion, CreateDocumentVersionMultipart},
};
use super::{device_id_header::DeviceIdHeader, requests::CreateDocumentVersion};
use crate::{
app_state::{
AppState,
database::models::{
DocumentId, DocumentVersionWithoutContent, StoredDocumentVersion, VaultId,
},
database::models::{DocumentVersionWithoutContent, StoredDocumentVersion, VaultId},
},
config::user_config::User,
errors::{SyncServerError, client_error, server_error},
utils::{normalize::normalize, sanitize_path::sanitize_path},
};
// This is required for aide to infer the path parameter types and names
#[derive(Deserialize, JsonSchema)]
#[derive(Deserialize)]
pub struct CreateDocumentPathParams {
#[serde(deserialize_with = "normalize")]
vault_id: VaultId,
@ -37,63 +28,12 @@ pub struct CreateDocumentPathParams {
/// 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(
pub async fn create_document(
Path(CreateDocumentPathParams { vault_id }): Path<CreateDocumentPathParams>,
Extension(user): Extension<User>,
TypedHeader(device_id): TypedHeader<DeviceIdHeader>,
State(state): State<AppState>,
TypedMultipart(axum_typed_multipart::TypedMultipart(request)): TypedMultipart<
CreateDocumentVersionMultipart,
>,
) -> Result<Json<DocumentVersionWithoutContent>, SyncServerError> {
internal_create_document(
user,
device_id,
state,
vault_id,
request.document_id,
request.relative_path,
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(
Path(CreateDocumentPathParams { vault_id }): Path<CreateDocumentPathParams>,
Extension(user): Extension<User>,
TypedHeader(device_id): TypedHeader<DeviceIdHeader>,
State(state): State<AppState>,
Json(request): Json<CreateDocumentVersion>,
) -> Result<Json<DocumentVersionWithoutContent>, 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(
user,
device_id,
state,
vault_id,
request.document_id,
request.relative_path,
content_bytes,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn internal_create_document(
user: User,
device_id: DeviceIdHeader,
state: AppState,
vault_id: VaultId,
document_id: Option<DocumentId>,
relative_path: String,
content: Vec<u8>,
TypedMultipart(request): TypedMultipart<CreateDocumentVersion>,
) -> Result<Json<DocumentVersionWithoutContent>, SyncServerError> {
let mut transaction = state
.database
@ -101,7 +41,7 @@ async fn internal_create_document(
.await
.map_err(server_error)?;
let document_id = match document_id {
let document_id = match request.document_id {
Some(document_id) => {
let existing_version = state
.database
@ -126,13 +66,13 @@ async fn internal_create_document(
.await
.map_err(server_error)?;
let sanitized_relative_path = sanitize_path(&relative_path);
let sanitized_relative_path = sanitize_path(&request.relative_path);
let new_version = StoredDocumentVersion {
vault_update_id: last_update_id + 1,
document_id,
relative_path: sanitized_relative_path,
content,
content: request.content.contents.to_vec(),
updated_date: chrono::Utc::now(),
is_deleted: false,
user_id: user.name,

View file

@ -1,11 +1,9 @@
use anyhow::Context as _;
use axum::{
Extension,
Extension, Json,
extract::{Path, State},
};
use axum_extra::TypedHeader;
use axum_jsonschema::Json;
use schemars::JsonSchema;
use serde::Deserialize;
use super::{device_id_header::DeviceIdHeader, requests::DeleteDocumentVersion};
@ -21,8 +19,7 @@ use crate::{
utils::{normalize::normalize, sanitize_path::sanitize_path},
};
// This is required for aide to infer the path parameter types and names
#[derive(Deserialize, JsonSchema)]
#[derive(Deserialize)]
pub struct DeleteDocumentPathParams {
#[serde(deserialize_with = "normalize")]
vault_id: VaultId,

View file

@ -1,7 +1,8 @@
use anyhow::anyhow;
use axum::extract::{Path, State};
use axum_jsonschema::Json;
use schemars::JsonSchema;
use axum::{
Json,
extract::{Path, State},
};
use serde::Deserialize;
use crate::{
@ -13,8 +14,7 @@ use crate::{
utils::normalize::normalize,
};
// This is required for aide to infer the path parameter types and names
#[derive(Deserialize, JsonSchema)]
#[derive(Deserialize)]
pub struct FetchDocumentVersionPathParams {
#[serde(deserialize_with = "normalize")]
vault_id: VaultId,

View file

@ -3,7 +3,6 @@ use axum::{
body::Bytes,
extract::{Path, State},
};
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{
@ -15,8 +14,7 @@ use crate::{
utils::normalize::normalize,
};
// This is required for aide to infer the path parameter types and names
#[derive(Deserialize, JsonSchema)]
#[derive(Deserialize)]
pub struct FetchDocumentVersionContentPathParams {
#[serde(deserialize_with = "normalize")]
vault_id: VaultId,

View file

@ -1,7 +1,8 @@
use anyhow::anyhow;
use axum::extract::{Path, State};
use axum_jsonschema::Json;
use schemars::JsonSchema;
use axum::{
Json,
extract::{Path, State},
};
use serde::Deserialize;
use crate::{
@ -13,8 +14,7 @@ use crate::{
utils::normalize::normalize,
};
// This is required for aide to infer the path parameter types and names
#[derive(Deserialize, JsonSchema)]
#[derive(Deserialize)]
pub struct FetchLatestDocumentVersionPathParams {
#[serde(deserialize_with = "normalize")]
vault_id: VaultId,

View file

@ -1,6 +1,7 @@
use axum::extract::{Path, Query, State};
use axum_jsonschema::Json;
use schemars::JsonSchema;
use axum::{
Json,
extract::{Path, Query, State},
};
use serde::Deserialize;
use super::responses::FetchLatestDocumentsResponse;
@ -13,15 +14,13 @@ use crate::{
utils::normalize::normalize,
};
// This is required for aide to infer the path parameter types and names
#[derive(Deserialize, JsonSchema)]
#[derive(Deserialize)]
pub struct FetchLatestDocumentsPathParams {
#[serde(deserialize_with = "normalize")]
vault_id: VaultId,
}
// This is required for aide to infer the path parameter types and names
#[derive(Deserialize, JsonSchema)]
#[derive(Deserialize)]
pub struct QueryParams {
since_update_id: Option<VaultUpdateId>,
}

View file

@ -6,7 +6,6 @@ use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use schemars::JsonSchema;
use serde::Deserialize;
use super::{auth::auth, responses::PingResponse};
@ -16,8 +15,7 @@ use crate::{
utils::normalize::normalize,
};
// This is required for aide to infer the path parameter types and names
#[derive(Deserialize, JsonSchema)]
#[derive(Deserialize)]
pub struct PingPathParams {
#[serde(deserialize_with = "normalize")]
vault_id: VaultId,

View file

@ -1,13 +1,12 @@
use aide_axum_typed_multipart::FieldData;
use axum::body::Bytes;
use axum_typed_multipart::TryFromMultipart;
use schemars::JsonSchema;
use axum_typed_multipart::{FieldData, TryFromMultipart};
use serde::{self, Deserialize};
use ts_rs::TS;
use crate::app_state::database::models::{DocumentId, VaultUpdateId};
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[derive(TS, Debug, TryFromMultipart)]
#[ts(export)]
pub struct CreateDocumentVersion {
/// The client can decide the document id (if it wishes to) in order
/// to help with syncing. If the client does not provide a document id,
@ -15,36 +14,26 @@ pub struct CreateDocumentVersion {
/// it must not already exist in the database.
pub document_id: Option<DocumentId>,
pub relative_path: String,
pub content_base64: String,
}
#[derive(Debug, TryFromMultipart, JsonSchema)]
pub struct CreateDocumentVersionMultipart {
pub document_id: Option<DocumentId>,
pub relative_path: String,
#[ts(as = "Vec<u8>")]
#[form_data(limit = "unlimited")]
pub content: FieldData<Bytes>,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[derive(TS, Debug, TryFromMultipart)]
#[ts(export)]
pub struct UpdateDocumentVersion {
pub parent_version_id: VaultUpdateId,
pub relative_path: String,
pub content_base64: String,
}
#[derive(Debug, TryFromMultipart, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateDocumentVersionMultipart {
pub parent_version_id: VaultUpdateId,
pub relative_path: String,
#[ts(as = "Vec<u8>")]
#[form_data(limit = "unlimited")]
pub content: FieldData<Bytes>,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[derive(TS, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct DeleteDocumentVersion {
pub relative_path: String,
}

View file

@ -1,13 +1,14 @@
use schemars::JsonSchema;
use serde::{self, Serialize};
use ts_rs::TS;
use crate::app_state::database::models::{
DocumentVersion, DocumentVersionWithoutContent, VaultUpdateId,
};
/// Response to a ping request.
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[derive(TS, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct PingResponse {
/// Semantic version of the server.
pub server_version: String,
@ -18,8 +19,9 @@ pub struct PingResponse {
}
/// Response to a fetch latest documents request.
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[derive(TS, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct FetchLatestDocumentsResponse {
pub latest_documents: Vec<DocumentVersionWithoutContent>,
@ -28,8 +30,9 @@ pub struct FetchLatestDocumentsResponse {
}
/// Response to an update document request.
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[derive(TS, Debug, Clone, Serialize)]
#[serde(tag = "type")]
#[ts(export)]
pub enum DocumentUpdateResponse {
/// Returned when the created/updated document's content is the same as was
/// sent in the create/update request and thus the response doesn't contain

View file

@ -1,33 +1,29 @@
use aide_axum_typed_multipart::TypedMultipart;
use anyhow::{Context as _, anyhow};
use axum::{
Extension,
Extension, Json,
extract::{Path, State},
};
use axum_extra::TypedHeader;
use axum_jsonschema::Json;
use axum_typed_multipart::TypedMultipart;
use log::info;
use schemars::JsonSchema;
use serde::Deserialize;
use sync_lib::{base64_to_bytes, is_file_type_mergable, merge};
use sync_lib::{is_file_type_mergable, merge};
use super::{
device_id_header::DeviceIdHeader,
requests::{UpdateDocumentVersion, UpdateDocumentVersionMultipart},
device_id_header::DeviceIdHeader, requests::UpdateDocumentVersion,
responses::DocumentUpdateResponse,
};
use crate::{
app_state::{
AppState,
database::models::{DocumentId, StoredDocumentVersion, VaultId, VaultUpdateId},
database::models::{DocumentId, StoredDocumentVersion, VaultId},
},
config::user_config::User,
errors::{SyncServerError, client_error, not_found_error, server_error},
errors::{SyncServerError, not_found_error, server_error},
utils::{dedup_paths::dedup_paths, normalize::normalize, sanitize_path::sanitize_path},
};
// This is required for aide to infer the path parameter types and names
#[derive(Deserialize, JsonSchema)]
#[derive(Deserialize)]
pub struct UpdateDocumentPathParams {
#[serde(deserialize_with = "normalize")]
vault_id: VaultId,
@ -36,7 +32,8 @@ pub struct UpdateDocumentPathParams {
}
#[axum::debug_handler]
pub async fn update_document_multipart(
#[allow(clippy::too_many_lines)]
pub async fn update_document(
Path(UpdateDocumentPathParams {
vault_id,
document_id,
@ -44,79 +41,25 @@ pub async fn update_document_multipart(
Extension(user): Extension<User>,
TypedHeader(device_id): TypedHeader<DeviceIdHeader>,
State(state): State<AppState>,
TypedMultipart(axum_typed_multipart::TypedMultipart(request)): TypedMultipart<
UpdateDocumentVersionMultipart,
>,
) -> Result<Json<DocumentUpdateResponse>, SyncServerError> {
internal_update_document(
user,
device_id,
state,
vault_id,
document_id,
request.parent_version_id,
request.relative_path,
request.content.contents.to_vec(),
)
.await
}
#[axum::debug_handler]
pub async fn update_document_json(
Path(UpdateDocumentPathParams {
vault_id,
document_id,
}): Path<UpdateDocumentPathParams>,
Extension(user): Extension<User>,
TypedHeader(device_id): TypedHeader<DeviceIdHeader>,
State(state): State<AppState>,
Json(request): Json<UpdateDocumentVersion>,
) -> Result<Json<DocumentUpdateResponse>, SyncServerError> {
let content_bytes = base64_to_bytes(&request.content_base64)
.context("Failed to decode base64 content in request")
.map_err(client_error)?;
internal_update_document(
user,
device_id,
state,
vault_id,
document_id,
request.parent_version_id,
request.relative_path,
content_bytes,
)
.await
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
async fn internal_update_document(
user: User,
device_id: DeviceIdHeader,
state: AppState,
vault_id: VaultId,
document_id: DocumentId,
parent_version_id: VaultUpdateId,
relative_path: String,
content: Vec<u8>,
TypedMultipart(request): TypedMultipart<UpdateDocumentVersion>,
) -> Result<Json<DocumentUpdateResponse>, SyncServerError> {
// No need for a transaction as document versions are immutable
let parent_document = state
.database
.get_document_version(&vault_id, parent_version_id, None)
.get_document_version(&vault_id, request.parent_version_id, None)
.await
.map_err(server_error)?
.map_or_else(
|| {
Err(not_found_error(anyhow!(
"Parent version with id `{}` not found",
parent_version_id
request.parent_version_id
)))
},
Ok,
)?;
let sanitized_relative_path = sanitize_path(&relative_path);
let sanitized_relative_path = sanitize_path(&request.relative_path);
let mut transaction = state
.database
@ -156,6 +99,8 @@ async fn internal_update_document(
)));
}
let content = request.content.contents.to_vec();
// Return the latest version if the content and path are the same as the latest
// version
if content == latest_version.content && sanitized_relative_path == latest_version.relative_path