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:
991
Cargo.lock
generated
991
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,8 @@ futures = "0.3.5"
|
||||
jsonwebtoken = "7.2.0"
|
||||
serde = "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"
|
||||
sha2 = "0.9"
|
||||
chrono = "0.4"
|
||||
|
||||
12
src/auth.rs
12
src/auth.rs
@@ -1,3 +1,4 @@
|
||||
use std::sync::Mutex;
|
||||
use actix_web::Responder;
|
||||
use actix_web::{
|
||||
web::{self, Json},
|
||||
@@ -15,12 +16,13 @@ use crate::{
|
||||
#[allow(dead_code)]
|
||||
async fn register<D: UserDao>(
|
||||
user: Json<CreateAccountRequest>,
|
||||
user_dao: web::Data<D>,
|
||||
user_dao: web::Data<Mutex<D>>,
|
||||
) -> impl Responder {
|
||||
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()
|
||||
} 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()
|
||||
} else {
|
||||
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);
|
||||
|
||||
let mut user_dao = user_dao.lock().expect("Unable to get UserDao");
|
||||
|
||||
if let Some(user) = user_dao.get_user(&creds.username, &creds.password) {
|
||||
let claims = Claims {
|
||||
sub: user.id.to_string(),
|
||||
|
||||
@@ -16,12 +16,27 @@ pub struct Token<'a> {
|
||||
pub token: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Claims {
|
||||
pub sub: String,
|
||||
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 {
|
||||
if cfg!(test) {
|
||||
String::from("test_key")
|
||||
|
||||
@@ -2,9 +2,9 @@ use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
use diesel::prelude::*;
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use std::{
|
||||
ops::Deref,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use crate::database::models::{Favorite, InsertFavorite, InsertUser, User};
|
||||
|
||||
@@ -12,9 +12,9 @@ pub mod models;
|
||||
pub mod schema;
|
||||
|
||||
pub trait UserDao {
|
||||
fn create_user(&self, user: &str, password: &str) -> Option<User>;
|
||||
fn get_user(&self, user: &str, password: &str) -> Option<User>;
|
||||
fn user_exists(&self, user: &str) -> bool;
|
||||
fn create_user(&mut self, user: &str, password: &str) -> Option<User>;
|
||||
fn get_user(&mut self, user: &str, password: &str) -> Option<User>;
|
||||
fn user_exists(&mut self, user: &str) -> bool;
|
||||
}
|
||||
|
||||
pub struct SqliteUserDao {
|
||||
@@ -31,7 +31,7 @@ impl SqliteUserDao {
|
||||
|
||||
impl UserDao for SqliteUserDao {
|
||||
// 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::*;
|
||||
|
||||
let hashed = hash(pass, DEFAULT_COST);
|
||||
@@ -41,12 +41,12 @@ impl UserDao for SqliteUserDao {
|
||||
username: user,
|
||||
password: &hash,
|
||||
})
|
||||
.execute(&self.connection)
|
||||
.execute(&mut self.connection)
|
||||
.unwrap();
|
||||
|
||||
users
|
||||
.filter(username.eq(username))
|
||||
.load::<User>(&self.connection)
|
||||
.load::<User>(&mut self.connection)
|
||||
.unwrap()
|
||||
.first()
|
||||
.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::*;
|
||||
|
||||
match users
|
||||
.filter(username.eq(user))
|
||||
.load::<User>(&self.connection)
|
||||
.load::<User>(&mut self.connection)
|
||||
.unwrap_or_default()
|
||||
.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::*;
|
||||
|
||||
users
|
||||
.filter(username.eq(user))
|
||||
.load::<User>(&self.connection)
|
||||
.load::<User>(&mut self.connection)
|
||||
.unwrap_or_default()
|
||||
.first()
|
||||
.is_some()
|
||||
@@ -109,9 +109,9 @@ pub enum DbErrorKind {
|
||||
}
|
||||
|
||||
pub trait FavoriteDao: Sync + Send {
|
||||
fn add_favorite(&self, user_id: i32, favorite_path: &str) -> Result<usize, DbError>;
|
||||
fn remove_favorite(&self, user_id: i32, favorite_path: String);
|
||||
fn get_favorites(&self, user_id: i32) -> Result<Vec<Favorite>, DbError>;
|
||||
fn add_favorite(&mut self, user_id: i32, favorite_path: &str) -> Result<usize, DbError>;
|
||||
fn remove_favorite(&mut self, user_id: i32, favorite_path: String);
|
||||
fn get_favorites(&mut self, user_id: i32) -> Result<Vec<Favorite>, DbError>;
|
||||
}
|
||||
|
||||
pub struct SqliteFavoriteDao {
|
||||
@@ -127,15 +127,14 @@ impl 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::*;
|
||||
|
||||
let connection = self.connection.lock().unwrap();
|
||||
let connection = connection.deref();
|
||||
let mut connection = self.connection.lock().expect("Unable to get FavoriteDao");
|
||||
|
||||
if favorites
|
||||
.filter(userid.eq(user_id).and(path.eq(&favorite_path)))
|
||||
.first::<Favorite>(connection)
|
||||
.first::<Favorite>(connection.deref_mut())
|
||||
.is_err()
|
||||
{
|
||||
diesel::insert_into(favorites)
|
||||
@@ -143,28 +142,28 @@ impl FavoriteDao for SqliteFavoriteDao {
|
||||
userid: &user_id,
|
||||
path: favorite_path,
|
||||
})
|
||||
.execute(connection)
|
||||
.execute(connection.deref_mut())
|
||||
.map_err(|_| DbError::new(DbErrorKind::InsertError))
|
||||
} else {
|
||||
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::*;
|
||||
|
||||
diesel::delete(favorites)
|
||||
.filter(userid.eq(user_id).and(path.eq(favorite_path)))
|
||||
.execute(self.connection.lock().unwrap().deref())
|
||||
.execute(self.connection.lock().unwrap().deref_mut())
|
||||
.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::*;
|
||||
|
||||
favorites
|
||||
.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))
|
||||
}
|
||||
}
|
||||
|
||||
14
src/error.rs
Normal file
14
src/error.rs
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
||||
83
src/main.rs
83
src/main.rs
@@ -4,6 +4,7 @@ extern crate rayon;
|
||||
|
||||
use actix_web::web::Data;
|
||||
use actix_web_prom::PrometheusMetricsBuilder;
|
||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
||||
use futures::stream::StreamExt;
|
||||
use lazy_static::lazy_static;
|
||||
use prometheus::{self, IntGauge};
|
||||
@@ -14,6 +15,8 @@ use std::{
|
||||
io::ErrorKind,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::error::Error;
|
||||
use std::sync::Mutex;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use actix_files::NamedFile;
|
||||
@@ -23,6 +26,7 @@ use actix_web::{
|
||||
web::{self, BufMut, BytesMut},
|
||||
App, HttpRequest, HttpResponse, HttpServer, Responder,
|
||||
};
|
||||
use diesel::sqlite::Sqlite;
|
||||
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||
use rayon::prelude::*;
|
||||
|
||||
@@ -39,6 +43,7 @@ use crate::video::*;
|
||||
mod auth;
|
||||
mod data;
|
||||
mod database;
|
||||
mod error;
|
||||
mod files;
|
||||
mod state;
|
||||
mod tags;
|
||||
@@ -60,12 +65,14 @@ lazy_static! {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
|
||||
|
||||
#[get("/image")]
|
||||
async fn get_image(
|
||||
_claims: Claims,
|
||||
request: HttpRequest,
|
||||
req: web::Query<ThumbnailRequest>,
|
||||
app_state: web::Data<AppState>,
|
||||
app_state: Data<AppState>,
|
||||
) -> impl Responder {
|
||||
if let Some(path) = is_valid_full_path(&app_state.base_path, &req.path) {
|
||||
if req.size.is_some() {
|
||||
@@ -97,7 +104,7 @@ async fn get_image(
|
||||
async fn get_file_metadata(
|
||||
_: Claims,
|
||||
path: web::Query<ThumbnailRequest>,
|
||||
app_state: web::Data<AppState>,
|
||||
app_state: Data<AppState>,
|
||||
) -> impl Responder {
|
||||
match is_valid_full_path(&app_state.base_path, &path.path)
|
||||
.ok_or_else(|| ErrorKind::InvalidData.into())
|
||||
@@ -119,7 +126,7 @@ async fn get_file_metadata(
|
||||
async fn upload_image(
|
||||
_: Claims,
|
||||
mut payload: mp::Multipart,
|
||||
app_state: web::Data<AppState>,
|
||||
app_state: Data<AppState>,
|
||||
) -> impl Responder {
|
||||
let mut file_content: BytesMut = BytesMut::new();
|
||||
let mut file_name: Option<String> = None;
|
||||
@@ -148,7 +155,7 @@ async fn upload_image(
|
||||
if !file_content.is_empty() {
|
||||
let full_path = PathBuf::from(&path).join(file_name.unwrap());
|
||||
if let Some(full_path) =
|
||||
is_valid_full_path(&app_state.base_path, full_path.to_str().unwrap_or(""))
|
||||
is_valid_full_path(&app_state.base_path, full_path.to_str().unwrap_or(""))
|
||||
{
|
||||
if !full_path.is_file() && is_image_or_video(&full_path) {
|
||||
let mut file = File::create(full_path).unwrap();
|
||||
@@ -170,7 +177,7 @@ async fn upload_image(
|
||||
#[post("/video/generate")]
|
||||
async fn generate_video(
|
||||
_claims: Claims,
|
||||
app_state: web::Data<AppState>,
|
||||
app_state: Data<AppState>,
|
||||
body: web::Json<ThumbnailRequest>,
|
||||
) -> impl Responder {
|
||||
let filename = PathBuf::from(&body.path);
|
||||
@@ -200,7 +207,7 @@ async fn stream_video(
|
||||
request: HttpRequest,
|
||||
_: Claims,
|
||||
path: web::Query<ThumbnailRequest>,
|
||||
app_state: web::Data<AppState>,
|
||||
app_state: Data<AppState>,
|
||||
) -> impl Responder {
|
||||
let playlist = &path.path;
|
||||
debug!("Playlist: {}", playlist);
|
||||
@@ -235,9 +242,10 @@ async fn get_video_part(
|
||||
#[get("image/favorites")]
|
||||
async fn favorites(
|
||||
claims: Claims,
|
||||
favorites_dao: web::Data<Box<dyn FavoriteDao>>,
|
||||
favorites_dao: Data<Mutex<Box<dyn FavoriteDao>>>,
|
||||
) -> 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)) => {
|
||||
let favorites = favorites
|
||||
@@ -262,14 +270,14 @@ async fn favorites(
|
||||
async fn put_add_favorite(
|
||||
claims: Claims,
|
||||
body: web::Json<AddFavoriteRequest>,
|
||||
favorites_dao: web::Data<Box<dyn FavoriteDao>>,
|
||||
favorites_dao: Data<Mutex<Box<dyn FavoriteDao>>>,
|
||||
) -> impl Responder {
|
||||
if let Ok(user_id) = claims.sub.parse::<i32>() {
|
||||
let path = body.path.clone();
|
||||
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
|
||||
{
|
||||
Ok(Err(e)) if e.kind == DbErrorKind::AlreadyExists => {
|
||||
debug!("Favorite: {} exists for user: {}", &body.path, user_id);
|
||||
@@ -298,15 +306,15 @@ async fn put_add_favorite(
|
||||
async fn delete_favorite(
|
||||
claims: Claims,
|
||||
body: web::Query<AddFavoriteRequest>,
|
||||
favorites_dao: web::Data<Box<dyn FavoriteDao>>,
|
||||
favorites_dao: Data<Mutex<Box<dyn FavoriteDao>>>,
|
||||
) -> impl Responder {
|
||||
if let Ok(user_id) = claims.sub.parse::<i32>() {
|
||||
let path = body.path.clone();
|
||||
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
|
||||
.unwrap();
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
info!(
|
||||
"Removing favorite \"{}\" for userid: {}",
|
||||
@@ -325,7 +333,7 @@ fn create_thumbnails() {
|
||||
|
||||
let images = PathBuf::from(dotenv::var("BASE_PATH").unwrap());
|
||||
|
||||
walkdir::WalkDir::new(&images)
|
||||
WalkDir::new(&images)
|
||||
.into_iter()
|
||||
.collect::<Vec<Result<_, _>>>()
|
||||
.into_par_iter()
|
||||
@@ -422,12 +430,15 @@ fn main() -> std::io::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
env_logger::init();
|
||||
|
||||
run_migrations(&mut connect())
|
||||
.expect("Failed to run migrations");
|
||||
|
||||
create_thumbnails();
|
||||
watch_files();
|
||||
|
||||
let system = actix::System::new();
|
||||
system.block_on(async {
|
||||
let app_data = web::Data::new(AppState::default());
|
||||
let app_data = Data::new(AppState::default());
|
||||
|
||||
let labels = HashMap::new();
|
||||
let prometheus = PrometheusMetricsBuilder::new("api")
|
||||
@@ -447,6 +458,7 @@ fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(move || {
|
||||
let user_dao = SqliteUserDao::new();
|
||||
let favorites_dao = SqliteFavoriteDao::new();
|
||||
let tag_dao = SqliteTagDao::default();
|
||||
App::new()
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(web::resource("/login").route(web::post().to(login::<SqliteUserDao>)))
|
||||
@@ -460,21 +472,32 @@ fn main() -> std::io::Result<()> {
|
||||
.service(put_add_favorite)
|
||||
.service(delete_favorite)
|
||||
.service(get_file_metadata)
|
||||
.service(add_tag)
|
||||
.service(get_tags)
|
||||
.service(remove_tagged_photo)
|
||||
.service(
|
||||
web::resource("image/tags")
|
||||
.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::<Data<SqliteUserDao>>(Data::new(user_dao))
|
||||
.app_data::<Data<Box<dyn FavoriteDao>>>(Data::new(Box::new(favorites_dao)))
|
||||
.app_data::<Data<Mutex<SqliteUserDao>>>(Data::new(Mutex::new(user_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())
|
||||
})
|
||||
.bind(dotenv::var("BIND_URL").unwrap())?
|
||||
.bind("localhost:8088")?
|
||||
.run()
|
||||
.await
|
||||
.bind(dotenv::var("BIND_URL").unwrap())?
|
||||
.bind("localhost:8088")?
|
||||
.run()
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn run_migrations(connection: &mut impl MigrationHarness<Sqlite>) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
|
||||
connection.run_pending_migrations(MIGRATIONS)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn watch_files() {
|
||||
std::thread::spawn(|| {
|
||||
let (wtx, wrx) = channel();
|
||||
@@ -492,10 +515,10 @@ fn watch_files() {
|
||||
let image_base_path = PathBuf::from(env::var("BASE_PATH").unwrap());
|
||||
let image_relative = orig.strip_prefix(&image_base_path).unwrap();
|
||||
if let Ok(old_thumbnail) =
|
||||
env::var("THUMBNAILS").map(PathBuf::from).map(|mut base| {
|
||||
base.push(image_relative);
|
||||
base
|
||||
})
|
||||
env::var("THUMBNAILS").map(PathBuf::from).map(|mut base| {
|
||||
base.push(image_relative);
|
||||
base
|
||||
})
|
||||
{
|
||||
if let Err(e) = std::fs::remove_file(&old_thumbnail) {
|
||||
error!(
|
||||
|
||||
430
src/tags.rs
430
src/tags.rs
@@ -1,153 +1,73 @@
|
||||
use crate::{
|
||||
connect,
|
||||
data::AddTagRequest,
|
||||
database,
|
||||
database::schema::{tagged_photo, tags},
|
||||
schema, Claims, ThumbnailRequest,
|
||||
};
|
||||
use actix_web::{delete, get, post, web, HttpResponse, Responder};
|
||||
use crate::{connect, data::AddTagRequest, error::IntoHttpError, schema, Claims, ThumbnailRequest};
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
use anyhow::Context;
|
||||
use chrono::Utc;
|
||||
use diesel::prelude::*;
|
||||
use log::{error, info};
|
||||
use log::info;
|
||||
use schema::{tagged_photo, tags};
|
||||
use serde::Serialize;
|
||||
use std::borrow::BorrowMut;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[post("image/tags")]
|
||||
pub async fn add_tag(_: Claims, body: web::Json<AddTagRequest>) -> impl Responder {
|
||||
let tag = body.tag_name.clone();
|
||||
pub async fn add_tag<D: TagDao>(
|
||||
_: Claims,
|
||||
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();
|
||||
match tags::table
|
||||
.filter(tags::name.eq(&tag))
|
||||
.get_result::<Tag>(connection)
|
||||
.optional()
|
||||
.and_then(|t| {
|
||||
if let Some(t) = t {
|
||||
Ok(t.id)
|
||||
tag_dao
|
||||
.get_all_tags()
|
||||
.and_then(|tags| {
|
||||
if let Some(tag) = tags.iter().find(|t| t.name == tag_name) {
|
||||
Ok(tag.clone())
|
||||
} else {
|
||||
match diesel::insert_into(tags::table)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
tag_dao.create_tag(&tag_name)
|
||||
}
|
||||
})
|
||||
.map(|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()
|
||||
}
|
||||
}
|
||||
.and_then(|tag| tag_dao.tag_file(&body.file_name, tag.id))
|
||||
.map(|_| HttpResponse::Ok())
|
||||
.into_http_internal_err()
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
#[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())
|
||||
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 {
|
||||
info!("No tag found with name '{}'", &request.tag_name);
|
||||
Ok(HttpResponse::NotFound())
|
||||
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)]
|
||||
@@ -179,3 +99,243 @@ pub struct TaggedPhoto {
|
||||
pub tag_id: i32,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user