diff options
Diffstat (limited to 'src/api/store.rs')
| -rw-r--r-- | src/api/store.rs | 103 |
1 files changed, 81 insertions, 22 deletions
diff --git a/src/api/store.rs b/src/api/store.rs index 89123e2..f60d30d 100644 --- a/src/api/store.rs +++ b/src/api/store.rs @@ -6,10 +6,10 @@ use automerge::ChangeHash; use axum::{ Extension, body::{Body, Bytes}, - extract::{Path, Query, State}, + extract::{Path, Query, RawQuery, State}, http::{ HeaderMap, HeaderValue, StatusCode, - header::{ACCEPT, CONTENT_TYPE}, + header::{ACCEPT, CONTENT_TYPE, LOCATION}, }, response::{IntoResponse, Response}, }; @@ -39,13 +39,6 @@ pub(crate) struct ReadQuery { } #[derive(Debug, Deserialize)] -pub(crate) struct InsertQuery { - /// Where in the list: the items from there on move up one. At the end without one, - /// or with `-1`. - index: Option<i64>, -} - -#[derive(Debug, Deserialize)] pub(crate) struct DeleteQuery { #[serde(default)] prune: bool, @@ -103,8 +96,12 @@ pub(crate) async fn get_store_entry( Path(key): Path<String>, headers: HeaderMap, Query(query): Query<ReadQuery>, + RawQuery(raw_query): RawQuery, ) -> Result<Response, StatusCode> { let path = parse_key_path(&key)?; + if let Some(redirect) = to_positive(&state, &path, false, raw_query.as_deref()).await? { + return Ok(redirect); + } if query.shallow { return shallow(&state, path, false, &query).await; } @@ -201,6 +198,7 @@ pub(crate) async fn put_store_entry( State(state): State<AppState>, Extension(user): Extension<AuthenticatedUser>, Path(key): Path<String>, + RawQuery(raw_query): RawQuery, headers: HeaderMap, body: Bytes, ) -> Result<Response, StatusCode> { @@ -209,6 +207,9 @@ pub(crate) async fn put_store_entry( } let path = parse_write_path(&state, &user, &key, "PUT")?; + if let Some(redirect) = to_positive(&state, &path, false, raw_query.as_deref()).await? { + return Ok(redirect); + } let value = body_value(&headers, &body)?; let written = value.clone(); @@ -241,14 +242,15 @@ fn body_value(headers: &HeaderMap, body: &Bytes) -> Result<JsonValue, StatusCode } } -/// `POST /store/{key}`: inserts the body into the list at `key` (`store::insert_path`), -/// at `?index=` or at the end; `201` with where it went (`{"index": n}`). No list there, -/// or an index past its end: `409`, saying which. +/// `POST /store/{key}`: inserts the body into a list, `key` ending in the index it +/// goes to (`store::insert_path`: the items from there on move up; the length, or `-1`, +/// appends); `201` with where it went (`{"index": n}`). Not a list there, or an index +/// past its end: `409`, saying which. pub(crate) async fn post_store_entry( State(state): State<AppState>, Extension(user): Extension<AuthenticatedUser>, Path(key): Path<String>, - Query(query): Query<InsertQuery>, + RawQuery(raw_query): RawQuery, headers: HeaderMap, body: Bytes, ) -> Result<Response, StatusCode> { @@ -256,16 +258,16 @@ pub(crate) async fn post_store_entry( return Err(StatusCode::SERVICE_UNAVAILABLE); } let path = parse_write_path(&state, &user, &key, "POST")?; - let index = match query.index { - None | Some(-1) => None, - Some(index) => Some(usize::try_from(index).map_err(|_| StatusCode::BAD_REQUEST)?), - }; + if let Some(redirect) = to_positive(&state, &path, true, raw_query.as_deref()).await? { + return Ok(redirect); + } + let index = path.last().and_then(|leaf| store::list_index(leaf)); let value = body_value(&headers, &body)?; let inserted = state - .mutate_doc(move |doc| store::insert_path(doc, &path, index, &value)) + .mutate_doc(move |doc| store::insert_path(doc, &path, &value)) .await; match inserted { - Ok(index) => Ok(( + Ok(()) => Ok(( StatusCode::CREATED, axum::Json(serde_json::json!({ "index": index })), ) @@ -284,7 +286,8 @@ pub(crate) async fn delete_store_entry( Extension(user): Extension<AuthenticatedUser>, Path(key): Path<String>, Query(query): Query<DeleteQuery>, -) -> Result<StatusCode, StatusCode> { + RawQuery(raw_query): RawQuery, +) -> Result<Response, StatusCode> { let path = parse_write_path(&state, &user, &key, "DELETE")?; tracing::Span::current().record("path", path.join("/").as_str()); @@ -297,13 +300,69 @@ pub(crate) async fn delete_store_entry( return Err(StatusCode::BAD_REQUEST); } + if let Some(redirect) = to_positive(&state, &path, false, raw_query.as_deref()).await? { + return Ok(redirect); + } + let prune = query.prune; state - .mutate_doc(move |doc| store::delete_path(doc, &path, prune)) + .mutate_doc(move |doc| store::delete_at(doc, &path, prune)) .await .map_err(|e| store_status(e, "Failed to delete store value"))?; - Ok(StatusCode::NO_CONTENT) + Ok(StatusCode::NO_CONTENT.into_response()) +} + +/// A path with negative list indexes (`-1` the last item; for an insert, the end): a +/// `307` to it with them made positive (`store::positive_path`), the query kept, so a +/// client reads, writes or deletes what the index names now. None without any. +async fn to_positive( + state: &AppState, + path: &[String], + insertion: bool, + query: Option<&str>, +) -> Result<Option<Response>, StatusCode> { + if !path.iter().any(|segment| segment.starts_with('-')) { + return Ok(None); + } + let positive = { + let replica = state.doc.read().await; + store::positive_path(&replica.doc, path, insertion) + } + .map_err(|e| store_status(e, "Failed to resolve a negative index"))?; + let Some(positive) = positive else { + return Ok(None); + }; + let mut location = format!( + "/store/{}", + positive + .iter() + .map(|segment| encode_segment(segment)) + .collect::<Vec<_>>() + .join("/") + ); + if let Some(query) = query.filter(|query| !query.is_empty()) { + location.push('?'); + location.push_str(query); + } + let location = HeaderValue::from_str(&location).map_err(|_| StatusCode::BAD_REQUEST)?; + Ok(Some( + (StatusCode::TEMPORARY_REDIRECT, [(LOCATION, location)]).into_response(), + )) +} + +/// `segment` as a URL path segment: unreserved characters as they are, the rest +/// percent-encoded (UTF-8). +fn encode_segment(segment: &str) -> String { + let mut encoded = String::with_capacity(segment.len()); + for byte in segment.bytes() { + if byte.is_ascii_alphanumeric() || b"-._~".contains(&byte) { + encoded.push(char::from(byte)); + } else { + encoded.push_str(&format!("%{byte:02X}")); + } + } + encoded } // --- streams ----------------------------------------------------------------- |
