From 861c40c96ebd29fd5560cb917322ca801062e349 Mon Sep 17 00:00:00 2001 From: asonix Date: Fri, 11 Dec 2020 22:33:56 -0600 Subject: [PATCH] Handle errors better (render a page), use program start time as last modified for static assets --- src/lib.rs | 175 ++++++++++++++++++++++++++++------------ src/middleware.rs | 7 +- src/store.rs | 33 ++++---- templates/error.rs.html | 18 +++++ 4 files changed, 164 insertions(+), 69 deletions(-) create mode 100644 templates/error.rs.html diff --git a/src/lib.rs b/src/lib.rs index 271e53f..6c15e65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,6 +62,7 @@ pub struct State { scope: String, store: Store, db: Db, + startup: SystemTime, } impl State { @@ -165,6 +166,7 @@ pub fn state(config: Config, scope: &str, db: Db) -> Result scope: scope.to_string(), store: Store::new(&db)?, db, + startup: SystemTime::now(), }) } @@ -225,12 +227,28 @@ fn to_home(state: &State) -> HttpResponse { .finish() } +trait ResultExt { + fn stateful(self, state: &web::Data) -> Result; +} + +impl ResultExt for Result +where + Error: From, +{ + fn stateful(self, state: &web::Data) -> Result { + self.map_err(Error::from).map_err(|error| StateError { + state: state.clone(), + error, + }) + } +} + async fn static_files(filename: web::Path, state: web::Data) -> HttpResponse { let filename = filename.into_inner(); if let Some(data) = self::templates::statics::StaticFile::get(&filename) { return HttpResponse::Ok() - .set(LastModified(SystemTime::now().into())) + .set(LastModified(state.startup.into())) .set(CacheControl(vec![ CacheDirective::Public, CacheDirective::MaxAge(365 * DAYS), @@ -243,16 +261,53 @@ async fn static_files(filename: web::Path, state: web::Data) -> H to_404(&state) } -async fn not_found(state: web::Data) -> Result { +async fn not_found(state: web::Data) -> Result { let mut cursor = Cursor::new(vec![]); - self::templates::not_found(&mut cursor, &state)?; + self::templates::not_found(&mut cursor, &state).stateful(&state)?; Ok(HttpResponse::NotFound() .content_type(mime::TEXT_HTML.essence_str()) .body(cursor.into_inner())) } +struct StateError { + state: web::Data, + error: Error, +} + +impl std::fmt::Debug for StateError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct("StateError") + .field("error", &self.error) + .finish() + } +} + +impl std::fmt::Display for StateError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.error) + } +} + +impl ResponseError for StateError { + fn status_code(&self) -> StatusCode { + StatusCode::INTERNAL_SERVER_ERROR + } + + fn error_response(&self) -> HttpResponse { + let mut cursor = Cursor::new(vec![]); + match self::templates::error(&mut cursor, &self.error.to_string(), &self.state) { + Ok(_) => HttpResponse::build(self.status_code()) + .content_type(mime::TEXT_HTML.essence_str()) + .body(cursor.into_inner()), + Err(_) => HttpResponse::build(self.status_code()) + .content_type(mime::TEXT_PLAIN.essence_str()) + .body(self.error.to_string()), + } + } +} + #[derive(Debug, thiserror::Error)] enum Error { #[error("{0}")] @@ -268,23 +323,6 @@ enum Error { UploadString(String), } -impl ResponseError for Error { - fn status_code(&self) -> StatusCode { - StatusCode::INTERNAL_SERVER_ERROR - } - - fn error_response(&self) -> HttpResponse { - match self { - Self::Store(self::store::Error::NotFound) => HttpResponse::MovedPermanently() - .header(LOCATION, "/404") - .finish(), - _ => HttpResponse::build(self.status_code()) - .content_type(mime::TEXT_PLAIN.essence_str()) - .body(format!("{}", self)), - } - } -} - #[derive(serde::Deserialize, serde::Serialize)] pub struct Collection { title: String, @@ -370,17 +408,18 @@ async fn upload( token: ValidToken, conn: web::Data, state: web::Data, -) -> Result { - let images = conn.upload(&req, pl).await?; +) -> Result { + let images = conn.upload(&req, pl).await.stateful(&state)?; if images.is_err() { - return Err(Error::UploadString(images.message().to_owned())); + return Err(Error::UploadString(images.message().to_owned())).stateful(&state); } let image = images .files() .next() - .ok_or(Error::UploadString("Missing file".to_owned()))?; + .ok_or(Error::UploadString("Missing file".to_owned())) + .stateful(&state)?; let entry = Entry { title: String::new(), @@ -399,7 +438,8 @@ async fn upload( entry: &entry, } .exec(&state.store) - .await?; + .await + .stateful(&state)?; Ok(to_edit_page(path.collection, &token, &state)) } @@ -438,10 +478,10 @@ async fn thumbnail( .await } -async fn index(state: web::Data) -> Result { +async fn index(state: web::Data) -> Result { let mut cursor = Cursor::new(vec![]); - self::templates::index(&mut cursor, &state)?; + self::templates::index(&mut cursor, &state).stateful(&state)?; Ok(HttpResponse::Ok() .content_type(mime::TEXT_HTML.essence_str()) @@ -452,10 +492,12 @@ async fn collection( path: web::Path, token: Option, state: web::Data, -) -> Result { +) -> Result { match token { - Some(token) => edit_collection(path, token, state).await, - None => view_collection(path, state).await, + Some(token) => edit_collection(path, token, state.clone()) + .await + .stateful(&state), + None => view_collection(path, state.clone()).await.stateful(&state), } } @@ -463,7 +505,10 @@ async fn view_collection( path: web::Path, state: web::Data, ) -> Result { - let collection = state.store.collection(&path).await?; + let collection = match state.store.collection(&path).await? { + Some(collection) => collection, + None => return Ok(to_404(&state)), + }; let entries = state.store.entries(path.entry_range()).await?; let mut cursor = Cursor::new(vec![]); @@ -480,7 +525,10 @@ async fn edit_collection( token: ValidToken, state: web::Data, ) -> Result { - let collection = state.store.collection(&path).await?; + let collection = match state.store.collection(&path).await? { + Some(collection) => collection, + None => return Ok(to_404(&state)), + }; let entries = state.store.entries(path.entry_range()).await?; let mut cursor = Cursor::new(vec![]); @@ -502,7 +550,7 @@ async fn edit_collection( async fn create_collection( collection: web::Form, state: web::Data, -) -> Result { +) -> Result { let collection_id = Uuid::new_v4(); let collection_path = CollectionPath { collection: collection_id, @@ -517,7 +565,8 @@ async fn create_collection( token: &token, } .exec(&state.store) - .await?; + .await + .stateful(&state)?; Ok(to_edit_page( collection_path.collection, @@ -531,13 +580,14 @@ async fn update_collection( form: web::Form, token: ValidToken, state: web::Data, -) -> Result { +) -> Result { store::UpdateCollection { collection_path: &path, collection: &form, } .exec(&state.store) - .await?; + .await + .stateful(&state)?; Ok(to_edit_page(path.collection, &token, &state)) } @@ -547,13 +597,14 @@ async fn update_entry( entry: web::Form, token: ValidToken, state: web::Data, -) -> Result { +) -> Result { store::UpdateEntry { entry_path: &entry_path, entry: &entry, } .exec(&state.store) - .await?; + .await + .stateful(&state)?; Ok(to_edit_page(entry_path.collection, &token, &state)) } @@ -569,10 +620,15 @@ async fn delete_entry( token: ValidToken, conn: web::Data, state: web::Data, -) -> Result { - if !query.confirmed.unwrap_or(false) { - let entry = state.store.entry(&entry_path).await?; +) -> Result { + let res = state.store.entry(&entry_path).await.stateful(&state)?; + let entry = match res { + Some(entry) => entry, + None => return Ok(to_404(&state)), + }; + + if !query.confirmed.unwrap_or(false) { let mut cursor = Cursor::new(vec![]); self::templates::confirm_entry_delete( &mut cursor, @@ -581,22 +637,24 @@ async fn delete_entry( &entry, &token, &state, - )?; + ) + .stateful(&state)?; return Ok(HttpResponse::Ok() .content_type(mime::TEXT_HTML.essence_str()) .body(cursor.into_inner())); } - let entry = state.store.entry(&entry_path).await?; - - conn.delete(&entry.filename, &entry.delete_token).await?; + conn.delete(&entry.filename, &entry.delete_token) + .await + .stateful(&state)?; store::DeleteEntry { entry_path: &entry_path, } .exec(&state.store) - .await?; + .await + .stateful(&state)?; Ok(to_edit_page(entry_path.collection, &token, &state)) } @@ -607,32 +665,45 @@ async fn delete_collection( token: ValidToken, conn: web::Data, state: web::Data, -) -> Result { +) -> Result { if !query.confirmed.unwrap_or(false) { - let collection = state.store.collection(&path).await?; + let res = state.store.collection(&path).await.stateful(&state)?; + + let collection = match res { + Some(collection) => collection, + None => return Ok(to_404(&state)), + }; let mut cursor = Cursor::new(vec![]); - self::templates::confirm_delete(&mut cursor, path.collection, &collection, &token, &state)?; + self::templates::confirm_delete(&mut cursor, path.collection, &collection, &token, &state) + .stateful(&state)?; return Ok(HttpResponse::Ok() .content_type(mime::TEXT_HTML.essence_str()) .body(cursor.into_inner())); } - let entries = state.store.entries(path.entry_range()).await?; + let entries = state + .store + .entries(path.entry_range()) + .await + .stateful(&state)?; let future_vec = entries .iter() .map(|(_, entry)| conn.delete(&entry.filename, &entry.delete_token)) .collect::>(); - futures::future::try_join_all(future_vec).await?; + futures::future::try_join_all(future_vec) + .await + .stateful(&state)?; store::DeleteCollection { collection_path: &path, } .exec(&state.store) - .await?; + .await + .stateful(&state)?; Ok(to_home(&state)) } diff --git a/src/middleware.rs b/src/middleware.rs index a254568..2fc540b 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -140,7 +140,12 @@ async fn verify( token: crate::Token, state: &crate::State, ) -> Result<(), VerifyError> { - let token_storage = state.store.token(path).await.map_err(|_| VerifyError)?; + let token_storage = state + .store + .token(path) + .await + .map_err(|_| VerifyError)? + .ok_or(VerifyError)?; let verified = web::block(move || token_storage.verify(&token)) .await diff --git a/src/store.rs b/src/store.rs index 1e93873..a09b8cf 100644 --- a/src/store.rs +++ b/src/store.rs @@ -178,7 +178,7 @@ impl Store { pub(crate) async fn collection( &self, path: &CollectionPath, - ) -> Result { + ) -> Result, Error> { let collection_key = path.key(); let tree = self.tree.clone(); @@ -187,13 +187,13 @@ impl Store { match opt { Some(a) => { let collection = serde_json::from_slice(&a)?; - Ok(collection) + Ok(Some(collection)) } - None => Err(Error::NotFound), + None => Ok(None), } } - pub(crate) async fn entry(&self, path: &EntryPath) -> Result { + pub(crate) async fn entry(&self, path: &EntryPath) -> Result, Error> { let entry_key = path.key(); let tree = self.tree.clone(); @@ -202,9 +202,9 @@ impl Store { match opt { Some(e) => { let entry = serde_json::from_slice(&e)?; - Ok(entry) + Ok(Some(entry)) } - None => Err(Error::NotFound), + None => Ok(None), } } @@ -232,18 +232,22 @@ impl Store { Ok(v) } - pub(crate) async fn token(&self, path: &CollectionPath) -> Result { + pub(crate) async fn token( + &self, + path: &CollectionPath, + ) -> Result, Error> { let token_key = path.token_key(); let tree = self.tree.clone(); let token_opt = web::block(move || tree.get(token_key.as_bytes())).await?; - let token = match token_opt { - Some(token_ivec) => serde_json::from_slice(&token_ivec)?, - None => return Err(Error::NotFound), - }; - - Ok(token) + match token_opt { + Some(token_ivec) => { + let token = serde_json::from_slice(&token_ivec)?; + Ok(Some(token)) + } + None => Ok(None), + } } } @@ -266,9 +270,6 @@ pub(crate) enum Error { #[error("Panic in blocking operation")] Blocking, - - #[error("Item is not found")] - NotFound, } impl From> for Error diff --git a/templates/error.rs.html b/templates/error.rs.html new file mode 100644 index 0000000..3ddb710 --- /dev/null +++ b/templates/error.rs.html @@ -0,0 +1,18 @@ +@use crate::State; +@use super::{layout, return_home}; + +@(error: &str, state: &State) + +@:layout(state, "Error", Some(error), {}, { +
+
+
+

Error

+
+
+

@error

+
+
+
+@:return_home(state) +})