diff --git a/src/database/mod.rs b/src/database/mod.rs index 432b17b..185a176 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,6 +1,10 @@ use bcrypt::{hash, verify, DEFAULT_COST}; use diesel::prelude::*; use diesel::sqlite::SqliteConnection; +use std::{ + ops::Deref, + sync::{Arc, Mutex}, +}; use crate::database::models::{Favorite, InsertFavorite, InsertUser, User}; @@ -85,25 +89,87 @@ fn connect() -> SqliteConnection { SqliteConnection::establish(&db_url).expect("Error connecting to DB") } -pub fn add_favorite(user_id: i32, favorite_path: String) { - use schema::favorites::dsl::*; - - let connection = connect(); - diesel::insert_into(favorites) - .values(InsertFavorite { - userid: &user_id, - path: &favorite_path, - }) - .execute(&connection) - .unwrap(); +#[derive(Debug)] +pub struct DbError { + pub kind: DbErrorKind, } -pub fn get_favorites(user_id: i32) -> diesel::QueryResult> { - use schema::favorites::dsl::*; +impl DbError { + fn new(kind: DbErrorKind) -> Self { + DbError { kind } + } - favorites - .filter(userid.eq(user_id)) - .load::(&connect()) + fn exists() -> Self { + DbError::new(DbErrorKind::AlreadyExists) + } +} + +#[derive(Debug, PartialEq)] +pub enum DbErrorKind { + AlreadyExists, + InsertError, + QueryError, +} + +pub trait FavoriteDao: Sync + Send { + fn add_favorite(&self, user_id: i32, favorite_path: &str) -> Result; + fn remove_favorite(&self, user_id: i32, favorite_path: String); + fn get_favorites(&self, user_id: i32) -> Result, DbError>; +} + +pub struct SqliteFavoriteDao { + connection: Arc>, +} + +impl SqliteFavoriteDao { + pub fn new() -> Self { + SqliteFavoriteDao { + connection: Arc::new(Mutex::new(connect())), + } + } +} + +impl FavoriteDao for SqliteFavoriteDao { + fn add_favorite(&self, user_id: i32, favorite_path: &str) -> Result { + use schema::favorites::dsl::*; + + let connection = self.connection.lock().unwrap(); + let connection = connection.deref(); + + if favorites + .filter(userid.eq(user_id).and(path.eq(&favorite_path))) + .first::(connection) + .is_err() + { + diesel::insert_into(favorites) + .values(InsertFavorite { + userid: &user_id, + path: &favorite_path, + }) + .execute(connection) + .map_err(|_| DbError::new(DbErrorKind::InsertError)) + } else { + Err(DbError::exists()) + } + } + + fn remove_favorite(&self, user_id: i32, favorite_path: String) { + use schema::favorites::dsl::*; + + diesel::delete(favorites) + .filter(userid.eq(user_id).and(path.eq(favorite_path))) + .execute(self.connection.lock().unwrap().deref()) + .unwrap(); + } + + fn get_favorites(&self, user_id: i32) -> Result, DbError> { + use schema::favorites::dsl::*; + + favorites + .filter(userid.eq(user_id)) + .load::(self.connection.lock().unwrap().deref()) + .map_err(|_| DbError::new(DbErrorKind::QueryError)) + } } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index 1b160d8..d9d95b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ extern crate diesel; extern crate rayon; use crate::auth::login; -use database::{SqliteUserDao, UserDao}; +use database::{DbError, DbErrorKind, FavoriteDao, SqliteFavoriteDao, SqliteUserDao, UserDao}; use futures::stream::StreamExt; use std::io::prelude::*; use std::path::{Path, PathBuf}; @@ -15,7 +15,9 @@ use actix::{Actor, Addr}; use actix_files::NamedFile; use actix_multipart as mp; use actix_web::{ - get, post, + delete, + error::BlockingError, + get, post, put, web::{self, BufMut, BytesMut}, App, HttpServer, Responder, }; @@ -31,7 +33,6 @@ use data::{AddFavoriteRequest, ThumbnailRequest}; use log::{debug, error, info}; use crate::data::Claims; -use crate::database::{add_favorite, get_favorites}; use crate::files::{is_image_or_video, is_valid_path, list_files}; use crate::video::*; @@ -216,13 +217,17 @@ async fn get_video_part( } #[get("image/favorites")] -async fn favorites(claims: Claims) -> impl Responder { - let favorites = web::block(move || get_favorites(claims.sub.parse::().unwrap())) - .await - .unwrap() - .into_iter() - .map(|favorite| favorite.path) - .collect::>(); +async fn favorites( + claims: Claims, + favorites_dao: web::Data>, +) -> impl Responder { + let favorites = + web::block(move || favorites_dao.get_favorites(claims.sub.parse::().unwrap())) + .await + .unwrap() + .into_iter() + .map(|favorite| favorite.path) + .collect::>(); HttpResponse::Ok().json(PhotosResponse { photos: &favorites, @@ -230,17 +235,55 @@ async fn favorites(claims: Claims) -> impl Responder { }) } -#[post("image/favorites")] -async fn post_add_favorite(claims: Claims, body: web::Json) -> impl Responder { +#[put("image/favorites")] +async fn put_add_favorite( + claims: Claims, + body: web::Json, + favorites_dao: web::Data>, +) -> impl Responder { + if let Ok(user_id) = claims.sub.parse::() { + let path = body.path.clone(); + match web::block::<_, usize, DbError>(move || favorites_dao.add_favorite(user_id, &path)) + .await + { + Err(BlockingError::Error(e)) if e.kind == DbErrorKind::AlreadyExists => { + debug!("Favorite: {} exists for user: {}", &body.path, user_id); + HttpResponse::Ok() + } + Err(e) => { + info!("{:?} {}. for user: {}", e, body.path, user_id); + HttpResponse::BadRequest() + } + Ok(_) => { + debug!("Adding favorite \"{}\" for userid: {}", body.path, user_id); + HttpResponse::Created() + } + } + } else { + error!("Unable to parse sub as i32: {}", claims.sub); + HttpResponse::BadRequest() + } +} + +#[delete("image/favorites")] +async fn delete_favorite( + claims: Claims, + body: web::Query, + favorites_dao: web::Data>, +) -> impl Responder { if let Ok(user_id) = claims.sub.parse::() { let path = body.path.clone(); web::block::<_, _, String>(move || { - add_favorite(user_id, path); + favorites_dao.remove_favorite(user_id, path); Ok(()) }) .await .unwrap(); - debug!("Adding favorite \"{}\" for userid: {}", body.path, user_id); + + debug!( + "Removing favorite \"{}\" for userid: {}", + body.path, user_id + ); HttpResponse::Ok() } else { error!("Unable to parse sub as i32: {}", claims.sub); @@ -367,6 +410,7 @@ fn main() -> std::io::Result<()> { HttpServer::new(move || { let user_dao = SqliteUserDao::new(); + let favorites_dao = SqliteFavoriteDao::new(); App::new() .wrap(middleware::Logger::default()) .service(web::resource("/login").route(web::post().to(login))) @@ -377,9 +421,11 @@ fn main() -> std::io::Result<()> { .service(stream_video) .service(get_video_part) .service(favorites) - .service(post_add_favorite) + .service(put_add_favorite) + .service(delete_favorite) .app_data(app_data.clone()) .data::>(Box::new(user_dao)) + .data::>(Box::new(favorites_dao)) }) .bind(dotenv::var("BIND_URL").unwrap())? .bind("localhost:8088")?