From 4834cacfc37af2835e767dd211ad0d9793797fce Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Fri, 3 Sep 2021 19:34:38 -0400 Subject: [PATCH] Create Tag tables and Add Tag endpoint --- .../2021-09-02-000740_create_tags/down.sql | 3 + .../2021-09-02-000740_create_tags/up.sql | 11 +++ src/data/mod.rs | 6 ++ src/database/mod.rs | 4 +- src/database/models.rs | 28 ++++++- src/database/schema.rs | 24 +++++- src/main.rs | 84 ++++++++++++++++++- 7 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 migrations/2021-09-02-000740_create_tags/down.sql create mode 100644 migrations/2021-09-02-000740_create_tags/up.sql diff --git a/migrations/2021-09-02-000740_create_tags/down.sql b/migrations/2021-09-02-000740_create_tags/down.sql new file mode 100644 index 0000000..e0745fc --- /dev/null +++ b/migrations/2021-09-02-000740_create_tags/down.sql @@ -0,0 +1,3 @@ +DROP TABLE tags; +DROP TABLE tagged_photo; + diff --git a/migrations/2021-09-02-000740_create_tags/up.sql b/migrations/2021-09-02-000740_create_tags/up.sql new file mode 100644 index 0000000..4cbb6ef --- /dev/null +++ b/migrations/2021-09-02-000740_create_tags/up.sql @@ -0,0 +1,11 @@ +CREATE TABLE tags ( + id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL +); + +CREATE TABLE tagged_photo ( + id INTEGER PRIMARY KEY NOT NULL, + photo_name TEXT NOT NULL, + tag_id INTEGER NOT NULL, + CONSTRAINT tagid FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE ON UPDATE CASCADE +); diff --git a/src/data/mod.rs b/src/data/mod.rs index 1dabfa8..c5bfcf1 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -133,6 +133,12 @@ impl From for MetadataResponse { } } +#[derive(Debug, Deserialize)] +pub struct AddTagRequest { + pub file_name: String, + pub tag_name: String, +} + #[cfg(test)] mod tests { use super::Claims; diff --git a/src/database/mod.rs b/src/database/mod.rs index aac67ce..328e310 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -9,7 +9,7 @@ use std::{ use crate::database::models::{Favorite, InsertFavorite, InsertUser, User}; pub mod models; -mod schema; +pub mod schema; pub trait UserDao { fn create_user(&self, user: &str, password: &str) -> Option; @@ -81,7 +81,7 @@ impl UserDao for SqliteUserDao { } } -fn connect() -> SqliteConnection { +pub fn connect() -> SqliteConnection { let db_url = dotenv::var("DATABASE_URL").expect("DATABASE_URL must be set"); SqliteConnection::establish(&db_url).expect("Error connecting to DB") } diff --git a/src/database/models.rs b/src/database/models.rs index dca23ba..fecd900 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -1,4 +1,4 @@ -use crate::database::schema::{favorites, users}; +use crate::database::schema::{favorites, tagged_photo, tags, users}; use serde::Serialize; #[derive(Insertable)] @@ -29,3 +29,29 @@ pub struct Favorite { pub userid: i32, pub path: String, } + +#[derive(Serialize, Queryable, Clone, Debug)] +pub struct Tag { + pub id: i32, + pub name: String, +} + +#[derive(Insertable, Clone, Debug)] +#[table_name = "tags"] +pub struct InsertTag { + pub name: String, +} + +#[derive(Insertable, Clone, Debug)] +#[table_name = "tagged_photo"] +pub struct InsertTaggedPhoto { + pub tag_id: i32, + pub photo_name: String, +} + +#[derive(Queryable, Clone, Debug)] +pub struct TaggedPhoto { + pub id: i32, + pub photo_name: String, + pub tag_id: i32, +} diff --git a/src/database/schema.rs b/src/database/schema.rs index e254eee..3bb2549 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -6,6 +6,21 @@ table! { } } +table! { + tagged_photo (id) { + id -> Integer, + photo_name -> Text, + tag_id -> Integer, + } +} + +table! { + tags (id) { + id -> Integer, + name -> Text, + } +} + table! { users (id) { id -> Integer, @@ -14,4 +29,11 @@ table! { } } -allow_tables_to_appear_in_same_query!(favorites, users,); +joinable!(tagged_photo -> tags (tag_id)); + +allow_tables_to_appear_in_same_query!( + favorites, + tagged_photo, + tags, + users, +); diff --git a/src/main.rs b/src/main.rs index 545e0b5..6dcf71b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,7 @@ use actix_web::{ web::{self, BufMut, BytesMut, HttpRequest, HttpResponse}, App, HttpServer, Responder, }; +use diesel::prelude::*; use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use rayon::prelude::*; @@ -34,11 +35,12 @@ use crate::auth::login; use crate::data::*; use crate::database::*; use crate::files::{is_image_or_video, is_valid_path}; +use crate::models::{InsertTag, InsertTaggedPhoto, Tag, TaggedPhoto}; use crate::video::*; mod auth; mod data; -mod database; +pub mod database; mod files; mod video; @@ -292,6 +294,85 @@ async fn delete_favorite( } } +#[post("image/tags")] +async fn add_tag(_: Claims, body: web::Json) -> impl Responder { + let tag = body.tag_name.clone(); + + use database::schema::tags; + + let connection = &connect(); + let tag_id = tags::table + .filter(tags::name.eq(&tag)) + .get_result::(connection) + .optional() + .map_or(-1, |t| { + if let Some(t) = t { + t.id + } else { + match diesel::insert_into(tags::table) + .values(InsertTag { name: tag.clone() }) + .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_results::(connection) + }) { + Err(e) => { + error!("Error inserting tag: '{}'. {:?}", tag, e); + -1 + } + Ok(id) => { + debug!("Inserted tag: '{}' with id: {:?}", tag, id); + *id.first().expect("We should have just inserted the row") + } + } + } + }); + + if tag_id == -1 { + HttpResponse::InternalServerError() + } else { + 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::(connection) + .optional() + { + Ok(Some(_)) => HttpResponse::NoContent(), + Ok(None) => diesel::insert_into(tagged_photo::table) + .values(InsertTaggedPhoto { + tag_id, + photo_name: file_name.clone(), + }) + .execute(connection) + .map(|_| { + debug!("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() + } + } + } +} + fn create_thumbnails() { let thumbs = &dotenv::var("THUMBNAILS").expect("THUMBNAILS not defined"); let thumbnail_directory: &Path = Path::new(thumbs); @@ -474,6 +555,7 @@ fn main() -> std::io::Result<()> { .service(put_add_favorite) .service(delete_favorite) .service(get_file_metadata) + .service(add_tag) .app_data(app_data.clone()) .data::>(Box::new(user_dao)) .data::>(Box::new(favorites_dao))