Handle errors better (render a page), use program start time as last modified for static assets
This commit is contained in:
parent
fbf10e2eea
commit
861c40c96e
175
src/lib.rs
175
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<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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
33
src/store.rs
33
src/store.rs
|
@ -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
18
templates/error.rs.html
Normal 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)
|
||||
})
|
Loading…
Reference in a new issue