feature/tagging #16

Merged
cameron merged 22 commits from feature/tagging into master 2023-04-10 12:55:28 +00:00
6 changed files with 143 additions and 50 deletions
Showing only changes of commit de4041bd17 - Show all commits

View File

@@ -1,4 +1,3 @@
use std::sync::Mutex;
use actix_web::Responder; use actix_web::Responder;
use actix_web::{ use actix_web::{
web::{self, Json}, web::{self, Json},
@@ -7,6 +6,7 @@ use actix_web::{
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use jsonwebtoken::{encode, EncodingKey, Header}; use jsonwebtoken::{encode, EncodingKey, Header};
use log::{debug, error}; use log::{debug, error};
use std::sync::Mutex;
use crate::{ use crate::{
data::{secret_key, Claims, CreateAccountRequest, LoginRequest, Token}, data::{secret_key, Claims, CreateAccountRequest, LoginRequest, Token},
@@ -32,7 +32,10 @@ async fn register<D: UserDao>(
} }
} }
pub async fn login<D: UserDao>(creds: Json<LoginRequest>, user_dao: web::Data<Mutex<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"); let mut user_dao = user_dao.lock().expect("Unable to get UserDao");

View File

@@ -1,10 +1,8 @@
use bcrypt::{hash, verify, DEFAULT_COST}; use bcrypt::{hash, verify, DEFAULT_COST};
use diesel::prelude::*; use diesel::prelude::*;
use diesel::sqlite::SqliteConnection; use diesel::sqlite::SqliteConnection;
use std::{
sync::{Arc, Mutex},
};
use std::ops::DerefMut; use std::ops::DerefMut;
use std::sync::{Arc, Mutex};
use crate::database::models::{Favorite, InsertFavorite, InsertUser, User}; use crate::database::models::{Favorite, InsertFavorite, InsertUser, User};

View File

@@ -33,9 +33,4 @@ table! {
joinable!(tagged_photo -> tags (tag_id)); joinable!(tagged_photo -> tags (tag_id));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(favorites, tagged_photo, tags, users,);
favorites,
tagged_photo,
tags,
users,
);

View File

@@ -36,6 +36,7 @@ use crate::auth::login;
use crate::data::*; use crate::data::*;
use crate::database::*; use crate::database::*;
use crate::files::{is_image_or_video, is_valid_full_path}; use crate::files::{is_image_or_video, is_valid_full_path};
use crate::service::ServiceBuilder;
use crate::state::AppState; use crate::state::AppState;
use crate::tags::*; use crate::tags::*;
use crate::video::*; use crate::video::*;
@@ -49,6 +50,7 @@ mod state;
mod tags; mod tags;
mod video; mod video;
mod service;
#[cfg(test)] #[cfg(test)]
mod testhelpers; mod testhelpers;
@@ -482,14 +484,7 @@ 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_feature(add_tag_services::<_, SqliteTagDao>)
web::resource("image/tags")
.route(web::post().to(add_tag::<SqliteTagDao>))
.route(web::get().to(get_tags::<SqliteTagDao>))
.route(web::delete().to(remove_tagged_photo::<SqliteTagDao>)),
)
.service(web::resource("image/tags/all").route(web::get().to(get_all_tags::<SqliteTagDao>)))
.service(web::resource("image/tags/batch").route(web::post().to(update_tags::<SqliteTagDao>)))
.app_data(app_data.clone()) .app_data(app_data.clone())
.app_data::<Data<Mutex<SqliteUserDao>>>(Data::new(Mutex::new(user_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( .app_data::<Data<Mutex<Box<dyn FavoriteDao>>>>(Data::new(Mutex::new(Box::new(

16
src/service.rs Normal file
View File

@@ -0,0 +1,16 @@
use actix_web::App;
pub trait ServiceBuilder<T> {
fn add_feature<F>(self, f: F) -> App<T>
where
F: Fn(App<T>) -> App<T>;
}
impl<T> ServiceBuilder<T> for App<T> {
fn add_feature<F>(self, create_feature: F) -> App<T>
where
F: Fn(App<T>) -> App<T>,
{
create_feature(self)
}
}

View File

@@ -1,5 +1,6 @@
use crate::{connect, data::AddTagRequest, error::IntoHttpError, schema, Claims, ThumbnailRequest}; use crate::{connect, data::AddTagRequest, error::IntoHttpError, schema, Claims, ThumbnailRequest};
use actix_web::{web, HttpResponse, Responder}; use actix_web::dev::{ServiceFactory, ServiceRequest};
use actix_web::{web, App, HttpResponse, Responder};
use anyhow::Context; use anyhow::Context;
use chrono::Utc; use chrono::Utc;
use diesel::prelude::*; use diesel::prelude::*;
@@ -9,7 +10,21 @@ use serde::{Deserialize, Serialize};
use std::borrow::BorrowMut; use std::borrow::BorrowMut;
use std::sync::Mutex; use std::sync::Mutex;
pub async fn add_tag<D: TagDao>( pub fn add_tag_services<T, TagD: TagDao + 'static>(app: App<T>) -> App<T>
where
T: ServiceFactory<ServiceRequest, Config = (), Error = actix_web::Error, InitError = ()>,
{
app.service(
web::resource("image/tags")
.route(web::post().to(add_tag::<TagD>))
.route(web::get().to(get_tags::<TagD>))
.route(web::delete().to(remove_tagged_photo::<TagD>)),
)
.service(web::resource("image/tags/all").route(web::get().to(get_all_tags::<TagD>)))
.service(web::resource("image/tags/batch").route(web::post().to(update_tags::<TagD>)))
}
async fn add_tag<D: TagDao>(
_: Claims, _: Claims,
body: web::Json<AddTagRequest>, body: web::Json<AddTagRequest>,
tag_dao: web::Data<Mutex<D>>, tag_dao: web::Data<Mutex<D>>,
@@ -32,7 +47,7 @@ pub async fn add_tag<D: TagDao>(
.into_http_internal_err() .into_http_internal_err()
} }
pub async fn get_tags<D: TagDao>( async fn get_tags<D: TagDao>(
_: Claims, _: Claims,
request: web::Query<ThumbnailRequest>, request: web::Query<ThumbnailRequest>,
tag_dao: web::Data<Mutex<D>>, tag_dao: web::Data<Mutex<D>>,
@@ -44,7 +59,7 @@ pub async fn get_tags<D: TagDao>(
.into_http_internal_err() .into_http_internal_err()
} }
pub async fn get_all_tags<D: TagDao>(_: Claims, tag_dao: web::Data<Mutex<D>>) -> impl Responder { 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"); let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao");
tag_dao tag_dao
.get_all_tags() .get_all_tags()
@@ -52,7 +67,7 @@ pub async fn get_all_tags<D: TagDao>(_: Claims, tag_dao: web::Data<Mutex<D>>) ->
.into_http_internal_err() .into_http_internal_err()
} }
pub async fn remove_tagged_photo<D: TagDao>( async fn remove_tagged_photo<D: TagDao>(
_: Claims, _: Claims,
request: web::Json<AddTagRequest>, request: web::Json<AddTagRequest>,
tag_dao: web::Data<Mutex<D>>, tag_dao: web::Data<Mutex<D>>,
@@ -70,39 +85,55 @@ pub async fn remove_tagged_photo<D: TagDao>(
.into_http_internal_err() .into_http_internal_err()
} }
pub async fn update_tags<D: TagDao>(_: Claims, tag_dao: web::Data<Mutex<D>>, request: web::Json<AddTagsRequest>) -> impl Responder { async fn update_tags<D: TagDao>(
let mut dao = tag_dao.lock() _: Claims,
.expect("Unable to get TagDao"); tag_dao: web::Data<Mutex<D>>,
request: web::Json<AddTagsRequest>,
) -> impl Responder {
let mut dao = tag_dao.lock().expect("Unable to get TagDao");
return dao.get_tags_for_path(&request.file_name) return dao
.and_then(|existing_tags| { .get_tags_for_path(&request.file_name)
dao.get_all_tags().map(|all| (existing_tags, all)) .and_then(|existing_tags| dao.get_all_tags().map(|all| (existing_tags, all)))
})
.and_then(|(existing_tags, all_tags)| { .and_then(|(existing_tags, all_tags)| {
let tags_to_remove = existing_tags.iter() let tags_to_remove = existing_tags
.iter()
.filter(|&t| !request.tag_ids.contains(&t.id)) .filter(|&t| !request.tag_ids.contains(&t.id))
.collect::<Vec<&Tag>>(); .collect::<Vec<&Tag>>();
for tag in tags_to_remove { for tag in tags_to_remove {
info!("Removing tag {:?} from file: {:?}", tag.name, request.file_name); info!(
"Removing tag {:?} from file: {:?}",
tag.name, request.file_name
);
dao.remove_tag(&tag.name, &request.file_name) dao.remove_tag(&tag.name, &request.file_name)
.expect(&format!("Unable to remove tag {:?}", &tag.name)); .expect(&format!("Unable to remove tag {:?}", &tag.name));
} }
let new_tags = all_tags.iter() let new_tags = all_tags
.iter()
.filter(|&t| !existing_tags.contains(t) && request.tag_ids.contains(&t.id)) .filter(|&t| !existing_tags.contains(t) && request.tag_ids.contains(&t.id))
.collect::<Vec<&Tag>>(); .collect::<Vec<&Tag>>();
for new_tag in new_tags { for new_tag in new_tags {
info!("Adding tag {:?} to file: {:?}", new_tag.name, request.file_name); info!(
"Adding tag {:?} to file: {:?}",
new_tag.name, request.file_name
);
dao.tag_file(&request.file_name, new_tag.id) dao.tag_file(&request.file_name, new_tag.id)
.with_context(|| format!("Unable to tag file {:?} with tag: {:?}", request.file_name, new_tag.name)) .with_context(|| {
format!(
"Unable to tag file {:?} with tag: {:?}",
request.file_name, new_tag.name
)
})
.unwrap(); .unwrap();
} }
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
}).into_http_internal_err(); })
.into_http_internal_err();
} }
#[derive(Serialize, Queryable, Clone, Debug, PartialEq)] #[derive(Serialize, Queryable, Clone, Debug, PartialEq)]
@@ -256,16 +287,18 @@ impl TagDao for SqliteTagDao {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_web::web::Data; use actix_web::web::{Data, Json};
use std::{borrow::Borrow, cell::RefCell, collections::HashMap}; use std::{cell::RefCell, collections::HashMap};
use diesel::result::Error::NotFound; use diesel::result::Error::NotFound;
use log::warn;
use super::*; use super::*;
struct TestTagDao { struct TestTagDao {
tags: RefCell<Vec<Tag>>, tags: RefCell<Vec<Tag>>,
tagged_photos: RefCell<HashMap<String, Vec<Tag>>>, tagged_photos: RefCell<HashMap<String, Vec<Tag>>>,
tag_count: i32,
} }
impl TestTagDao { impl TestTagDao {
@@ -273,6 +306,7 @@ mod tests {
Self { Self {
tags: RefCell::new(vec![]), tags: RefCell::new(vec![]),
tagged_photos: RefCell::new(HashMap::new()), tagged_photos: RefCell::new(HashMap::new()),
tag_count: 0,
} }
} }
} }
@@ -283,6 +317,9 @@ mod tests {
} }
fn get_tags_for_path(&mut self, path: &str) -> anyhow::Result<Vec<Tag>> { fn get_tags_for_path(&mut self, path: &str) -> anyhow::Result<Vec<Tag>> {
info!("Getting test tags for: {:?}", path);
warn!("Tags for path: {:?}", self.tagged_photos);
Ok(self Ok(self
.tagged_photos .tagged_photos
.borrow() .borrow()
@@ -292,13 +329,18 @@ mod tests {
} }
fn create_tag(&mut self, name: &str) -> anyhow::Result<Tag> { fn create_tag(&mut self, name: &str) -> anyhow::Result<Tag> {
self.tag_count += 1;
let tag_id = self.tag_count;
let tag = Tag { let tag = Tag {
id: 0, id: tag_id as i32,
name: name.to_string(), name: name.to_string(),
created_time: Utc::now().timestamp(), created_time: Utc::now().timestamp(),
}; };
self.tags.borrow_mut().push(tag.clone()); self.tags.borrow_mut().push(tag.clone());
debug!("Created tag: {:?}", tag);
Ok(tag) Ok(tag)
} }
@@ -323,7 +365,11 @@ mod tests {
} }
fn tag_file(&mut self, path: &str, tag_id: i32) -> anyhow::Result<TaggedPhoto> { fn tag_file(&mut self, path: &str, tag_id: i32) -> anyhow::Result<TaggedPhoto> {
debug!("Tagging file: {:?} with tag_id: {:?}", path, tag_id);
if let Some(tag) = self.tags.borrow().iter().find(|t| t.id == tag_id) { if let Some(tag) = self.tags.borrow().iter().find(|t| t.id == tag_id) {
debug!("Found tag: {:?}", tag);
let tagged_photo = TaggedPhoto { let tagged_photo = TaggedPhoto {
id: self.tagged_photos.borrow().len() as i32, id: self.tagged_photos.borrow().len() as i32,
tag_id: tag.id, tag_id: tag.id,
@@ -331,10 +377,19 @@ mod tests {
photo_name: path.to_string(), photo_name: path.to_string(),
}; };
if self.tagged_photos.borrow().contains_key(path) {
let mut photo_tags = self.tagged_photos.borrow()[path].clone();
photo_tags.push(tag.clone());
self.tagged_photos
.borrow_mut()
.insert(path.to_string(), photo_tags);
} else {
//TODO: Add to existing tags (? huh) //TODO: Add to existing tags (? huh)
self.tagged_photos self.tagged_photos
.borrow_mut() .borrow_mut()
.insert(path.to_string(), vec![tag.clone()]); .insert(path.to_string(), vec![tag.clone()]);
}
Ok(tagged_photo) Ok(tagged_photo)
} else { } else {
@@ -345,14 +400,14 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn add_new_tag_test() { async fn add_new_tag_test() {
let mut tag_dao = TestTagDao::new(); let tag_dao = TestTagDao::new();
let claims = Claims::valid_user(String::from("1")); let claims = Claims::valid_user(String::from("1"));
let body = AddTagRequest { let body = AddTagRequest {
file_name: String::from("test.png"), file_name: String::from("test.png"),
tag_name: String::from("test-tag"), tag_name: String::from("test-tag"),
}; };
let mut tag_data = Data::new(Mutex::new(tag_dao)); let tag_data = Data::new(Mutex::new(tag_dao));
add_tag(claims, web::Json(body), tag_data.clone()).await; add_tag(claims, web::Json(body), tag_data.clone()).await;
let mut tag_dao = tag_data.lock().unwrap(); let mut tag_dao = tag_data.lock().unwrap();
@@ -365,7 +420,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn remove_tag_test() { async fn remove_tag_test() {
let mut tag_dao = TestTagDao::new(); let tag_dao = TestTagDao::new();
let claims = Claims::valid_user(String::from("1")); let claims = Claims::valid_user(String::from("1"));
let add_request = AddTagRequest { let add_request = AddTagRequest {
file_name: String::from("test.png"), file_name: String::from("test.png"),
@@ -388,4 +443,35 @@ mod tests {
let previously_added_tagged_photo = tagged_photos.get("test.png").unwrap(); let previously_added_tagged_photo = tagged_photos.get("test.png").unwrap();
assert_eq!(previously_added_tagged_photo.len(), 0) assert_eq!(previously_added_tagged_photo.len(), 0)
} }
#[actix_rt::test]
async fn replace_tags_keeps_existing_tags_removes_extras_adds_missing_test() {
let mut tag_dao = TestTagDao::new();
let new_tag = tag_dao.create_tag("Test").unwrap();
let new_tag2 = tag_dao.create_tag("Test2").unwrap();
let _ = tag_dao.create_tag("Test3").unwrap();
tag_dao.tag_file("test.jpg", new_tag.id).unwrap();
tag_dao.tag_file("test.jpg", new_tag2.id).unwrap();
let claims = Claims::valid_user(String::from("1"));
let tag_data = Data::new(Mutex::new(tag_dao));
let add_tags_request = AddTagsRequest {
tag_ids: vec![1, 3],
file_name: String::from("test.jpg"),
};
update_tags(claims, tag_data.clone(), Json(add_tags_request)).await;
let tag_dao = tag_data.lock().unwrap();
let tags_for_test_photo = &tag_dao.tagged_photos.borrow()["test.jpg"];
assert_eq!(tags_for_test_photo.len(), 2);
// ID of 2 was removed and 3 was added
assert_eq!(
tags_for_test_photo.iter().find(|&t| t.name == "Test2"),
None
);
}
} }