Db-Relation Tag added.
				
					
				
			This commit is contained in:
		| @@ -2,11 +2,14 @@ CREATE TABLE images ( | |||||||
|   id VARCHAR(40) PRIMARY KEY NOT NULL, |   id VARCHAR(40) PRIMARY KEY NOT NULL, | ||||||
|   path VARCHAR(500) NOT NULL, |   path VARCHAR(500) NOT NULL, | ||||||
|   title VARCHAR(200), |   title VARCHAR(200), | ||||||
|  |   image_type INTEGER NOT NULL, | ||||||
|   last_changed TIMESTAMP NOT NULL, |   last_changed TIMESTAMP NOT NULL, | ||||||
|   published BOOLEAN NOT NULL DEFAULT 'f' |   published BOOLEAN NOT NULL DEFAULT 'f' | ||||||
| ); | ); | ||||||
|  |  | ||||||
| INSERT INTO images (id, path, title, last_changed, published) VALUES | CREATE INDEX image_path_idx ON images (path); | ||||||
| ('8e153cab-7dc9-46c7-9a72-91e56ac174ca', './BlueSquare.jpg', 'Blaues Quadrat', '2020-05-27 13:33:56.747473', false ); |  | ||||||
| INSERT INTO images (id, path, title, last_changed, published) VALUES | INSERT INTO images (id, path, title, image_type, last_changed, published) VALUES | ||||||
| ('ea01f5cd-a532-4232-810c-bae977cc4336', './Canon_40D.jpg', 'Canon 40d', '2020-05-27 13:33:56.747473', false ); | ('8e153cab-7dc9-46c7-9a72-91e56ac174ca', './BlueSquare.jpg', 'Blaues Quadrat', 1, '2020-05-27 13:33:56.747473', false ); | ||||||
|  | INSERT INTO images (id, path, title, image_type, last_changed, published) VALUES | ||||||
|  | ('ea01f5cd-a532-4232-810c-bae977cc4336', './Canon_40D.jpg', 'Canon 40d', 1, '2020-05-27 13:33:56.747473', false ); | ||||||
							
								
								
									
										2
									
								
								migrations/2020-07-01-172642_tags/down.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								migrations/2020-07-01-172642_tags/down.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | -- This file should undo anything in `up.sql` | ||||||
|  | DROP TABLE IF EXISTS tags; | ||||||
							
								
								
									
										8
									
								
								migrations/2020-07-01-172642_tags/up.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								migrations/2020-07-01-172642_tags/up.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | -- Your SQL goes here | ||||||
