diff --git a/README.md b/README.md index a572284..ff6f363 100644 --- a/README.md +++ b/README.md @@ -13,21 +13,15 @@ Still very much an early WIP. - Post pools. - RSS feeds. -### Known issues +### Possible roadmap -- No way to close tag autocompletion on mobile. - -### Roadmap - -- [ ] Logging and improved error handling -- [ ] Lossless compression -- [ ] Post selection - [ ] Caching - [ ] Text media - [ ] Improve CSS -- [ ] Background tasks for garbage collection (dangling tags) - [ ] User management +- [ ] Display thumbnails on post selection - [ ] Testing +- [ ] Lossless compression - [ ] Migrate to Cot...? ## Running diff --git a/src/auth.rs b/src/auth.rs index dd1e034..0d1adf1 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -221,3 +221,9 @@ impl ExpiredDeletion for SessionStorage { Ok(()) } } + +impl From> for SameyError { + fn from(value: axum_login::Error) -> Self { + value.into() + } +} diff --git a/src/error.rs b/src/error.rs index 729f622..018456c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,10 +4,28 @@ use axum::{ response::{Html, IntoResponse, Response}, }; +#[derive(askama::Template)] +#[template(path = "pages/bad_request.html")] +struct BadRequestTemplate<'a> { + error: &'a str, +} + +#[derive(askama::Template)] +#[template(path = "pages/unauthorized.html")] +struct UnauthorizedTemplate; + +#[derive(askama::Template)] +#[template(path = "pages/forbidden.html")] +struct ForbiddenTemplate; + #[derive(askama::Template)] #[template(path = "pages/not_found.html")] struct NotFoundTemplate; +#[derive(askama::Template)] +#[template(path = "pages/internal_server_error.html")] +struct InternalServerErrorTemplate; + /// Errors from Samey. #[derive(Debug, thiserror::Error)] pub enum SameyError { @@ -54,7 +72,6 @@ pub enum SameyError { impl IntoResponse for SameyError { fn into_response(self) -> Response { - println!("Server error - {}", &self); match &self { SameyError::IntConversion(_) | SameyError::IntParse(_) @@ -64,11 +81,37 @@ impl IntoResponse for SameyError { | SameyError::Database(_) | SameyError::Image(_) | SameyError::Other(_) => { - (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong!").into_response() - } - SameyError::Multipart(_) | SameyError::BadRequest(_) => { - (StatusCode::BAD_REQUEST, "Invalid request").into_response() + println!("Internal server error - {:?}", &self); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Html( + InternalServerErrorTemplate {} + .render() + .expect("shouldn't fail to render InternalServerErrorTemplate"), + ), + ) + .into_response() } + SameyError::Multipart(error) => ( + StatusCode::BAD_REQUEST, + Html( + BadRequestTemplate { + error: &error.body_text(), + } + .render() + .expect("shouldn't fail to render BadRequestTemplate"), + ), + ) + .into_response(), + SameyError::BadRequest(error) => ( + StatusCode::BAD_REQUEST, + Html( + BadRequestTemplate { error } + .render() + .expect("shouldn't fail to render BadRequestTemplate"), + ), + ) + .into_response(), SameyError::NotFound => ( StatusCode::NOT_FOUND, Html( @@ -78,10 +121,24 @@ impl IntoResponse for SameyError { ), ) .into_response(), - SameyError::Authentication(_) => { - (StatusCode::UNAUTHORIZED, "Not authorized").into_response() - } - SameyError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden").into_response(), + SameyError::Authentication(_) => ( + StatusCode::UNAUTHORIZED, + Html( + UnauthorizedTemplate {} + .render() + .expect("shouldn't fail to render UnauthorizedTemplate"), + ), + ) + .into_response(), + SameyError::Forbidden => ( + StatusCode::FORBIDDEN, + Html( + ForbiddenTemplate {} + .render() + .expect("shouldn't fail to render ForbiddenTemplate"), + ), + ) + .into_response(), } } } diff --git a/src/query.rs b/src/query.rs index 21034b3..e46c83a 100644 --- a/src/query.rs +++ b/src/query.rs @@ -3,9 +3,9 @@ use std::collections::HashSet; use chrono::NaiveDateTime; use samey_migration::{Expr, Query}; use sea_orm::{ - ColumnTrait, Condition, DatabaseConnection, EntityTrait, FromQueryResult, IntoSimpleExpr, - QueryFilter, QueryOrder, QuerySelect, RelationTrait, Select, SelectColumns, SelectModel, - Selector, + ColumnTrait, Condition, DatabaseConnection, EntityTrait, FromQueryResult, IntoIdentity, + IntoSimpleExpr, QueryFilter, QueryOrder, QuerySelect, RelationTrait, Select, SelectColumns, + SelectModel, Selector, }; use crate::{ @@ -276,3 +276,18 @@ pub(crate) fn filter_posts_by_user( ), } } + +pub(crate) async fn clean_dangling_tags(db: &DatabaseConnection) -> Result<(), SameyError> { + let dangling_tags = SameyTag::find() + .select_column_as(samey_tag_post::Column::Id.count(), "count") + .left_join(SameyTagPost) + .group_by(samey_tag::Column::Id) + .having(Expr::column("count".into_identity()).eq(0)) + .all(db) + .await?; + SameyTag::delete_many() + .filter(samey_tag::Column::Id.is_in(dangling_tags.into_iter().map(|tag| tag.id))) + .exec(db) + .await?; + Ok(()) +} diff --git a/src/views.rs b/src/views.rs index 3ff886e..2545502 100644 --- a/src/views.rs +++ b/src/views.rs @@ -40,8 +40,8 @@ use crate::{ }, error::SameyError, query::{ - PoolPost, PostOverview, PostPoolData, filter_posts_by_user, get_pool_data_for_post, - get_posts_in_pool, get_tags_for_post, search_posts, + PoolPost, PostOverview, PostPoolData, clean_dangling_tags, filter_posts_by_user, + get_pool_data_for_post, get_posts_in_pool, get_tags_for_post, search_posts, }, video::{generate_thumbnail, get_dimensions_for_video}, }; @@ -183,24 +183,17 @@ pub(crate) async fn login( mut auth_session: AuthSession, Form(credentials): Form, ) -> Result { - let user = match auth_session.authenticate(credentials).await { - Ok(Some(user)) => user, - Ok(None) => return Err(SameyError::Authentication("Invalid credentials".into())), - Err(_) => return Err(SameyError::Other("Auth session error".into())), + let user = match auth_session.authenticate(credentials).await? { + Some(user) => user, + None => return Err(SameyError::Authentication("Invalid credentials".into())), }; - auth_session - .login(&user) - .await - .map_err(|_| SameyError::Other("Login failed".into()))?; + auth_session.login(&user).await?; Ok(Redirect::to("/")) } pub(crate) async fn logout(mut auth_session: AuthSession) -> Result { - auth_session - .logout() - .await - .map_err(|_| SameyError::Other("Logout error".into()))?; + auth_session.logout().await?; Ok(Redirect::to("/")) } @@ -260,10 +253,9 @@ impl FromStr for Format { "application/x-matroska" | "video/mastroska" => Ok(Self::Video(".mkv")), "video/quicktime" => Ok(Self::Video(".mov")), _ => Ok(Self::Image( - ImageFormat::from_mime_type(content_type).ok_or(SameyError::Other(format!( - "Unknown content type: {}", - content_type - )))?, + ImageFormat::from_mime_type(content_type).ok_or(SameyError::BadRequest( + format!("Unknown content type: {}", content_type), + ))?, )), } } @@ -334,7 +326,7 @@ pub(crate) async fn upload( "media-file" => { let content_type = field .content_type() - .ok_or(SameyError::Other("Missing content type".into()))?; + .ok_or(SameyError::BadRequest("Missing content type".into()))?; match Format::from_str(content_type)? { format @ Format::Video(video_format) => { media_type = Some(format.media_type()); @@ -502,7 +494,9 @@ pub(crate) async fn upload( Ok(Redirect::to(&format!("/post/{}", uploaded_post))) } else { - Err(SameyError::Other("Missing parameters for upload".into())) + Err(SameyError::BadRequest( + "Missing parameters for upload".into(), + )) } } @@ -1376,25 +1370,24 @@ pub(crate) async fn update_settings( let mut configs = vec![]; - if !body.application_name.is_empty() { + let application_name = body.application_name.trim(); + if !application_name.is_empty() { let _ = mem::replace( &mut app_config.write().await.application_name, - body.application_name.clone(), + application_name.into(), ); configs.push(samey_config::ActiveModel { key: Set(APPLICATION_NAME_KEY.into()), - data: Set(body.application_name.into()), + data: Set(application_name.into()), ..Default::default() }); } - let _ = mem::replace( - &mut app_config.write().await.base_url, - body.base_url.clone(), - ); + let base_url = body.base_url.trim_end_matches('/'); + let _ = mem::replace(&mut app_config.write().await.base_url, base_url.into()); configs.push(samey_config::ActiveModel { key: Set(BASE_URL_KEY.into()), - data: Set(body.base_url.into()), + data: Set(base_url.into()), ..Default::default() }); @@ -1772,6 +1765,12 @@ pub(crate) async fn submit_post_details( .all(&db) .await?; + tokio::spawn(async move { + if let Err(err) = clean_dangling_tags(&db).await { + println!("Error when cleaning dangling tags - {}", err); + } + }); + Ok(Html( SubmitPostDetailsTemplate { post, @@ -1887,6 +1886,9 @@ pub(crate) async fn delete_post( tokio::spawn(async move { let _ = std::fs::remove_file(files_dir.join(post.media)); let _ = std::fs::remove_file(files_dir.join(post.thumbnail)); + if let Err(err) = clean_dangling_tags(&db).await { + println!("Error when cleaning dangling tags - {}", err); + } }); Ok(Redirect::to("/")) diff --git a/templates/fragments/search_tags.html b/templates/fragments/search_tags.html index c01593f..dfa176b 100644 --- a/templates/fragments/search_tags.html +++ b/templates/fragments/search_tags.html @@ -1,4 +1,10 @@ -{% for tag in tags %} +{% if !tags.is_empty() %} +
  • + +
  • +{% endif %}{% for tag in tags %}