Add API for propagating cursor locations #61

Merged
schmelczer merged 30 commits from asch/show-cursors into main 2025-06-08 20:20:53 +01:00
15 changed files with 85 additions and 562 deletions
Showing only changes of commit 4ca55768c5 - Show all commits

Remove aide

Andras Schmelczer 2025-06-07 17:15:16 +01:00
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C

286
backend/Cargo.lock generated
View file

@ -17,20 +17,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"getrandom 0.2.15",
"once_cell",
"serde",
"version_check",
"zerocopy 0.7.35",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@ -40,41 +26,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "aide"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5678d2978845ddb4bd736a026f467dd652d831e9e6254b0e41b07f7ee7523309"
dependencies = [
"axum",
"axum-extra",
"bytes",
"cfg-if",
"http",
"indexmap 2.7.0",
"schemars",
"serde",
"serde_json",
"serde_qs",
"thiserror 1.0.69",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "aide-axum-typed-multipart"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b8f5c830a08754addfa31fa09e6c183bac8d2ae7bd007131f9eb84fcb87a40e"
dependencies = [
"aide",
"axum",
"axum_typed_multipart",
"indexmap 2.7.0",
"schemars",
]
[[package]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.21" version = "0.2.21"
@ -265,26 +216,6 @@ dependencies = [
"tower-service", "tower-service",
] ]
[[package]]
name = "axum-jsonschema"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcffe29ca1b60172349fea781ec34441d598809bd227ccbb5bf5dc2879cd9c78"
dependencies = [
"aide",
"async-trait",
"axum",
"http",
"http-body",
"itertools",
"jsonschema",
"schemars",
"serde",
"serde_json",
"serde_path_to_error",
"tracing",
]
[[package]] [[package]]
name = "axum-macros" name = "axum-macros"
version = "0.4.2" version = "0.4.2"
@ -368,21 +299,6 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.6.0" version = "2.6.0"
@ -407,12 +323,6 @@ version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytecount"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@ -700,12 +610,6 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dyn-clone"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]] [[package]]
name = "either" name = "either"
version = "1.13.0" version = "1.13.0"
@ -768,16 +672,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "fancy-regex"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
dependencies = [
"bit-set",
"regex",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.2.0" version = "2.2.0"
@ -816,16 +710,6 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fraction"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678"
dependencies = [
"lazy_static",
"num",
]
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.31" version = "0.3.31"
@ -943,10 +827,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -1346,24 +1228,6 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "iso8601"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153"
dependencies = [
"nom",
]
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.14" version = "1.0.14"
@ -1380,34 +1244,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "jsonschema"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978"
dependencies = [
"ahash",
"anyhow",
"base64 0.21.7",
"bytecount",
"fancy-regex",
"fraction",
"getrandom 0.2.15",
"iso8601",
"itoa",
"memchr",
"num-cmp",
"once_cell",
"parking_lot",
"percent-encoding",
"regex",
"serde",
"serde_json",
"time",
"url",
"uuid",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -1521,12 +1357,6 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.0" version = "0.8.0"
@ -1564,16 +1394,6 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.46.0"
@ -1584,30 +1404,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.4" version = "0.8.4"
@ -1625,21 +1421,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "num-cmp"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
@ -1666,17 +1447,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -2077,34 +1847,6 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "schemars"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
dependencies = [
"bytes",
"chrono",
"dyn-clone",
"indexmap 2.7.0",
"schemars_derive",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "schemars_derive"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.90",
]
[[package]] [[package]]
name = "scoped-tls" name = "scoped-tls"
version = "1.0.1" version = "1.0.1"
@ -2137,17 +1879,6 @@ dependencies = [
"syn 2.0.90", "syn 2.0.90",
] ]
[[package]]
name = "serde_derive_internals"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.140" version = "1.0.140"
@ -2170,19 +1901,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_qs"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6"
dependencies = [
"axum",
"futures",
"percent-encoding",
"serde",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -2610,12 +2328,9 @@ dependencies = [
name = "sync_server" name = "sync_server"
version = "0.3.15" version = "0.3.15"
dependencies = [ dependencies = [
"aide",
"aide-axum-typed-multipart",
"anyhow", "anyhow",
"axum", "axum",
"axum-extra", "axum-extra",
"axum-jsonschema",
"axum_typed_multipart", "axum_typed_multipart",
"bimap", "bimap",
"chrono", "chrono",
@ -2626,7 +2341,6 @@ dependencies = [
"rand 0.9.0", "rand 0.9.0",
"regex", "regex",
"sanitize-filename", "sanitize-filename",
"schemars",
"serde", "serde",
"serde_json", "serde_json",
"serde_with", "serde_with",

View file

@ -18,22 +18,18 @@ log = { version = "0.4.27" }
anyhow = { version = "1.0.98", features = ["backtrace"] } anyhow = { version = "1.0.98", features = ["backtrace"] }
axum = { version = "0.7.4", features = ["ws", "macros", "tracing", "multipart"]} axum = { version = "0.7.4", features = ["ws", "macros", "tracing", "multipart"]}
axum-extra = { version = "0.9.6", features = ["typed-header"] } axum-extra = { version = "0.9.6", features = ["typed-header"] }
aide-axum-typed-multipart = "0.13.0"
axum_typed_multipart = "0.11.0" axum_typed_multipart = "0.11.0"
tower-http = { version = "0.6.1", features = ["cors", "trace", "limit", "timeout"] } 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"]} 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"] } sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "uuid", "chrono"] }
chrono = { version = "0.4.41", features = ["serde"] } 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" rand = "0.9.0"
sanitize-filename = "0.6.0" sanitize-filename = "0.6.0"
axum-jsonschema = { version = "0.8.0", features = ["aide"] }
regex = "1.11.1" regex = "1.11.1"
clap = { version = "4.5.38", features = ["derive"] } clap = { version = "4.5.38", features = ["derive"] }
futures = "0.3.31" futures = "0.3.31"
serde_yaml = "0.9.34"
serde_json = "1.0.140" serde_json = "1.0.140"
clap-verbosity-flag = "3.0.3" clap-verbosity-flag = "3.0.3"
bimap = "0.6.3" bimap = "0.6.3"

View file

@ -1,5 +1,4 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::Serialize; use serde::Serialize;
use sync_lib::bytes_to_base64; use sync_lib::bytes_to_base64;
use ts_rs::TS; 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 } 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")] #[serde(rename_all = "camelCase")]
pub struct DocumentVersionWithoutContent { pub struct DocumentVersionWithoutContent {
#[ts(as = "i32")] #[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")] #[serde(rename_all = "camelCase")]
pub struct DocumentVersion { pub struct DocumentVersion {
pub vault_update_id: VaultUpdateId, pub vault_update_id: VaultUpdateId,

View file

@ -12,29 +12,20 @@ mod responses;
mod update_document; mod update_document;
mod websocket; 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 anyhow::{Context as _, Result, anyhow};
use auth::auth_middleware; use auth::auth_middleware;
use axum::{ use axum::{
Extension, Json, Router,
extract::{DefaultBodyLimit, Request}, extract::{DefaultBodyLimit, Request},
http::{self, HeaderValue, Method}, http::{self, HeaderValue, Method},
middleware, middleware,
response::IntoResponse, response::IntoResponse,
routing::IntoMakeService, routing::{IntoMakeService, delete, get, post, put},
}; };
use device_id_header::DEVICE_ID_HEADER_NAME; use device_id_header::DEVICE_ID_HEADER_NAME;
use log::{error, info}; use log::info;
use tokio::signal; use tokio::signal;
use tower_http::{ use tower_http::{
LatencyUnit, LatencyUnit,
@ -51,26 +42,20 @@ use tracing::{Level, info_span};
use crate::{ use crate::{
app_state::AppState, app_state::AppState,
config::server_config::ServerConfig, 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<()> { 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) let app_state = AppState::try_new(config_path)
.await .await
.context("Failed to initialise app state")?; .context("Failed to initialise app state")?;
let server_config = app_state.config.server.clone(); let server_config = app_state.config.server.clone();
let mut api = create_open_api(); let app = Router::new()
let app = ApiRouter::new()
.nest("/", get_authed_routes(app_state.clone())) .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("/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(DefaultBodyLimit::disable())
.layer(RequestBodyLimitLayer::new( .layer(RequestBodyLimitLayer::new(
app_state.config.server.max_body_size_mb * 1024 * 1024, 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)), .on_failure(DefaultOnFailure::new().level(Level::ERROR)),
) )
.with_state(app_state) .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_404)
.fallback(handle_405) .fallback(handle_405)
.into_make_service(); .into_make_service();
@ -117,67 +100,33 @@ pub async fn create_server(config_path: Option<OsString>) -> Result<()> {
start_server(app, &server_config).await start_server(app, &server_config).await
} }
async fn serve_api(Extension(api): Extension<Arc<OpenApi>>) -> impl IntoResponse { Json(api) } fn get_authed_routes(app_state: AppState) -> Router<AppState> {
Router::new()
fn create_open_api() -> OpenApi { .route(
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(
"/vaults/:vault_id/documents", "/vaults/:vault_id/documents",
get(fetch_latest_documents::fetch_latest_documents), get(fetch_latest_documents::fetch_latest_documents),
) )
.api_route( .route(
"/vaults/:vault_id/documents", "/vaults/:vault_id/documents",
post(create_document::create_document_multipart), post(create_document::create_document),
) )
.api_route( .route(
"/vaults/:vault_id/documents/json",
post(create_document::create_document_json),
)
.api_route(
"/vaults/:vault_id/documents/:document_id", "/vaults/:vault_id/documents/:document_id",
get(fetch_latest_document_version::fetch_latest_document_version), get(fetch_latest_document_version::fetch_latest_document_version),
) )
.api_route( .route(
"/vaults/:vault_id/documents/:document_id", "/vaults/:vault_id/documents/:document_id",
put(update_document::update_document_multipart), put(update_document::update_document),
) )
.api_route( .route(
"/vaults/:vault_id/documents/:document_id/json",
put(update_document::update_document_json),
)
.api_route(
"/vaults/:vault_id/documents/:document_id/versions/:version_id", "/vaults/:vault_id/documents/:document_id/versions/:version_id",
put(fetch_document_version::fetch_document_version), put(fetch_document_version::fetch_document_version),
) )
.api_route( .route(
"/vaults/:vault_id/documents/:document_id/versions/:version_id/content", "/vaults/:vault_id/documents/:document_id/versions/:version_id/content",
put(fetch_document_version_content::fetch_document_version_content), put(fetch_document_version_content::fetch_document_version_content),
) )
.api_route( .route(
"/vaults/:vault_id/documents/:document_id", "/vaults/:vault_id/documents/:document_id",
delete(delete_document::delete_document), delete(delete_document::delete_document),
) )

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,14 @@
use schemars::JsonSchema;
use serde::{self, Serialize}; use serde::{self, Serialize};
use ts_rs::TS;
use crate::app_state::database::models::{ use crate::app_state::database::models::{
DocumentVersion, DocumentVersionWithoutContent, VaultUpdateId, DocumentVersion, DocumentVersionWithoutContent, VaultUpdateId,
}; };
/// Response to a ping request. /// Response to a ping request.
#[derive(Debug, Clone, Serialize, JsonSchema)] #[derive(TS, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct PingResponse { pub struct PingResponse {
/// Semantic version of the server. /// Semantic version of the server.
pub server_version: String, pub server_version: String,
@ -18,8 +19,9 @@ pub struct PingResponse {
} }
/// Response to a fetch latest documents request. /// Response to a fetch latest documents request.
#[derive(Debug, Clone, Serialize, JsonSchema)] #[derive(TS, Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct FetchLatestDocumentsResponse { pub struct FetchLatestDocumentsResponse {
pub latest_documents: Vec<DocumentVersionWithoutContent>, pub latest_documents: Vec<DocumentVersionWithoutContent>,
@ -28,8 +30,9 @@ pub struct FetchLatestDocumentsResponse {
} }
/// Response to an update document request. /// Response to an update document request.
#[derive(Debug, Clone, Serialize, JsonSchema)] #[derive(TS, Debug, Clone, Serialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[ts(export)]
pub enum DocumentUpdateResponse { pub enum DocumentUpdateResponse {
/// Returned when the created/updated document's content is the same as was /// 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 /// 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 anyhow::{Context as _, anyhow};
use axum::{ use axum::{
Extension, Extension, Json,
extract::{Path, State}, extract::{Path, State},
}; };
use axum_extra::TypedHeader; use axum_extra::TypedHeader;
use axum_jsonschema::Json; use axum_typed_multipart::TypedMultipart;
use log::info; use log::info;
use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use sync_lib::{base64_to_bytes, is_file_type_mergable, merge}; use sync_lib::{is_file_type_mergable, merge};
use super::{ use super::{
device_id_header::DeviceIdHeader, device_id_header::DeviceIdHeader, requests::UpdateDocumentVersion,
requests::{UpdateDocumentVersion, UpdateDocumentVersionMultipart},
responses::DocumentUpdateResponse, responses::DocumentUpdateResponse,
}; };
use crate::{ use crate::{
app_state::{ app_state::{
AppState, AppState,
database::models::{DocumentId, StoredDocumentVersion, VaultId, VaultUpdateId}, database::models::{DocumentId, StoredDocumentVersion, VaultId},
}, },
config::user_config::User, 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}, 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)]
#[derive(Deserialize, JsonSchema)]
pub struct UpdateDocumentPathParams { pub struct UpdateDocumentPathParams {
#[serde(deserialize_with = "normalize")] #[serde(deserialize_with = "normalize")]
vault_id: VaultId, vault_id: VaultId,
@ -36,7 +32,8 @@ pub struct UpdateDocumentPathParams {
} }
#[axum::debug_handler] #[axum::debug_handler]
pub async fn update_document_multipart( #[allow(clippy::too_many_lines)]
pub async fn update_document(
Path(UpdateDocumentPathParams { Path(UpdateDocumentPathParams {
vault_id, vault_id,
document_id, document_id,
@ -44,79 +41,25 @@ pub async fn update_document_multipart(
Extension(user): Extension<User>, Extension(user): Extension<User>,
TypedHeader(device_id): TypedHeader<DeviceIdHeader>, TypedHeader(device_id): TypedHeader<DeviceIdHeader>,
State(state): State<AppState>, State(state): State<AppState>,
TypedMultipart(axum_typed_multipart::TypedMultipart(request)): TypedMultipart< TypedMultipart(request): TypedMultipart<UpdateDocumentVersion>,
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>,
) -> Result<Json<DocumentUpdateResponse>, SyncServerError> { ) -> Result<Json<DocumentUpdateResponse>, SyncServerError> {
// No need for a transaction as document versions are immutable // No need for a transaction as document versions are immutable
let parent_document = state let parent_document = state
.database .database
.get_document_version(&vault_id, parent_version_id, None) .get_document_version(&vault_id, request.parent_version_id, None)
.await .await
.map_err(server_error)? .map_err(server_error)?
.map_or_else( .map_or_else(
|| { || {
Err(not_found_error(anyhow!( Err(not_found_error(anyhow!(
"Parent version with id `{}` not found", "Parent version with id `{}` not found",
parent_version_id request.parent_version_id
))) )))
}, },
Ok, Ok,
)?; )?;
let sanitized_relative_path = sanitize_path(&relative_path); let sanitized_relative_path = sanitize_path(&request.relative_path);
let mut transaction = state let mut transaction = state
.database .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 // Return the latest version if the content and path are the same as the latest
// version // version
if content == latest_version.content && sanitized_relative_path == latest_version.relative_path if content == latest_version.content && sanitized_relative_path == latest_version.relative_path

View file

@ -3,13 +3,9 @@
set -e set -e
rm -rf backend/sync_server/bindings rm -rf backend/sync_server/bindings
cd backend cd backend
cargo test export_bindings cargo test export_bindings
cd - cd -
cp -r backend/sync_server/bindings/* frontend/sync-client/src/services/types/ cp -r backend/sync_server/bindings/* frontend/sync-client/src/services/types/
./scripts/utils/wait-for-server.sh
npm install -g openapi-typescript
openapi-typescript http://localhost:3000/api.json --output frontend/sync-client/src/services/types/http-api.ts