|  | CREATE TABLE tags | ||||||
|  | ( | ||||||
|  |     image_id VARCHAR(40)  NOT NULL REFERENCES images (id), | ||||||
|  |     tag_type  VARCHAR(20)  NOT NULL, | ||||||
|  |     name     VARCHAR(500) NOT NULL, | ||||||
|  |     PRIMARY KEY (image_id, name) | ||||||
|  | ); | ||||||
| @@ -1,18 +1,33 @@ | |||||||
| extern crate curators; | extern crate curators; | ||||||
|  |  | ||||||
| use anyhow::{Context, Result}; |  | ||||||
| use curators::database::*; | use curators::database::*; | ||||||
| use curators::models::Image; | use curators::models::{Image, Tag}; | ||||||
| use std::path::Path; | use std::path::Path; | ||||||
|  |  | ||||||
| fn main() { | fn main() { | ||||||
|     let results = load_images(); |     dotenv::dotenv().ok(); | ||||||
|  |     let conn = establish_connection(); | ||||||
|  |     let results = load_images(&conn, 0, 20); | ||||||
|     println!("Displaying {} images", results.len()); |     println!("Displaying {} images", results.len()); | ||||||
|     for img in results { |     for img in results { | ||||||
|         println!("{:?} ", img); |         println!("{:?} ", img); | ||||||
|  |         delete_image(&conn, img.id).ok(); | ||||||
|  |     } | ||||||
|  |     let mut images = Vec::new(); | ||||||
|  |  | ||||||
|  |     let p = Path::new(r"./BlueSquare.jpg"); | ||||||
|  |     images.push(Image::try_from_path(&p).unwrap()); | ||||||
|  |     let mut tags = Tag::create_tags(&p); | ||||||
|  |     let p = Path::new(r"./Canon_40D.jpg"); | ||||||
|  |  | ||||||
|  |     images.push(Image::try_from_path(&p).unwrap()); | ||||||
|  |     tags.append(&mut Tag::create_tags(&p)); | ||||||
|  |  | ||||||
|  |     for i in images { | ||||||
|  |         println!("{:?}", &i); | ||||||
|  |         save_image(&conn, &i).ok(); | ||||||
|  |     } | ||||||
|  |     for t in tags { | ||||||
|  |         println!("{:?} = {:?}", &t, save_tags(&conn, &t)); | ||||||
|     } |     } | ||||||
|     let p = Path::new(r"./BlueSquare.jpg").canonicalize().unwrap(); |  | ||||||
|     println!("{:?}", Image::try_from(&p)); |  | ||||||
|     let p = Path::new(r"./Canon_40D.jpg").canonicalize().unwrap(); |  | ||||||
|     println!("{:?}", Image::try_from(&p)); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,7 +5,12 @@ use strum_macros::EnumString; | |||||||
| 
 | 
 | ||||||
| #[derive(EnumString, Debug)] | #[derive(EnumString, Debug)] | ||||||
| enum Cmd { | enum Cmd { | ||||||
|     #[strum(serialize = "read", serialize = "r")] |     #[strum(
 | ||||||
|  |         serialize = "read", | ||||||
|  |         serialize = "r", | ||||||
|  |         serialize = "get", | ||||||
|  |         serialize = "g" | ||||||
|  |     )] | ||||||
|     Read, |     Read, | ||||||
|     #[strum(
 |     #[strum(
 | ||||||
|         serialize = "write", |         serialize = "write", | ||||||
| @@ -2,25 +2,51 @@ use diesel; | |||||||
| use diesel::prelude::*; | use diesel::prelude::*; | ||||||
| use diesel::sqlite::SqliteConnection; | use diesel::sqlite::SqliteConnection; | ||||||
|  |  | ||||||
| use crate::models::Image; |  | ||||||
| use dotenv::dotenv; |  | ||||||
| use std::env; | use std::env; | ||||||
|  |  | ||||||
| pub fn establish_connection() -> SqliteConnection { | use crate::models::{Image, Tag}; | ||||||
|     dotenv().ok(); | use crate::schema::images::dsl::*; | ||||||
|  | use crate::schema::tags::dsl::*; | ||||||
|  |  | ||||||
|  | type Conn = SqliteConnection; | ||||||
|  |  | ||||||
|  | pub fn establish_connection() -> Conn { | ||||||
|     let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); |     let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); | ||||||
|     SqliteConnection::establish(&database_url) |     SqliteConnection::establish(&database_url) | ||||||
|         .expect(&format!("Error connecting to {}", database_url)) |         .expect(&format!("Error connecting to {}", database_url)) | ||||||
| } | } | ||||||
|  |  | ||||||
| pub fn load_images() -> Vec<Image> { | pub fn load_images(conn: &Conn, start: i64, limit: i64) -> Vec<Image> { | ||||||
|     use crate::schema::images::dsl::*; |  | ||||||
|  |  | ||||||
|     let connection = establish_connection(); |  | ||||||
|     images |     images | ||||||
|         .filter(published.eq(false)) |         .filter(published.eq(false)) | ||||||
|         .limit(50) |         .offset(start) | ||||||
|         .load::<Image>(&connection) |         .limit(limit) | ||||||
|  |         .load::<Image>(conn) | ||||||
|         .expect("Error loading images") |         .expect("Error loading images") | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub fn load_tags<T>(conn: &Conn, uuid: T) -> Vec<Tag> | ||||||
|  | where | ||||||
|  |     T: Into<String>, | ||||||
|  | { | ||||||
|  |     tags.filter(image_id.eq(uuid.into())) | ||||||
|  |         .load::<Tag>(conn) | ||||||
|  |         .expect("Error loading tags.") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn save_image(conn: &Conn, img: &Image) -> QueryResult<usize> { | ||||||
|  |     diesel::insert_into(images).values(img).execute(conn) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn save_tags(conn: &Conn, tag: &Tag) -> QueryResult<usize> { | ||||||
|  |     diesel::insert_into(tags).values(tag).execute(conn) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn delete_image<T>(conn: &Conn, uuid: T) -> QueryResult<usize> | ||||||
|  | where | ||||||
|  |     T: Into<String>, | ||||||
|  | { | ||||||
|  |     let uuid_str = uuid.into(); | ||||||
|  |     diesel::delete(tags.filter(image_id.eq(&uuid_str))).execute(conn)?; | ||||||
|  |     diesel::delete(images.filter(id.eq(&uuid_str))).execute(conn) | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										214
									
								
								src/models.rs
									
									
									
									
									
								
							
							
						
						
									
										214
									
								
								src/models.rs
									
									
									
									
									
								
							| @@ -1,26 +1,42 @@ | |||||||
| use crate::schema::images; | use crate::schema::{images, tags}; | ||||||
|  |  | ||||||
| use anyhow::{Context, Result}; | use anyhow::{Context, Result}; | ||||||
| use chrono::NaiveDateTime; | use chrono::NaiveDateTime; | ||||||
| use std::ffi::OsString; | use diesel::{ | ||||||
| use std::{ |     deserialize::FromSqlRow, | ||||||
|     fs, |     expression::{helper_types::AsExprOf, AsExpression}, | ||||||
|     path::{Path, PathBuf}, |     row::Row, | ||||||
|     time::{SystemTime, UNIX_EPOCH}, |     sql_types::{Integer, Text}, | ||||||
|  |     sqlite::Sqlite, | ||||||
|  |     Queryable, | ||||||
| }; | }; | ||||||
|  | use std::convert::TryFrom; | ||||||
|  | use std::error::Error; | ||||||
|  | use std::{path::Path, time::UNIX_EPOCH}; | ||||||
|  | use strum_macros::{Display, EnumString}; | ||||||
| use uuid::Uuid; | use uuid::Uuid; | ||||||
|  |  | ||||||
| #[derive(Debug, Clone, Queryable, Insertable, Identifiable)] | #[derive(Debug, Clone, Queryable, Identifiable, Insertable)] | ||||||
| pub struct Image { | pub struct Image { | ||||||
|     pub id: String, |     pub id: String, | ||||||
|     pub path: String, |     pub path: String, | ||||||
|     pub title: Option<String>, |     pub title: Option<String>, | ||||||
|  |     pub image_type: ImageType, | ||||||
|     pub last_changed: NaiveDateTime, |     pub last_changed: NaiveDateTime, | ||||||
|     pub published: bool, |     pub published: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl Image { | impl Image { | ||||||
|     pub fn try_from(path: &Path) -> anyhow::Result<Self> { |     pub fn create_uuid(path: &Path) -> anyhow::Result<Uuid> { | ||||||
|  |         let abs_path = path.canonicalize().context("Path not valid!")?; | ||||||
|  |         let os_path = abs_path.clone().into_os_string(); | ||||||
|  |         let path_str = os_path.into_string().expect(&format!( | ||||||
|  |             "Converting path-name problem! {:?}", | ||||||
|  |             path.display() | ||||||
|  |         )); | ||||||
|  |         Ok(Uuid::new_v5(&Uuid::NAMESPACE_URL, &path_str.as_bytes())) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn try_from_path(path: &Path) -> anyhow::Result<Self> { | ||||||
|         let abs_path = path.canonicalize().context("Path not valid!")?; |         let abs_path = path.canonicalize().context("Path not valid!")?; | ||||||
|         let os_path = abs_path.clone().into_os_string(); |         let os_path = abs_path.clone().into_os_string(); | ||||||
|         let path_str = os_path.into_string().expect(&format!( |         let path_str = os_path.into_string().expect(&format!( | ||||||
| @@ -39,10 +55,12 @@ impl Image { | |||||||
|             ))?, |             ))?, | ||||||
|         }; |         }; | ||||||
|         let mtime = time.duration_since(UNIX_EPOCH).unwrap(); |         let mtime = time.duration_since(UNIX_EPOCH).unwrap(); | ||||||
|  |         let image_type = ImageType::from_path(path); | ||||||
|         Ok(Image { |         Ok(Image { | ||||||
|             id: uuid.to_string(), |             id: uuid.to_string(), | ||||||
|             path: path_str.to_string(), |             path: path_str.to_string(), | ||||||
|             title: None, |             title: None, | ||||||
|  |             image_type, | ||||||
|             last_changed: NaiveDateTime::from_timestamp( |             last_changed: NaiveDateTime::from_timestamp( | ||||||
|                 mtime.as_secs() as i64, |                 mtime.as_secs() as i64, | ||||||
|                 mtime.subsec_nanos() as u32, |                 mtime.subsec_nanos() as u32, | ||||||
| @@ -51,3 +69,181 @@ impl Image { | |||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Clone, Queryable, Identifiable, Insertable)] | ||||||
|  | #[primary_key(image_id, name)] | ||||||
|  | pub struct Tag { | ||||||
|  |     pub image_id: String, | ||||||
|  |     pub tag_type: TagType, | ||||||
|  |     pub name: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Tag { | ||||||
|  |     pub fn create_tags(rel_path: &Path) -> Vec<Self> { | ||||||
|  |         match Image::create_uuid(rel_path) { | ||||||
|  |             Ok(uuid) => { | ||||||
|  |                 let image_id = uuid.to_string(); | ||||||
|  |                 rel_path | ||||||
|  |                     .ancestors() | ||||||
|  |                     .filter(|&p| p.display().to_string().len() > 0) | ||||||
|  |                     .map(|p: &Path| Self { | ||||||
|  |                         image_id: image_id.clone(), | ||||||
|  |                         tag_type: TagType::Path, | ||||||
|  |                         name: p.display().to_string(), | ||||||
|  |                     }) | ||||||
|  |                     .collect() | ||||||
|  |             } | ||||||
|  |             Err(_) => vec![], | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl TryFrom<&str> for TagType { | ||||||
|  |     type Error = String; | ||||||
|  |  | ||||||
|  |     fn try_from(value: &str) -> Result<Self, Self::Error> { | ||||||
|  |         match value { | ||||||
|  |             "Location" => Ok(TagType::Location), | ||||||
|  |             "Path" => Ok(TagType::Path), | ||||||
|  |             "Face" => Ok(TagType::Face), | ||||||
|  |             x => Err(format!("unknown TagType '{}'", x)), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Clone, Copy, Debug, Display, Eq, PartialEq, SqlType, AsExpression, FromSqlRow)] | ||||||
|  | pub enum ImageType { | ||||||
|  |     UNKNOWN, | ||||||
|  |     JPEG, | ||||||
|  |     PNG, | ||||||
|  |     GIF, | ||||||
|  |     TIFF, | ||||||
|  |     RAW, | ||||||
|  |     HEIF, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ImageType { | ||||||
|  |     pub fn from_path(path: &Path) -> Self { | ||||||
|  |         let ext = path.extension(); | ||||||
|  |         if ext == None { | ||||||
|  |             return ImageType::UNKNOWN; | ||||||
|  |         } | ||||||
|  |         match path.extension().unwrap().to_str() { | ||||||
|  |             Some("heif") => ImageType::HEIF, | ||||||
|  |             Some("hevc") => ImageType::HEIF, | ||||||
|  |             Some("jpeg") => ImageType::JPEG, | ||||||
|  |             Some("jpg") => ImageType::JPEG, | ||||||
|  |             Some("png") => ImageType::PNG, | ||||||
|  |             Some("raw") => ImageType::RAW, | ||||||
|  |             Some("tif") => ImageType::TIFF, | ||||||
|  |             Some("tiff") => ImageType::TIFF, | ||||||
|  |             _ => ImageType::UNKNOWN, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn to_mimetype(&self) -> &str { | ||||||
|  |         match self { | ||||||
|  |             Self::HEIF => "image/heif", | ||||||
|  |             Self::GIF => "image/gif", | ||||||
|  |             Self::JPEG => "image/jpeg", | ||||||
|  |             Self::PNG => "image/png", | ||||||
|  |             Self::TIFF => "image/tiff", | ||||||
|  |             _ => "application/octet-stream", | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<i32> for ImageType { | ||||||
|  |     fn from(int: i32) -> Self { | ||||||
|  |         match int { | ||||||
|  |             1 => ImageType::JPEG, | ||||||
|  |             2 => ImageType::PNG, | ||||||
|  |             3 => ImageType::GIF, | ||||||
|  |             4 => ImageType::TIFF, | ||||||
|  |             5 => ImageType::RAW, | ||||||
|  |             6 => ImageType::HEIF, | ||||||
|  |             _ => ImageType::UNKNOWN, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<&ImageType> for i32 { | ||||||
|  |     fn from(image_type: &ImageType) -> Self { | ||||||
|  |         match image_type { | ||||||
|  |             ImageType::UNKNOWN => 0, | ||||||
|  |             ImageType::JPEG => 1, | ||||||
|  |             ImageType::PNG => 2, | ||||||
|  |             ImageType::GIF => 3, | ||||||
|  |             ImageType::TIFF => 4, | ||||||
|  |             ImageType::RAW => 5, | ||||||
|  |             ImageType::HEIF => 6, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl From<ImageType> for i32 { | ||||||
|  |     fn from(image_type: ImageType) -> Self { | ||||||
|  |         i32::from(&image_type) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // traits for diesel | ||||||
|  |  | ||||||
|  | impl diesel::Queryable<Integer, Sqlite> for ImageType { | ||||||
|  |     type Row = i32; | ||||||
|  |     fn build(row: Self::Row) -> Self { | ||||||
|  |         row.into() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl FromSqlRow<Integer, Sqlite> for ImageType { | ||||||
|  |     fn build_from_row<R: Row<Sqlite>>(row: &mut R) -> Result<Self, Box<dyn Error + Send + Sync>> { | ||||||
|  |         Ok(i32::build_from_row(row)?.into()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl AsExpression<Integer> for ImageType { | ||||||
|  |     type Expression = AsExprOf<i32, Integer>; | ||||||
|  |     fn as_expression(self) -> Self::Expression { | ||||||
|  |         <i32 as AsExpression<Integer>>::as_expression(self as i32) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<'a> AsExpression<Integer> for &'a ImageType { | ||||||
|  |     type Expression = AsExprOf<i32, Integer>; | ||||||
|  |     fn as_expression(self) -> Self::Expression { | ||||||
|  |         <i32 as AsExpression<Integer>>::as_expression(self.into()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive( | ||||||
|  |     Clone, Copy, Debug, Display, Eq, EnumString, PartialEq, SqlType, AsExpression, FromSqlRow, | ||||||
|  | )] | ||||||
|  | #[sqlite_type = "Text"] | ||||||
|  | pub enum TagType { | ||||||
|  |     Path, | ||||||
|  |     Face, | ||||||
|  |     Location, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // traits to help diesel. | ||||||
|  | impl diesel::Queryable<Text, Sqlite> for TagType { | ||||||
|  |     type Row = String; | ||||||
|  |     fn build(row: Self::Row) -> Self { | ||||||
|  |         TagType::try_from(row.as_str()).unwrap() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl AsExpression<Text> for TagType { | ||||||
|  |     type Expression = AsExprOf<String, Text>; | ||||||
|  |     fn as_expression(self) -> Self::Expression { | ||||||
|  |         <String as AsExpression<Text>>::as_expression(self.to_string()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl<'a> AsExpression<Text> for &'a TagType { | ||||||
|  |     type Expression = AsExprOf<String, Text>; | ||||||
|  |     fn as_expression(self) -> Self::Expression { | ||||||
|  |         <String as AsExpression<Text>>::as_expression(self.to_string()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -3,7 +3,23 @@ table! { | |||||||
|         id -> Text, |         id -> Text, | ||||||
|         path -> Text, |         path -> Text, | ||||||
|         title -> Nullable<Text>, |         title -> Nullable<Text>, | ||||||
|  |         image_type -> Integer, | ||||||
|         last_changed -> Timestamp, |         last_changed -> Timestamp, | ||||||
|         published -> Bool, |         published -> Bool, | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | table! { | ||||||
|  |     tags (image_id, name) { | ||||||
|  |         image_id -> Text, | ||||||
|  |         tag_type -> Text, | ||||||
|  |         name -> Text, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | joinable!(tags -> images (image_id)); | ||||||
|  |  | ||||||
|  | allow_tables_to_appear_in_same_query!( | ||||||
|  |     images, | ||||||
|  |     tags, | ||||||
|  | ); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user