diff --git a/migrations/2020-06-15-200823_images/up.sql b/migrations/2020-06-15-200823_images/up.sql index 797b144..2a7c1b4 100644 --- a/migrations/2020-06-15-200823_images/up.sql +++ b/migrations/2020-06-15-200823_images/up.sql @@ -2,11 +2,14 @@ CREATE TABLE images ( id VARCHAR(40) PRIMARY KEY NOT NULL, path VARCHAR(500) NOT NULL, title VARCHAR(200), + image_type INTEGER NOT NULL, last_changed TIMESTAMP NOT NULL, published BOOLEAN NOT NULL DEFAULT 'f' ); -INSERT INTO images (id, path, title, last_changed, published) VALUES -('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 -('ea01f5cd-a532-4232-810c-bae977cc4336', './Canon_40D.jpg', 'Canon 40d', '2020-05-27 13:33:56.747473', false ); \ No newline at end of file +CREATE INDEX image_path_idx ON images (path); + +INSERT INTO images (id, path, title, image_type, last_changed, published) VALUES +('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 ); \ No newline at end of file diff --git a/migrations/2020-07-01-172642_tags/down.sql b/migrations/2020-07-01-172642_tags/down.sql new file mode 100644 index 0000000..b59c931 --- /dev/null +++ b/migrations/2020-07-01-172642_tags/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS tags; \ No newline at end of file diff --git a/migrations/2020-07-01-172642_tags/up.sql b/migrations/2020-07-01-172642_tags/up.sql new file mode 100644 index 0000000..fa847b0 --- /dev/null +++ b/migrations/2020-07-01-172642_tags/up.sql @@ -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) +); diff --git a/src/bin/listdb.rs b/src/bin/listdb.rs index cbbf706..2f04cdf 100644 --- a/src/bin/listdb.rs +++ b/src/bin/listdb.rs @@ -1,18 +1,33 @@ extern crate curators; -use anyhow::{Context, Result}; use curators::database::*; -use curators::models::Image; +use curators::models::{Image, Tag}; use std::path::Path; 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()); for img in results { 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)); } diff --git a/src/main.rs b/src/bin/main.rs similarity index 90% rename from src/main.rs rename to src/bin/main.rs index 1114aa6..8e9a859 100644 --- a/src/main.rs +++ b/src/bin/main.rs @@ -5,7 +5,12 @@ use strum_macros::EnumString; #[derive(EnumString, Debug)] enum Cmd { - #[strum(serialize = "read", serialize = "r")] + #[strum( + serialize = "read", + serialize = "r", + serialize = "get", + serialize = "g" + )] Read, #[strum( serialize = "write", diff --git a/src/database.rs b/src/database.rs index b802fb1..6fa750e 100644 --- a/src/database.rs +++ b/src/database.rs @@ -2,25 +2,51 @@ use diesel; use diesel::prelude::*; use diesel::sqlite::SqliteConnection; -use crate::models::Image; -use dotenv::dotenv; use std::env; -pub fn establish_connection() -> SqliteConnection { - dotenv().ok(); +use crate::models::{Image, Tag}; +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"); SqliteConnection::establish(&database_url) .expect(&format!("Error connecting to {}", database_url)) } -pub fn load_images() -> Vec { - use crate::schema::images::dsl::*; - - let connection = establish_connection(); +pub fn load_images(conn: &Conn, start: i64, limit: i64) -> Vec { images .filter(published.eq(false)) - .limit(50) - .load::(&connection) + .offset(start) + .limit(limit) + .load::(conn) .expect("Error loading images") } + +pub fn load_tags(conn: &Conn, uuid: T) -> Vec +where + T: Into, +{ + tags.filter(image_id.eq(uuid.into())) + .load::(conn) + .expect("Error loading tags.") +} + +pub fn save_image(conn: &Conn, img: &Image) -> QueryResult { + diesel::insert_into(images).values(img).execute(conn) +} + +pub fn save_tags(conn: &Conn, tag: &Tag) -> QueryResult { + diesel::insert_into(tags).values(tag).execute(conn) +} + +pub fn delete_image(conn: &Conn, uuid: T) -> QueryResult +where + T: Into, +{ + 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) +} diff --git a/src/models.rs b/src/models.rs index 61edb0a..bf5bd35 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,26 +1,42 @@ -use crate::schema::images; - +use crate::schema::{images, tags}; use anyhow::{Context, Result}; use chrono::NaiveDateTime; -use std::ffi::OsString; -use std::{ - fs, - path::{Path, PathBuf}, - time::{SystemTime, UNIX_EPOCH}, +use diesel::{ + deserialize::FromSqlRow, + expression::{helper_types::AsExprOf, AsExpression}, + row::Row, + 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; -#[derive(Debug, Clone, Queryable, Insertable, Identifiable)] +#[derive(Debug, Clone, Queryable, Identifiable, Insertable)] pub struct Image { pub id: String, pub path: String, pub title: Option, + pub image_type: ImageType, pub last_changed: NaiveDateTime, pub published: bool, } impl Image { - pub fn try_from(path: &Path) -> anyhow::Result { + pub fn create_uuid(path: &Path) -> anyhow::Result { + 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 { 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!( @@ -39,10 +55,12 @@ impl Image { ))?, }; let mtime = time.duration_since(UNIX_EPOCH).unwrap(); + let image_type = ImageType::from_path(path); Ok(Image { id: uuid.to_string(), path: path_str.to_string(), title: None, + image_type, last_changed: NaiveDateTime::from_timestamp( mtime.as_secs() as i64, 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 { + 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 { + 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 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 for i32 { + fn from(image_type: ImageType) -> Self { + i32::from(&image_type) + } +} + +// traits for diesel + +impl diesel::Queryable for ImageType { + type Row = i32; + fn build(row: Self::Row) -> Self { + row.into() + } +} + +impl FromSqlRow for ImageType { + fn build_from_row>(row: &mut R) -> Result> { + Ok(i32::build_from_row(row)?.into()) + } +} + +impl AsExpression for ImageType { + type Expression = AsExprOf; + fn as_expression(self) -> Self::Expression { + >::as_expression(self as i32) + } +} + +impl<'a> AsExpression for &'a ImageType { + type Expression = AsExprOf; + fn as_expression(self) -> Self::Expression { + >::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 for TagType { + type Row = String; + fn build(row: Self::Row) -> Self { + TagType::try_from(row.as_str()).unwrap() + } +} + +impl AsExpression for TagType { + type Expression = AsExprOf; + fn as_expression(self) -> Self::Expression { + >::as_expression(self.to_string()) + } +} + +impl<'a> AsExpression for &'a TagType { + type Expression = AsExprOf; + fn as_expression(self) -> Self::Expression { + >::as_expression(self.to_string()) + } +} diff --git a/src/schema.rs b/src/schema.rs index 5150dee..40cd4fd 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -3,7 +3,23 @@ table! { id -> Text, path -> Text, title -> Nullable, + image_type -> Integer, last_changed -> Timestamp, 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, +);