feature/tagging #16
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"
|
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"
|
||||||
|
|||||||
12
src/auth.rs
12
src/auth.rs
@@ -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(),
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/main.rs
59
src/main.rs
@@ -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();
|
||||||
|
|||||||
436
src/tags.rs
436
src/tags.rs
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user