From e2f79edad6fe805bccd67e8127f3032563870002 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Wed, 29 Sep 2021 23:52:58 +0000 Subject: [PATCH] add get_snapshot API method --- sync-server/src/api/get_snapshot.rs | 111 ++++++++++++++++++++++++++++ sync-server/src/api/mod.rs | 5 ++ sync-server/src/server.rs | 56 ++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 sync-server/src/api/get_snapshot.rs diff --git a/sync-server/src/api/get_snapshot.rs b/sync-server/src/api/get_snapshot.rs new file mode 100644 index 000000000..6de0be427 --- /dev/null +++ b/sync-server/src/api/get_snapshot.rs @@ -0,0 +1,111 @@ +use crate::api::{ + client_key_header, failure_to_ise, ServerState, SNAPSHOT_CONTENT_TYPE, VERSION_ID_HEADER, +}; +use crate::server::get_snapshot; +use actix_web::{error, get, web, HttpRequest, HttpResponse, Result}; + +/// Get a snapshot. +/// +/// If a snapshot for this client exists, it is returned with content-type +/// `application/vnd.taskchampion.snapshot`. The `X-Version-Id` header contains the version of the +/// snapshot. +/// +/// If no snapshot exists, returns a 404 with no content. Returns other 4xx or 5xx responses on +/// other errors. +#[get("/v1/client/snapshot")] +pub(crate) async fn service( + req: HttpRequest, + server_state: web::Data, +) -> Result { + let mut txn = server_state.txn().map_err(failure_to_ise)?; + + let client_key = client_key_header(&req)?; + + let client = txn + .get_client(client_key) + .map_err(failure_to_ise)? + .ok_or_else(|| error::ErrorNotFound("no such client"))?; + + if let Some((version_id, data)) = + get_snapshot(txn, client_key, client).map_err(failure_to_ise)? + { + Ok(HttpResponse::Ok() + .content_type(SNAPSHOT_CONTENT_TYPE) + .header(VERSION_ID_HEADER, version_id.to_string()) + .body(data)) + } else { + Err(error::ErrorNotFound("no snapshot")) + } +} + +#[cfg(test)] +mod test { + use crate::storage::{InMemoryStorage, Snapshot, Storage}; + use crate::Server; + use actix_web::{http::StatusCode, test, App}; + use chrono::{TimeZone, Utc}; + use pretty_assertions::assert_eq; + use uuid::Uuid; + + #[actix_rt::test] + async fn test_not_found() { + let client_key = Uuid::new_v4(); + let storage: Box = Box::new(InMemoryStorage::new()); + + // set up the storage contents.. + { + let mut txn = storage.txn().unwrap(); + txn.new_client(client_key, Uuid::new_v4()).unwrap(); + } + + let server = Server::new(storage); + let mut app = test::init_service(App::new().service(server.service())).await; + + let uri = "/v1/client/snapshot"; + let req = test::TestRequest::get() + .uri(uri) + .header("X-Client-Key", client_key.to_string()) + .to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn test_success() { + let client_key = Uuid::new_v4(); + let version_id = Uuid::new_v4(); + let snapshot_data = vec![1, 2, 3, 4]; + let storage: Box = Box::new(InMemoryStorage::new()); + + // set up the storage contents.. + { + let mut txn = storage.txn().unwrap(); + txn.new_client(client_key, Uuid::new_v4()).unwrap(); + txn.set_snapshot( + client_key, + Snapshot { + version_id, + versions_since: 3, + timestamp: Utc.ymd(2001, 9, 9).and_hms(1, 46, 40), + }, + snapshot_data.clone(), + ) + .unwrap(); + } + + let server = Server::new(storage); + let mut app = test::init_service(App::new().service(server.service())).await; + + let uri = "/v1/client/snapshot"; + let req = test::TestRequest::get() + .uri(uri) + .header("X-Client-Key", client_key.to_string()) + .to_request(); + let mut resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + use futures::StreamExt; + let (bytes, _) = resp.take_body().into_future().await; + assert_eq!(bytes.unwrap().unwrap().as_ref(), snapshot_data); + } +} diff --git a/sync-server/src/api/mod.rs b/sync-server/src/api/mod.rs index 917dda83e..87666a35a 100644 --- a/sync-server/src/api/mod.rs +++ b/sync-server/src/api/mod.rs @@ -5,11 +5,15 @@ use std::sync::Arc; mod add_version; mod get_child_version; +mod get_snapshot; /// The content-type for history segments (opaque blobs of bytes) pub(crate) const HISTORY_SEGMENT_CONTENT_TYPE: &str = "application/vnd.taskchampion.history-segment"; +/// The content-type for snapshots (opaque blobs of bytes) +pub(crate) const SNAPSHOT_CONTENT_TYPE: &str = "application/vnd.taskchampion.snapshot"; + /// The header name for version ID pub(crate) const VERSION_ID_HEADER: &str = "X-Version-Id"; @@ -26,6 +30,7 @@ pub(crate) fn api_scope() -> Scope { web::scope("") .service(get_child_version::service) .service(add_version::service) + .service(get_snapshot::service) } /// Convert a failure::Error to an Actix ISE diff --git a/sync-server/src/server.rs b/sync-server/src/server.rs index 2b8f92445..0ed3f4e7b 100644 --- a/sync-server/src/server.rs +++ b/sync-server/src/server.rs @@ -105,6 +105,20 @@ pub(crate) fn add_version<'a>( Ok(AddVersionResult::Ok(version_id)) } +/// Implementation of the GetSnapshot protocol transaction +pub(crate) fn get_snapshot<'a>( + mut txn: Box, + client_key: ClientKey, + client: Client, +) -> anyhow::Result)>> { + Ok(if let Some(snap) = client.snapshot { + txn.get_snapshot_data(client_key, snap.version_id)? + .map(|data| (snap.version_id, data)) + } else { + None + }) +} + #[cfg(test)] mod test { use super::*; @@ -302,4 +316,46 @@ mod test { fn av_success_nil_latest_version_id() -> anyhow::Result<()> { test_av_success(false) } + + #[test] + fn get_snapshot_found() -> anyhow::Result<()> { + let storage = InMemoryStorage::new(); + let mut txn = storage.txn()?; + let client_key = Uuid::new_v4(); + let data = vec![1, 2, 3]; + let snapshot_version_id = Uuid::new_v4(); + + txn.new_client(client_key, snapshot_version_id)?; + txn.set_snapshot( + client_key, + Snapshot { + version_id: snapshot_version_id, + versions_since: 3, + timestamp: Utc.ymd(2001, 9, 9).and_hms(1, 46, 40), + }, + data.clone(), + )?; + + let client = txn.get_client(client_key)?.unwrap(); + assert_eq!( + get_snapshot(txn, client_key, client)?, + Some((snapshot_version_id, data.clone())) + ); + + Ok(()) + } + + #[test] + fn get_snapshot_not_found() -> anyhow::Result<()> { + let storage = InMemoryStorage::new(); + let mut txn = storage.txn()?; + let client_key = Uuid::new_v4(); + + txn.new_client(client_key, NO_VERSION_ID)?; + let client = txn.get_client(client_key)?.unwrap(); + + assert_eq!(get_snapshot(txn, client_key, client)?, None); + + Ok(()) + } }