Update and Migrate Diesel to 2.0

Almost have tag support working, still figuring out how to get photo
tags.
This commit is contained in:
Cameron Cordes
2023-03-18 14:43:41 -04:00
parent 40c79d13db
commit 68bfcbf85f
8 changed files with 1009 additions and 586 deletions

991
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,8 @@ futures = "0.3.5"
jsonwebtoken = "7.2.0" jsonwebtoken = "7.2.0"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
diesel = { version = "1.4.8", features = ["sqlite"] } diesel = { version = "2.0.2", features = ["sqlite"] }
diesel_migrations = "2.0.0"
hmac = "0.11" hmac = "0.11"
sha2 = "0.9" sha2 = "0.9"
chrono = "0.4" chrono = "0.4"

View File

@@ -1,3 +1,4 @@
use std::sync::Mutex;
use actix_web::Responder; use actix_web::Responder;
use actix_web::{ use actix_web::{
web::{self, Json}, web::{self, Json},
@@ -15,12 +16,13 @@ use crate::{
#[allow(dead_code)] #[allow(dead_code)]
async fn register<D: UserDao>( async fn register<D: UserDao>(
user: Json<CreateAccountRequest>, user: Json<CreateAccountRequest>,
user_dao: web::Data<D>, user_dao: web::Data<Mutex<D>>,
) -> impl Responder { ) -> impl Responder {
if !user.username.is_empty() && user.password.len() > 5 && user.password == user.confirmation { if !user.username.is_empty() && user.password.len() > 5 && user.password == user.confirmation {
if user_dao.user_exists(&user.username) { let mut dao = user_dao.lock().expect("Unable to get UserDao");
if dao.user_exists(&user.username) {
HttpResponse::BadRequest() HttpResponse::BadRequest()
} else if let Some(_user) = user_dao.create_user(&user.username, &user.password) { } else if let Some(_user) = dao.create_user(&user.username, &user.password) {
HttpResponse::Ok() HttpResponse::Ok()
} else { } else {
HttpResponse::InternalServerError() HttpResponse::InternalServerError()
@@ -30,9 +32,11 @@ async fn register<D: UserDao>(
} }
} }
pub async fn login<D: UserDao>(creds: Json<LoginRequest>, user_dao: web::Data<D>) -> HttpResponse { pub async fn login<D: UserDao>(creds: Json<LoginRequest>, user_dao: web::Data<Mutex<D>>) -> HttpResponse {
debug!("Logging in: {}", creds.username); debug!("Logging in: {}", creds.username);
let mut user_dao = user_dao.lock().expect("Unable to get UserDao");
if let Some(user) = user_dao.get_user(&creds.username, &creds.password) { if let Some(user) = user_dao.get_user(&creds.username, &creds.password) {
let claims = Claims { let claims = Claims {
sub: user.id.to_string(), sub: user.id.to_string(),

View File

@@ -16,12 +16,27 @@ pub struct Token<'a> {
pub token: &'a str, pub token: &'a str,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Claims { pub struct Claims {
pub sub: String, pub sub: String,
pub exp: i64, pub exp: i64,
} }
#[cfg(test)]
pub mod helper {
use super::Claims;
use chrono::{Duration, Utc};
impl Claims {
pub fn valid_user(user_id: String) -> Self {
Claims {
sub: user_id,
exp: (Utc::now() + Duration::minutes(1)).timestamp(),
}
}
}
}
pub fn secret_key() -> String { pub fn secret_key() -> String {
if cfg!(test) { if cfg!(test) {
String::from("test_key") String::from("test_key")

View File

@@ -2,9 +2,9 @@ use bcrypt::{hash, verify, DEFAULT_COST};
use diesel::prelude::*; use diesel::prelude::*;
use diesel::sqlite::SqliteConnection; use diesel::sqlite::SqliteConnection;
use std::{ use std::{
ops::Deref,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use std::ops::DerefMut;
use crate::database::models::{Favorite, InsertFavorite, InsertUser, User}; use crate::database::models::{Favorite, InsertFavorite, InsertUser, User};
@@ -12,9 +12,9 @@ pub mod models;
pub mod schema; pub mod schema;
pub trait UserDao { pub trait UserDao {
fn create_user(&self, user: &str, password: &str) -> Option<User>; fn create_user(&mut self, user: &str, password: &str) -> Option<User>;
fn get_user(&self, user: &str, password: &str) -> Option<User>; fn get_user(&mut self, user: &str, password: &str) -> Option<User>;
fn user_exists(&self, user: &str) -> bool; fn user_exists(&mut self, user: &str) -> bool;
} }
pub struct SqliteUserDao { pub struct SqliteUserDao {
@@ -31,7 +31,7 @@ impl SqliteUserDao {
impl UserDao for SqliteUserDao { impl UserDao for SqliteUserDao {
// TODO: Should probably use Result here // TODO: Should probably use Result here
fn create_user(&self, user: &str, pass: &str) -> std::option::Option<User> { fn create_user(&mut self, user: &str, pass: &str) -> Option<User> {
use schema::users::dsl::*; use schema::users::dsl::*;
let hashed = hash(pass, DEFAULT_COST); let hashed = hash(pass, DEFAULT_COST);
@@ -41,12 +41,12 @@ impl UserDao for SqliteUserDao {
username: user, username: user,
password: &hash, password: &hash,
}) })
.execute(&self.connection) .execute(&mut self.connection)
.unwrap(); .unwrap();
users users
.filter(username.eq(username)) .filter(username.eq(username))
.load::<User>(&self.connection) .load::<User>(&mut self.connection)
.unwrap() .unwrap()
.first() .first()
.cloned() .cloned()
@@ -55,12 +55,12 @@ impl UserDao for SqliteUserDao {
} }
} }
fn get_user(&self, user: &str, pass: &str) -> Option<User> { fn get_user(&mut self, user: &str, pass: &str) -> Option<User> {
use schema::users::dsl::*; use schema::users::dsl::*;
match users match users
.filter(username.eq(user)) .filter(username.eq(user))
.load::<User>(&self.connection) .load::<User>(&mut self.connection)
.unwrap_or_default() .unwrap_or_default()
.first() .first()
{ {
@@ -69,12 +69,12 @@ impl UserDao for SqliteUserDao {
} }
} }
fn user_exists(&self, user: &str) -> bool { fn user_exists(&mut self, user: &str) -> bool {
use schema::users::dsl::*; use schema::users::dsl::*;
users users
.filter(username.eq(user)) .filter(username.eq(user))
.load::<User>(&self.connection) .load::<User>(&mut self.connection)
.unwrap_or_default() .unwrap_or_default()
.first() .first()
.is_some() .is_some()
@@ -109,9 +109,9 @@ pub enum DbErrorKind {
} }
pub trait FavoriteDao: Sync + Send { pub trait FavoriteDao: Sync + Send {
fn add_favorite(&self, user_id: i32, favorite_path: &str) -> Result<usize, DbError>; fn add_favorite(&mut self, user_id: i32, favorite_path: &str) -> Result<usize, DbError>;
fn remove_favorite(&self, user_id: i32, favorite_path: String); fn remove_favorite(&mut self, user_id: i32, favorite_path: String);
fn get_favorites(&self, user_id: i32) -> Result<Vec<Favorite>, DbError>; fn get_favorites(&mut self, user_id: i32) -> Result<Vec<Favorite>, DbError>;
} }
pub struct SqliteFavoriteDao { pub struct SqliteFavoriteDao {
@@ -127,15 +127,14 @@ impl SqliteFavoriteDao {
} }
impl FavoriteDao for SqliteFavoriteDao { impl FavoriteDao for SqliteFavoriteDao {
fn add_favorite(&self, user_id: i32, favorite_path: &str) -> Result<usize, DbError> { fn add_favorite(&mut self, user_id: i32, favorite_path: &str) -> Result<usize, DbError> {
use schema::favorites::dsl::*; use schema::favorites::dsl::*;
let connection = self.connection.lock().unwrap(); let mut connection = self.connection.lock().expect("Unable to get FavoriteDao");
let connection = connection.deref();
if favorites if favorites
.filter(userid.eq(user_id).and(path.eq(&favorite_path))) .filter(userid.eq(user_id).and(path.eq(&favorite_path)))
.first::<Favorite>(connection) .first::<Favorite>(connection.deref_mut())
.is_err() .is_err()
{ {
diesel::insert_into(favorites) diesel::insert_into(favorites)
@@ -143,28 +142,28 @@ impl FavoriteDao for SqliteFavoriteDao {
userid: &user_id, userid: &user_id,
path: favorite_path, path: favorite_path,
}) })
.execute(connection) .execute(connection.deref_mut())
.map_err(|_| DbError::new(DbErrorKind::InsertError)) .map_err(|_| DbError::new(DbErrorKind::InsertError))
} else { } else {
Err(DbError::exists()) Err(DbError::exists())
} }
} }
fn remove_favorite(&self, user_id: i32, favorite_path: String) { fn remove_favorite(&mut self, user_id: i32, favorite_path: String) {
use schema::favorites::dsl::*; use schema::favorites::dsl::*;
diesel::delete(favorites) diesel::delete(favorites)
.filter(userid.eq(user_id).and(path.eq(favorite_path))) .filter(userid.eq(user_id).and(path.eq(favorite_path)))
.execute(self.connection.lock().unwrap().deref()) .execute(self.connection.lock().unwrap().deref_mut())
.unwrap(); .unwrap();
} }
fn get_favorites(&self, user_id: i32) -> Result<Vec<Favorite>, DbError> { fn get_favorites(&mut self, user_id: i32) -> Result<Vec<Favorite>, DbError> {
use schema::favorites::dsl::*; use schema::favorites::dsl::*;
favorites favorites
.filter(userid.eq(user_id)) .filter(userid.eq(user_id))
.load::<Favorite>(self.connection.lock().unwrap().deref()) .load::<Favorite>(self.connection.lock().unwrap().deref_mut())
.map_err(|_| DbError::new(DbErrorKind::QueryError)) .map_err(|_| DbError::new(DbErrorKind::QueryError))
} }
} }

14
src/error.rs Normal file
View File

@@ -0,0 +1,14 @@
use actix_web::{error::InternalError, http::StatusCode};
pub trait IntoHttpError<T> {
fn into_http_internal_err(self) -> Result<T, actix_web::Error>;
}
impl<T> IntoHttpError<T> for Result<T, anyhow::Error> {
fn into_http_internal_err(self) -> Result<T, actix_web::Error> {
self.map_err(|e| {
log::error!("Map to err: {:?}", e);
InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR).into()
})
}
}

View File

@@ -4,6 +4,7 @@ extern crate rayon;
use actix_web::web::Data; use actix_web::web::Data;
use actix_web_prom::PrometheusMetricsBuilder; use actix_web_prom::PrometheusMetricsBuilder;
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use futures::stream::StreamExt; use futures::stream::StreamExt;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use prometheus::{self, IntGauge}; use prometheus::{self, IntGauge};
@@ -14,6 +15,8 @@ use std::{
io::ErrorKind, io::ErrorKind,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use std::error::Error;
use std::sync::Mutex;
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
use actix_files::NamedFile; use actix_files::NamedFile;
@@ -23,6 +26,7 @@ use actix_web::{
web::{self, BufMut, BytesMut}, web::{self, BufMut, BytesMut},
App, HttpRequest, HttpResponse, HttpServer, Responder, App, HttpRequest, HttpResponse, HttpServer, Responder,
}; };
use diesel::sqlite::Sqlite;
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use rayon::prelude::*; use rayon::prelude::*;
@@ -39,6 +43,7 @@ use crate::video::*;
mod auth; mod auth;
mod data; mod data;
mod database; mod database;
mod error;
mod files; mod files;
mod state; mod state;
mod tags; mod tags;
@@ -60,12 +65,14 @@ lazy_static! {
.unwrap(); .unwrap();
} }
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
#[get("/image")] #[get("/image")]
async fn get_image( async fn get_image(
_claims: Claims, _claims: Claims,
request: HttpRequest, request: HttpRequest,
req: web::Query<ThumbnailRequest>, req: web::Query<ThumbnailRequest>,
app_state: web::Data<AppState>, app_state: Data<AppState>,
) -> impl Responder { ) -> impl Responder {
if let Some(path) = is_valid_full_path(&app_state.base_path, &req.path) { if let Some(path) = is_valid_full_path(&app_state.base_path, &req.path) {
if req.size.is_some() { if req.size.is_some() {
@@ -97,7 +104,7 @@ async fn get_image(
async fn get_file_metadata( async fn get_file_metadata(
_: Claims, _: Claims,
path: web::Query<ThumbnailRequest>, path: web::Query<ThumbnailRequest>,
app_state: web::Data<AppState>, app_state: Data<AppState>,
) -> impl Responder { ) -> impl Responder {
match is_valid_full_path(&app_state.base_path, &path.path) match is_valid_full_path(&app_state.base_path, &path.path)
.ok_or_else(|| ErrorKind::InvalidData.into()) .ok_or_else(|| ErrorKind::InvalidData.into())
@@ -119,7 +126,7 @@ async fn get_file_metadata(
async fn upload_image( async fn upload_image(
_: Claims, _: Claims,
mut payload: mp::Multipart, mut payload: mp::Multipart,
app_state: web::Data<AppState>, app_state: Data<AppState>,
) -> impl Responder { ) -> impl Responder {
let mut file_content: BytesMut = BytesMut::new(); let mut file_content: BytesMut = BytesMut::new();
let mut file_name: Option<String> = None; let mut file_name: Option<String> = None;
@@ -170,7 +177,7 @@ async fn upload_image(
#[post("/video/generate")] #[post("/video/generate")]
async fn generate_video( async fn generate_video(
_claims: Claims, _claims: Claims,
app_state: web::Data<AppState>, app_state: Data<AppState>,
body: web::Json<ThumbnailRequest>, body: web::Json<ThumbnailRequest>,
) -> impl Responder { ) -> impl Responder {
let filename = PathBuf::from(&body.path); let filename = PathBuf::from(&body.path);
@@ -200,7 +207,7 @@ async fn stream_video(
request: HttpRequest, request: HttpRequest,
_: Claims, _: Claims,
path: web::Query<ThumbnailRequest>, path: web::Query<ThumbnailRequest>,
app_state: web::Data<AppState>, app_state: Data<AppState>,
) -> impl Responder { ) -> impl Responder {
let playlist = &path.path; let playlist = &path.path;
debug!("Playlist: {}", playlist); debug!("Playlist: {}", playlist);
@@ -235,9 +242,10 @@ async fn get_video_part(
#[get("image/favorites")] #[get("image/favorites")]
async fn favorites( async fn favorites(
claims: Claims, claims: Claims,
favorites_dao: web::Data<Box<dyn FavoriteDao>>, favorites_dao: Data<Mutex<Box<dyn FavoriteDao>>>,
) -> impl Responder { ) -> impl Responder {
match web::block(move || favorites_dao.get_favorites(claims.sub.parse::<i32>().unwrap())).await match web::block(move || favorites_dao.lock()
.expect("Unable to get FavoritesDao").get_favorites(claims.sub.parse::<i32>().unwrap())).await
{ {
Ok(Ok(favorites)) => { Ok(Ok(favorites)) => {
let favorites = favorites let favorites = favorites
@@ -262,12 +270,12 @@ async fn favorites(
async fn put_add_favorite( async fn put_add_favorite(
claims: Claims, claims: Claims,
body: web::Json<AddFavoriteRequest>, body: web::Json<AddFavoriteRequest>,
favorites_dao: web::Data<Box<dyn FavoriteDao>>, favorites_dao: Data<Mutex<Box<dyn FavoriteDao>>>,
) -> impl Responder { ) -> impl Responder {
if let Ok(user_id) = claims.sub.parse::<i32>() { if let Ok(user_id) = claims.sub.parse::<i32>() {
let path = body.path.clone(); let path = body.path.clone();
match web::block::<_, Result<usize, DbError>>(move || { match web::block::<_, Result<usize, DbError>>(move || {
favorites_dao.add_favorite(user_id, &path) favorites_dao.lock().expect("Unable to get FavoritesDao").add_favorite(user_id, &path)
}) })
.await .await
{ {
@@ -298,12 +306,12 @@ async fn put_add_favorite(
async fn delete_favorite( async fn delete_favorite(
claims: Claims, claims: Claims,
body: web::Query<AddFavoriteRequest>, body: web::Query<AddFavoriteRequest>,
favorites_dao: web::Data<Box<dyn FavoriteDao>>, favorites_dao: Data<Mutex<Box<dyn FavoriteDao>>>,
) -> impl Responder { ) -> impl Responder {
if let Ok(user_id) = claims.sub.parse::<i32>() { if let Ok(user_id) = claims.sub.parse::<i32>() {
let path = body.path.clone(); let path = body.path.clone();
web::block(move || { web::block(move || {
favorites_dao.remove_favorite(user_id, path); favorites_dao.lock().expect("Unable to get favorites dao").remove_favorite(user_id, path);
}) })
.await .await
.unwrap(); .unwrap();
@@ -325,7 +333,7 @@ fn create_thumbnails() {
let images = PathBuf::from(dotenv::var("BASE_PATH").unwrap()); let images = PathBuf::from(dotenv::var("BASE_PATH").unwrap());
walkdir::WalkDir::new(&images) WalkDir::new(&images)
.into_iter() .into_iter()
.collect::<Vec<Result<_, _>>>() .collect::<Vec<Result<_, _>>>()
.into_par_iter() .into_par_iter()
@@ -422,12 +430,15 @@ fn main() -> std::io::Result<()> {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
env_logger::init(); env_logger::init();
run_migrations(&mut connect())
.expect("Failed to run migrations");
create_thumbnails(); create_thumbnails();
watch_files(); watch_files();
let system = actix::System::new(); let system = actix::System::new();
system.block_on(async { system.block_on(async {
let app_data = web::Data::new(AppState::default()); let app_data = Data::new(AppState::default());
let labels = HashMap::new(); let labels = HashMap::new();
let prometheus = PrometheusMetricsBuilder::new("api") let prometheus = PrometheusMetricsBuilder::new("api")
@@ -447,6 +458,7 @@ fn main() -> std::io::Result<()> {
HttpServer::new(move || { HttpServer::new(move || {
let user_dao = SqliteUserDao::new(); let user_dao = SqliteUserDao::new();
let favorites_dao = SqliteFavoriteDao::new(); let favorites_dao = SqliteFavoriteDao::new();
let tag_dao = SqliteTagDao::default();
App::new() App::new()
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.service(web::resource("/login").route(web::post().to(login::<SqliteUserDao>))) .service(web::resource("/login").route(web::post().to(login::<SqliteUserDao>)))
@@ -460,12 +472,17 @@ fn main() -> std::io::Result<()> {
.service(put_add_favorite) .service(put_add_favorite)
.service(delete_favorite) .service(delete_favorite)
.service(get_file_metadata) .service(get_file_metadata)
.service(add_tag) .service(
.service(get_tags) web::resource("image/tags")
.service(remove_tagged_photo) .route(web::post().to(add_tag::<SqliteTagDao>))
.route(web::get().to(get_all_tags::<SqliteTagDao>))
.route(web::get().to(get_tags::<SqliteTagDao>))
.route(web::delete().to(remove_tagged_photo::<SqliteTagDao>)),
)
.app_data(app_data.clone()) .app_data(app_data.clone())
.app_data::<Data<SqliteUserDao>>(Data::new(user_dao)) .app_data::<Data<Mutex<SqliteUserDao>>>(Data::new(Mutex::new(user_dao)))
.app_data::<Data<Box<dyn FavoriteDao>>>(Data::new(Box::new(favorites_dao))) .app_data::<Data<Mutex<Box<dyn FavoriteDao>>>>(Data::new(Mutex::new(Box::new(favorites_dao))))
.app_data::<Data<Mutex<SqliteTagDao>>>(Data::new(Mutex::new(tag_dao)))
.wrap(prometheus.clone()) .wrap(prometheus.clone())
}) })
.bind(dotenv::var("BIND_URL").unwrap())? .bind(dotenv::var("BIND_URL").unwrap())?
@@ -475,6 +492,12 @@ fn main() -> std::io::Result<()> {
}) })
} }
fn run_migrations(connection: &mut impl MigrationHarness<Sqlite>) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
connection.run_pending_migrations(MIGRATIONS)?;
Ok(())
}
fn watch_files() { fn watch_files() {
std::thread::spawn(|| { std::thread::spawn(|| {
let (wtx, wrx) = channel(); let (wtx, wrx) = channel();

View File

@@ -1,153 +1,73 @@
use crate::{ use crate::{connect, data::AddTagRequest, error::IntoHttpError, schema, Claims, ThumbnailRequest};
connect, use actix_web::{web, HttpResponse, Responder};
data::AddTagRequest, use anyhow::Context;
database,
database::schema::{tagged_photo, tags},
schema, Claims, ThumbnailRequest,
};
use actix_web::{delete, get, post, web, HttpResponse, Responder};
use chrono::Utc; use chrono::Utc;
use diesel::prelude::*; use diesel::prelude::*;
use log::{error, info}; use log::info;
use schema::{tagged_photo, tags};
use serde::Serialize; use serde::Serialize;
use std::borrow::BorrowMut;
use std::sync::Mutex;
#[post("image/tags")] pub async fn add_tag<D: TagDao>(
pub async fn add_tag(_: Claims, body: web::Json<AddTagRequest>) -> impl Responder { _: Claims,
let tag = body.tag_name.clone(); body: web::Json<AddTagRequest>,
tag_dao: web::Data<Mutex<D>>,
) -> impl Responder {
let tag_name = body.tag_name.clone();
use database::schema::tags; let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao");
let connection = &connect(); tag_dao
match tags::table .get_all_tags()
.filter(tags::name.eq(&tag)) .and_then(|tags| {
.get_result::<Tag>(connection) if let Some(tag) = tags.iter().find(|t| t.name == tag_name) {
.optional() Ok(tag.clone())
.and_then(|t| {
if let Some(t) = t {
Ok(t.id)
} else { } else {
match diesel::insert_into(tags::table) tag_dao.create_tag(&tag_name)
.values(InsertTag {
name: tag.clone(),
created_time: Utc::now().timestamp(),
})
.execute(connection)
.and_then(|_| {
no_arg_sql_function!(
last_insert_rowid,
diesel::sql_types::Integer,
"Represents the SQL last_insert_row() function"
);
diesel::select(last_insert_rowid).get_result::<i32>(connection)
}) {
Err(e) => {
error!("Error inserting tag: '{}'. {:?}", tag, e);
Err(e)
}
Ok(id) => {
info!("Inserted tag: '{}' with id: {:?}", tag, id);
Ok(id)
}
}
} }
}) })
.map(|tag_id| { .and_then(|tag| tag_dao.tag_file(&body.file_name, tag.id))
use database::schema::tagged_photo;
let file_name = body.file_name.clone();
match tagged_photo::table
.filter(tagged_photo::photo_name.eq(&file_name))
.filter(tagged_photo::tag_id.eq(tag_id))
.get_result::<TaggedPhoto>(connection)
.optional()
{
Ok(Some(_)) => HttpResponse::NoContent(),
Ok(None) => diesel::insert_into(tagged_photo::table)
.values(InsertTaggedPhoto {
tag_id,
photo_name: file_name.clone(),
created_time: Utc::now().timestamp(),
})
.execute(connection)
.map(|_| {
info!("Inserted tagged photo: {} -> '{}'", tag_id, file_name);
HttpResponse::Created()
})
.unwrap_or_else(|e| {
error!(
"Error inserting tagged photo: '{}' -> '{}'. {:?}",
tag_id, body.file_name, e
);
HttpResponse::InternalServerError()
}),
Err(e) => {
error!("Error querying tagged photo: {:?}", e);
HttpResponse::InternalServerError()
}
}
}) {
Ok(resp) => resp,
Err(e) => {
error!("{:?}", e);
HttpResponse::InternalServerError()
}
}
}
#[get("image/tags")]
pub async fn get_tags(_: Claims, request: web::Query<ThumbnailRequest>) -> impl Responder {
use schema::tagged_photo;
use schema::tags;
match tags::table
.left_join(tagged_photo::table)
.filter(tagged_photo::photo_name.eq(&request.path))
.select((tags::id, tags::name, tags::created_time))
.get_results::<Tag>(&connect())
{
Ok(tags) => HttpResponse::Ok().json(tags),
Err(e) => {
error!("Error getting tags for image: '{}'. {:?}", request.path, e);
HttpResponse::InternalServerError().finish()
}
}
}
#[delete("image/tags")]
pub async fn remove_tagged_photo(_: Claims, request: web::Json<AddTagRequest>) -> impl Responder {
use schema::tags;
match tags::table
.filter(tags::name.eq(&request.tag_name))
.get_result::<Tag>(&connect())
.optional()
.and_then(|tag| {
if let Some(tag) = tag {
use schema::tagged_photo;
diesel::delete(
tagged_photo::table
.filter(tagged_photo::tag_id.eq(tag.id))
.filter(tagged_photo::photo_name.eq(&request.file_name)),
)
.execute(&connect())
.map(|_| HttpResponse::Ok()) .map(|_| HttpResponse::Ok())
.into_http_internal_err()
}
pub async fn get_tags<D: TagDao>(
_: Claims,
request: web::Query<ThumbnailRequest>,
tag_dao: web::Data<Mutex<D>>,
) -> impl Responder {
let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao");
tag_dao
.get_tags_for_path(&request.path)
.map(|tags| HttpResponse::Ok().json(tags))
.into_http_internal_err()
}
pub async fn get_all_tags<D: TagDao>(_: Claims, tag_dao: web::Data<Mutex<D>>) -> impl Responder {
let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao");
tag_dao
.get_all_tags()
.map(|tags| HttpResponse::Ok().json(tags))
.into_http_internal_err()
}
pub async fn remove_tagged_photo<D: TagDao>(
_: Claims,
request: web::Json<AddTagRequest>,
tag_dao: web::Data<Mutex<D>>,
) -> impl Responder {
let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao");
tag_dao
.remove_tag(&request.tag_name, &request.file_name)
.map(|result| {
if result.is_some() {
HttpResponse::Ok()
} else { } else {
info!("No tag found with name '{}'", &request.tag_name); HttpResponse::NotFound()
Ok(HttpResponse::NotFound())
}
}) {
Ok(status) => status,
Err(err) => {
error!(
"Error removing tag '{}' from file: {}. {:?}",
&request.tag_name, &request.file_name, err
);
HttpResponse::InternalServerError()
}
} }
})
.into_http_internal_err()
} }
#[derive(Serialize, Queryable, Clone, Debug)] #[derive(Serialize, Queryable, Clone, Debug)]
@@ -179,3 +99,243 @@ pub struct TaggedPhoto {
pub tag_id: i32, pub tag_id: i32,
pub created_time: i64, pub created_time: i64,
} }
pub trait TagDao {
fn get_all_tags(&mut self) -> anyhow::Result<Vec<Tag>>;
fn get_tags_for_path(&mut self, path: &str) -> anyhow::Result<Vec<Tag>>;
fn create_tag(&mut self, name: &str) -> anyhow::Result<Tag>;
fn remove_tag(&mut self, tag_name: &str, path: &str) -> anyhow::Result<Option<()>>;
fn tag_file(&mut self, path: &str, tag_id: i32) -> anyhow::Result<TaggedPhoto>;
}
pub struct SqliteTagDao {
connection: SqliteConnection,
}
impl SqliteTagDao {
fn new(connection: SqliteConnection) -> Self {
SqliteTagDao { connection }
}
}
impl Default for SqliteTagDao {
fn default() -> Self {
SqliteTagDao::new(connect())
}
}
impl TagDao for SqliteTagDao {
fn get_all_tags(&mut self) -> anyhow::Result<Vec<Tag>> {
tags::table
.get_results(&mut self.connection)
.with_context(|| "Unable to get all tags")
}
fn get_tags_for_path(&mut self, path: &str) -> anyhow::Result<Vec<Tag>> {
tags::table
.left_join(tagged_photo::table)
.filter(tagged_photo::photo_name.eq(&path))
.select((tags::id, tags::name, tags::created_time))
.get_results::<Tag>(self.connection.borrow_mut())
.with_context(|| "Unable to get tags from Sqlite")
}
fn create_tag(&mut self, name: &str) -> anyhow::Result<Tag> {
diesel::insert_into(tags::table)
.values(InsertTag {
name: name.to_string(),
created_time: Utc::now().timestamp(),
})
.execute(&mut self.connection)
.with_context(|| "Unable to insert tag in Sqlite")
.and_then(|_| {
no_arg_sql_function!(
last_insert_rowid,
diesel::sql_types::Integer,
"Represents the SQL last_insert_row() function"
);
diesel::select(last_insert_rowid)
.get_result::<i32>(&mut self.connection)
.with_context(|| "Unable to get last inserted tag from Sqlite")
})
.and_then(|id| {
tags::table
.left_join(tagged_photo::table)
.filter(tagged_photo::id.eq(id))
.select((tags::id, tags::name, tags::created_time))
.get_result::<Tag>(self.connection.borrow_mut())
.with_context(|| "Unable to get tagged photo from Sqlite")
})
}
fn remove_tag(&mut self, tag_name: &str, path: &str) -> anyhow::Result<Option<()>> {
tags::table
.filter(tags::name.eq(tag_name))
.get_result::<Tag>(self.connection.borrow_mut())
.optional()
.with_context(|| format!("Unable to get tag '{}'", tag_name))
.and_then(|tag| {
if let Some(tag) = tag {
diesel::delete(
tagged_photo::table
.filter(tagged_photo::tag_id.eq(tag.id))
.filter(tagged_photo::photo_name.eq(path)),
)
.execute(&mut self.connection)
.with_context(|| format!("Unable to delete tag: '{}'", &tag.name))
.map(|_| Some(()))
} else {
info!("No tag found with name '{}'", tag_name);
Ok(None)
}
})
}
fn tag_file(&mut self, path: &str, tag_id: i32) -> anyhow::Result<TaggedPhoto> {
diesel::insert_into(tagged_photo::table)
.values(InsertTaggedPhoto {
tag_id,
photo_name: path.to_string(),
created_time: Utc::now().timestamp(),
})
.execute(self.connection.borrow_mut())
.with_context(|| "Unable to insert tag into sqlite")
.and_then(|tagged_id| {
tagged_photo::table
.find(tagged_id as i32)
.first(self.connection.borrow_mut())
.with_context(|| "Error getting inserted tagged photo")
})
}
}
#[cfg(test)]
mod tests {
use actix_web::web::Data;
use std::{borrow::Borrow, cell::RefCell, collections::HashMap};
use diesel::result::Error::NotFound;
use super::*;
struct TestTagDao {
tags: RefCell<Vec<Tag>>,
tagged_photos: RefCell<HashMap<String, Vec<Tag>>>,
}
impl TestTagDao {
fn new() -> Self {
Self {
tags: RefCell::new(vec![]),
tagged_photos: RefCell::new(HashMap::new()),
}
}
}
impl TagDao for TestTagDao {
fn get_all_tags(&self) -> anyhow::Result<Vec<Tag>> {
Ok(self.tags.borrow().clone())
}
fn get_tags_for_path(&self, path: &str) -> anyhow::Result<Vec<Tag>> {
Ok(self
.tagged_photos
.borrow()
.get(path)
.unwrap_or(&vec![])
.clone())
}
fn create_tag(&self, name: &str) -> anyhow::Result<Tag> {
let tag = Tag {
id: 0,
name: name.to_string(),
created_time: Utc::now().timestamp(),
};
self.tags.borrow_mut().push(tag.clone());
Ok(tag)
}
fn remove_tag(&self, tag_name: &str, path: &str) -> anyhow::Result<Option<()>> {
let mut clone = {
let photo_tags = &self.tagged_photos.borrow()[path];
photo_tags.clone()
};
clone.retain(|t| t.name != tag_name);
self.tagged_photos
.borrow_mut()
.insert(path.to_string(), clone);
let index = self.tags.borrow().iter().position(|t| t.name == tag_name);
if let Some(index) = index {
self.tags.borrow_mut().remove(index);
Ok(Some(()))
} else {
Ok(None)
}
}
fn tag_file(&self, path: &str, tag_id: i32) -> anyhow::Result<TaggedPhoto> {
if let Some(tag) = self.tags.borrow().iter().find(|t| t.id == tag_id) {
let tagged_photo = TaggedPhoto {
id: self.tagged_photos.borrow().len() as i32,
tag_id: tag.id,
created_time: Utc::now().timestamp(),
photo_name: path.to_string(),
};
//TODO: Add to existing tags (? huh)
self.tagged_photos
.borrow_mut()
.insert(path.to_string(), vec![tag.clone()]);
Ok(tagged_photo)
} else {
Err(NotFound.into())
}
}
}
#[actix_rt::test]
async fn add_new_tag_test() {
let tag_dao = Data::new(TestTagDao::new());
let claims = Claims::valid_user(String::from("1"));
let body = AddTagRequest {
file_name: String::from("test.png"),
tag_name: String::from("test-tag"),
};
add_tag(claims, web::Json(body), tag_dao.clone()).await;
let tags = tag_dao.get_all_tags().unwrap();
assert!(tags.len() == 1);
assert!(tags.first().unwrap().name == "test-tag");
assert!(tag_dao.tagged_photos.borrow()["test.png"].len() == 1)
}
#[actix_rt::test]
async fn remove_tag_test() {
let tag_dao = Data::new(TestTagDao::new());
let claims = Claims::valid_user(String::from("1"));
let add_request = AddTagRequest {
file_name: String::from("test.png"),
tag_name: String::from("test-tag"),
};
let remove_request = AddTagRequest {
file_name: String::from("test.png"),
tag_name: String::from("test-tag"),
};
add_tag(claims.clone(), web::Json(add_request), tag_dao.clone()).await;
remove_tagged_photo(claims, web::Json(remove_request), tag_dao.clone()).await;
let tags = tag_dao.get_all_tags().unwrap();
assert!(tags.is_empty());
let tagged_photos = tag_dao.tagged_photos.borrow();
let previously_added_tagged_photo = tagged_photos.get("test.png").unwrap();
assert!(previously_added_tagged_photo.len() == 0)
}
}