Handle errors better (render a page), use program start time as last modified for static assets

This commit is contained in:
asonix 2020-12-11 22:33:56 -06:00
parent fbf10e2eea
commit 861c40c96e
4 changed files with 164 additions and 69 deletions

View file

@ -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<State, sled::Error>
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<T> {
fn stateful(self, state: &web::Data<State>) -> Result<T, StateError>;
}
impl<T, E> ResultExt<T> for Result<T, E>
where
Error: From<E>,
{
fn stateful(self, state: &web::Data<State>) -> Result<T, StateError> {
self.map_err(Error::from).map_err(|error| StateError {
state: state.clone(),
error,
})
}
}
async fn static_files(filename: web::Path<String>, state: web::Data<State>) -> 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<String>, state: web::Data<State>) -> H
to_404(&state)
}
async fn not_found(state: web::Data<State>) -> Result<HttpResponse, Error> {
async fn not_found(state: web::Data<State>) -> Result<HttpResponse, StateError> {
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<State>,
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<Connection>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let images = conn.upload(&req, pl).await?;
) -> Result<HttpResponse, StateError> {
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<State>) -> Result<HttpResponse, Error> {
async fn index(state: web::Data<State>) -> Result<HttpResponse, StateError> {
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<CollectionPath>,
token: Option<ValidToken>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
) -> Result<HttpResponse, StateError> {
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<CollectionPath>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
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<State>,
) -> Result<HttpResponse, Error> {
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<Collection>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
) -> Result<HttpResponse, StateError> {
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<Collection>,
token: ValidToken,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
) -> Result<HttpResponse, StateError> {
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<Entry>,
token: ValidToken,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
) -> Result<HttpResponse, StateError> {
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<Connection>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
if !query.confirmed.unwrap_or(false) {
let entry = state.store.entry(&entry_path).await?;
) -> Result<HttpResponse, StateError> {
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<Connection>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
) -> Result<HttpResponse, StateError> {
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::<Vec<_>>();
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))
}

View file

@ -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

View file

@ -178,7 +178,7 @@ impl Store {
pub(crate) async fn collection(
&self,
path: &CollectionPath,
) -> Result<crate::Collection, Error> {
) -> Result<Option<crate::Collection>, 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<crate::Entry, Error> {
pub(crate) async fn entry(&self, path: &EntryPath) -> Result<Option<crate::Entry>, 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<crate::TokenStorage, Error> {
pub(crate) async fn token(
&self,
path: &CollectionPath,
) -> Result<Option<crate::TokenStorage>, 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<E> From<actix_web::error::BlockingError<E>> for Error

18
templates/error.rs.html Normal file
View file

@ -0,0 +1,18 @@
@use crate::State;
@use super::{layout, return_home};
@(error: &str, state: &State)
@:layout(state, "Error", Some(error), {}, {
<section>
<article>
<div class="content-group">
<h3>Error</h3>
</div>
<div class="content-group">
<p class="subtitle">@error</p>
</div>
</article>
</section>
@:return_home(state)
})