diff options
-rw-r--r-- | dichroism/Cargo.lock | 49 | ||||
-rw-r--r-- | dichroism/Cargo.toml | 3 | ||||
-rw-r--r-- | dichroism/src/dtos/mod.rs | 2 | ||||
-rw-r--r-- | dichroism/src/dtos/photo_set_get.rs | 22 | ||||
-rw-r--r-- | dichroism/src/dtos/product_patch.rs | 22 | ||||
-rw-r--r-- | dichroism/src/dtos/product_post.rs | 2 | ||||
-rw-r--r-- | dichroism/src/handlers.rs | 99 | ||||
-rw-r--r-- | dichroism/src/image_service.rs | 18 | ||||
-rw-r--r-- | dichroism/src/repo/mod.rs | 109 | ||||
-rw-r--r-- | dichroism/src/repo/photo_set_repo.rs | 42 | ||||
-rw-r--r-- | dichroism/src/repo/product_repo.rs | 78 |
11 files changed, 284 insertions, 162 deletions
diff --git a/dichroism/Cargo.lock b/dichroism/Cargo.lock index 9184f40..06bae04 100644 --- a/dichroism/Cargo.lock +++ b/dichroism/Cargo.lock @@ -93,6 +93,24 @@ dependencies = [ ] [[package]] +name = "actix-multipart" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774bfeb11b54bf9c857a005b8ab893293da4eaff79261a66a9200dab7f5ab6e3" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "bytes", + "derive_more", + "futures-util", + "httparse", + "log", + "mime", + "twoway", +] + +[[package]] name = "actix-router" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -720,14 +738,17 @@ dependencies = [ name = "dichroism" version = "0.1.0" dependencies = [ + "actix-multipart", "actix-web", "async-std", "base64 0.13.0", "diesel", "env_logger", + "futures", "image", "listenfd", "log", + "mime", "once_cell", "regex", "serde", @@ -877,6 +898,7 @@ checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -900,6 +922,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" [[package]] +name = "futures-executor" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] name = "futures-io" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2191,12 +2224,28 @@ dependencies = [ ] [[package]] +name = "twoway" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc" +dependencies = [ + "memchr", + "unchecked-index", +] + +[[package]] name = "typenum" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" [[package]] +name = "unchecked-index" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" + +[[package]] name = "unicode-bidi" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/dichroism/Cargo.toml b/dichroism/Cargo.toml index f699617..7dd6036 100644 --- a/dichroism/Cargo.toml +++ b/dichroism/Cargo.toml @@ -6,6 +6,9 @@ edition = "2018" [dependencies] actix-web = "3" +actix-multipart = "0.3" +mime = "0.3" +futures = "0.3" async-std = "1" base64 = "0.13" diesel = { version = "1", features = [ "sqlite", "r2d2" ] } diff --git a/dichroism/src/dtos/mod.rs b/dichroism/src/dtos/mod.rs index cc8edd1..a3e27e5 100644 --- a/dichroism/src/dtos/mod.rs +++ b/dichroism/src/dtos/mod.rs @@ -1,7 +1,9 @@ +mod photo_set_get; mod product_get; mod product_patch; mod product_post; +pub use photo_set_get::*; pub use product_get::*; pub use product_patch::*; pub use product_post::*; diff --git a/dichroism/src/dtos/photo_set_get.rs b/dichroism/src/dtos/photo_set_get.rs new file mode 100644 index 0000000..0736617 --- /dev/null +++ b/dichroism/src/dtos/photo_set_get.rs @@ -0,0 +1,22 @@ +use crate::models::PhotoSet; + +#[derive(Debug, Serialize)] +pub struct PhotoSetGet { + pub id: i32, + pub original: String, + pub fullsize: String, + pub base: String, + pub thumbnail: String, +} + +impl From<PhotoSet> for PhotoSetGet { + fn from(p: PhotoSet) -> Self { + Self { + id: p.id.unwrap_or(-1), + original: p.original.id, + fullsize: p.fullsize.id, + base: p.base.id, + thumbnail: p.thumbnail.id, + } + } +} diff --git a/dichroism/src/dtos/product_patch.rs b/dichroism/src/dtos/product_patch.rs index ed8bf1a..f231469 100644 --- a/dichroism/src/dtos/product_patch.rs +++ b/dichroism/src/dtos/product_patch.rs @@ -1,3 +1,5 @@ +use crate::models::Product; + #[derive(Debug, Deserialize)] pub struct ProductPatch { pub id: i32, @@ -7,5 +9,23 @@ pub struct ProductPatch { pub description: Option<String>, pub featured: Option<bool>, pub category_path: Option<String>, - pub photo_data: Option<String>, + pub photo_set: Option<i32>, +} + +impl ProductPatch { + pub fn patch(self, product: &mut Product) { + if let Some(name) = self.name { + product.name = name; + } + if let Some(category) = self.category_path { + product.category = category; + } + if let Some(description) = self.description { + product.description = description; + } + + product.quantity = self.quantity.unwrap_or(product.quantity); + product.cents = self.cents.unwrap_or(product.cents); + product.featured = self.featured.unwrap_or(product.featured); + } } diff --git a/dichroism/src/dtos/product_post.rs b/dichroism/src/dtos/product_post.rs index ef07536..ee3ba03 100644 --- a/dichroism/src/dtos/product_post.rs +++ b/dichroism/src/dtos/product_post.rs @@ -6,5 +6,5 @@ pub struct ProductPost { pub description: String, pub featured: bool, pub category_path: String, - pub photo_data: String, + pub photo_set: i32, } diff --git a/dichroism/src/handlers.rs b/dichroism/src/handlers.rs index 5d678f5..43c11d2 100644 --- a/dichroism/src/handlers.rs +++ b/dichroism/src/handlers.rs @@ -1,9 +1,11 @@ use crate::dtos::*; use crate::image_service; use crate::models::Product; -use crate::repo; +use crate::repo::{photo_set_repo, product_repo}; use crate::types::DbPool; +use actix_multipart::Multipart; use actix_web::{get, patch, post, web, Error, HttpResponse, Responder}; +use futures::{StreamExt, TryStreamExt}; fn to_internal_error(e: impl std::error::Error) -> HttpResponse { eprintln!("{}", e); @@ -18,7 +20,7 @@ async fn hello() -> impl Responder { #[get("/products")] async fn get_products(pool: web::Data<DbPool>) -> Result<HttpResponse, Error> { let conn = pool.get().map_err(to_internal_error)?; - let products: Vec<ProductGet> = web::block(move || repo::find_all(&conn)) + let products: Vec<ProductGet> = web::block(move || product_repo::find_all(&conn)) .await .map_err(to_internal_error)? .into_iter() @@ -30,43 +32,33 @@ async fn get_products(pool: web::Data<DbPool>) -> Result<HttpResponse, Error> { #[patch("/products")] async fn patch_product( pool: web::Data<DbPool>, - patch: web::Json<ProductPatch>, + web::Json(patch): web::Json<ProductPatch>, ) -> Result<HttpResponse, Error> { - let patch = patch.into_inner(); let id = patch.id; + // get product referenced by patch let conn = pool.get().map_err(to_internal_error)?; - let mut product = web::block(move || repo::find(&conn, id)) + let mut product = web::block(move || product_repo::find(&conn, id)) .await .map_err(to_internal_error)? .ok_or_else(|| HttpResponse::NotFound().finish())?; - if let Some(data_uri) = patch.photo_data { - // create new photo_set - let photo_set = web::block(move || image_service::generate_photo_set(&data_uri)) - .await - .map_err(|e| { - eprintln!("{}", e.to_string()); - HttpResponse::InternalServerError().body(e.to_string()) - })?; + // get photo set referenced by patch + if let Some(id) = patch.photo_set { let conn = pool.get().map_err(to_internal_error)?; - let photo_set = web::block(move || repo::store_photo_set(&conn, photo_set)) + let photo_set = web::block(move || photo_set_repo::find(&conn, id)) .await - .map_err(to_internal_error)?; + .map_err(to_internal_error)? + .ok_or_else(|| HttpResponse::NotFound().body("Photo set not found"))?; product.photo_set = photo_set; } - // patch the rest of the product - product.name = patch.name.unwrap_or(product.name); - product.quantity = patch.quantity.unwrap_or(product.quantity); - product.cents = patch.cents.unwrap_or(product.cents); - product.description = patch.description.unwrap_or(product.description); - product.featured = patch.featured.unwrap_or(product.featured); - product.category = patch.category_path.unwrap_or(product.category); + // update product fields + patch.patch(&mut product); // store the updated product let conn = pool.get().map_err(to_internal_error)?; - let product = web::block(move || repo::store_product(&conn, product)) + let product = web::block(move || product_repo::store(&conn, product)) .await .map_err(to_internal_error)?; @@ -76,24 +68,17 @@ async fn patch_product( #[post("/products")] async fn post_product( pool: web::Data<DbPool>, - post: web::Json<ProductPost>, + web::Json(post): web::Json<ProductPost>, ) -> Result<HttpResponse, Error> { - let post = post.into_inner(); - dbg!(&post); - - // create new photo_set - let data_uri = post.photo_data; - let photo_set = web::block(move || image_service::generate_photo_set(&data_uri)) - .await - .map_err(|e| { - eprintln!("{}", e.to_string()); - HttpResponse::InternalServerError().body(e.to_string()) - })?; + // find associated photo set + let photo_set = post.photo_set; let conn = pool.get().map_err(to_internal_error)?; - let photo_set = web::block(move || repo::store_photo_set(&conn, photo_set)) + let photo_set = web::block(move || photo_set_repo::find(&conn, photo_set)) .await - .map_err(to_internal_error)?; + .map_err(to_internal_error)? + .ok_or_else(|| HttpResponse::NotFound().body("Photo set not found."))?; + // create new product let product = Product { id: None, name: post.name, @@ -105,10 +90,48 @@ async fn post_product( photo_set, }; + // store product let conn = pool.get().map_err(to_internal_error)?; - let product = web::block(move || repo::store_product(&conn, product)) + let product = web::block(move || product_repo::store(&conn, product)) .await .map_err(to_internal_error)?; Ok(HttpResponse::Ok().json::<ProductGet>(product.into())) } + +#[post("/photos")] +async fn post_photo( + pool: web::Data<DbPool>, + mut payload: Multipart, +) -> Result<HttpResponse, Error> { + let mut responses: Vec<PhotoSetGet> = Vec::new(); + + if let Ok(Some(mut field)) = payload.try_next().await { + // bail if a non-JPEG file was going to be uploaded + if field.content_type() != &mime::IMAGE_JPEG { + return Ok(HttpResponse::BadRequest().body("File must be a JPEG image.")); + } + + // grab all bytes + let mut data: Vec<u8> = Vec::new(); + while let Some(chunk) = field.next().await { + let chunk = chunk?; + data.extend(chunk); + } + + // create new photo_set + let photo_set = web::block(move || image_service::generate_photo_set(&data)) + .await + .map_err(|e| { + eprintln!("{}", e.to_string()); + HttpResponse::InternalServerError().body(e.to_string()) + })?; + let conn = pool.get().map_err(to_internal_error)?; + let photo_set = web::block(move || photo_set_repo::store(&conn, photo_set)) + .await + .map_err(to_internal_error)?; + responses.push(photo_set.into()); + } + + Ok(HttpResponse::Ok().json(responses)) +} diff --git a/dichroism/src/image_service.rs b/dichroism/src/image_service.rs index 3a31e04..a69cca2 100644 --- a/dichroism/src/image_service.rs +++ b/dichroism/src/image_service.rs @@ -2,28 +2,14 @@ use crate::config::CONFIG_INSTANCE; use crate::constants::{PHOTO_BASE_XY, PHOTO_FULLSIZE_XY, PHOTO_THUMBNAIL_XY}; use crate::error::DichroismError; use crate::models::{Photo, PhotoSet}; -use base64::decode; use image::imageops::FilterType; use image::DynamicImage; use image::GenericImageView; -use once_cell::sync::Lazy; -use regex::Regex; use std::path::PathBuf; use uuid::Uuid; -static DATA_URI_RE: Lazy<Regex> = Lazy::new(|| { - Regex::new("^data:image/(png|jpeg);base64,(?P<data>.+)") - .expect("Couldn't parse data URI Regex.") -}); - -pub fn generate_photo_set(uri: &str) -> Result<PhotoSet, DichroismError> { - let data = DATA_URI_RE - .captures(uri) - .ok_or_else(|| DichroismError::UriDataExtract("Failed to parse URI".to_string()))? - .name("data") - .ok_or_else(|| DichroismError::UriDataExtract("Failed to extract data".to_string()))? - .as_str(); - let original = image::load_from_memory(&decode(data)?)?; +pub fn generate_photo_set(data: &[u8]) -> Result<PhotoSet, DichroismError> { + let original = image::load_from_memory(&data)?; let fullsize = original.resize(PHOTO_FULLSIZE_XY, PHOTO_FULLSIZE_XY, FilterType::Lanczos3); let base = original.resize(PHOTO_BASE_XY, PHOTO_BASE_XY, FilterType::Lanczos3); diff --git a/dichroism/src/repo/mod.rs b/dichroism/src/repo/mod.rs index 6dc245a..75a063c 100644 --- a/dichroism/src/repo/mod.rs +++ b/dichroism/src/repo/mod.rs @@ -1,110 +1,7 @@ -use crate::models; -use diesel::insert_into; -use diesel::prelude::*; -use diesel::result::Error; -use diesel::update; -use entities::*; +use diesel::SqliteConnection; pub mod entities; +pub mod photo_set_repo; +pub mod product_repo; type DBConn = SqliteConnection; - -pub fn store_photo_set( - conn: &DBConn, - mut photo_set: models::PhotoSet, -) -> Result<models::PhotoSet, Error> { - use crate::schema::photo_sets::dsl::*; - if photo_set.id.is_some() { - // update - let form = PhotoSetForm::from(photo_set.clone()); - update(photo_sets).set(&form).execute(conn)?; - } else { - // insert - photo_set.id = Some(find_next_photo_set_id(conn)?); - let form = PhotoSetForm::from(photo_set.clone()); - insert_into(photo_sets).values(&form).execute(conn)?; - } - Ok(photo_set) -} - -pub fn store_product( - conn: &DBConn, - mut product: models::Product, -) -> Result<models::Product, Error> { - use crate::schema::products::dsl::*; - if product.id.is_some() { - // update - let form = ProductForm::from(product.clone()); - update(products).set(&form).execute(conn)?; - } else { - // insert - product.id = Some(find_next_product_id(conn)?); - let form = ProductForm::from(product.clone()); - insert_into(products).values(&form).execute(conn)?; - } - - Ok(product) -} - -pub fn find_all(conn: &DBConn) -> Result<Vec<models::Product>, Error> { - use crate::schema::*; - let query = products::table.inner_join(photo_sets::table).select(( - products::id, - products::name, - products::description, - products::quantity, - products::cents, - products::featured, - photo_sets::id, - photo_sets::original, - photo_sets::fullsize, - photo_sets::base, - photo_sets::thumbnail, - )); - Ok(query - .load::<entities::Product>(conn)? - .into_iter() - .map(|p| p.into()) - .collect::<Vec<models::Product>>()) -} - -pub fn find(conn: &DBConn, id: i32) -> Result<Option<models::Product>, Error> { - use crate::schema::*; - let query = products::table - .inner_join(photo_sets::table) - .filter(products::id.eq(id)) - .select(( - products::id, - products::name, - products::description, - products::quantity, - products::cents, - products::featured, - photo_sets::id, - photo_sets::original, - photo_sets::fullsize, - photo_sets::base, - photo_sets::thumbnail, - )); - let product = query.first::<Product>(conn).map(|p| p.into()); - match product { - Ok(p) => Ok(Some(p)), - Err(e) => { - if e == Error::NotFound { - Ok(None) - } else { - Err(e) - } - } - } -} - -fn find_next_product_id(conn: &DBConn) -> Result<i32, Error> { - use crate::schema::products::dsl::*; - Ok(products.select(id).order(id.desc()).first::<i32>(conn)? + 1) -} - -fn find_next_photo_set_id(conn: &DBConn) -> Result<i32, Error> { - use crate::schema::photo_sets::dsl::*; - Ok(photo_sets.select(id).order(id.desc()).first::<i32>(conn)? + 1) -} diff --git a/dichroism/src/repo/photo_set_repo.rs b/dichroism/src/repo/photo_set_repo.rs new file mode 100644 index 0000000..339ea91 --- /dev/null +++ b/dichroism/src/repo/photo_set_repo.rs @@ -0,0 +1,42 @@ +use super::entities::*; +use super::DBConn; +use crate::models; +use diesel::{insert_into, prelude::*, result::Error, update}; + +pub fn store(conn: &DBConn, mut photo_set: models::PhotoSet) -> Result<models::PhotoSet, Error> { + use crate::schema::photo_sets::dsl::*; + if photo_set.id.is_some() { + // update + let form = PhotoSetForm::from(photo_set.clone()); + update(photo_sets).set(&form).execute(conn)?; + } else { + // insert + photo_set.id = Some(find_next_id(conn)?); + let form = PhotoSetForm::from(photo_set.clone()); + insert_into(photo_sets).values(&form).execute(conn)?; + } + Ok(photo_set) +} + +pub fn find(conn: &DBConn, dbid: i32) -> Result<Option<models::PhotoSet>, Error> { + use crate::schema::photo_sets::dsl::*; + let query = photo_sets + .filter(id.eq(dbid)) + .select((id, original, fullsize, base, thumbnail)); + let photo_set = query.first::<PhotoSet>(conn).map(|p| p.into()); + match photo_set { + Ok(p) => Ok(Some(p)), + Err(e) => { + if e == Error::NotFound { + Ok(None) + } else { + Err(e) + } + } + } +} + +fn find_next_id(conn: &DBConn) -> Result<i32, Error> { + use crate::schema::photo_sets::dsl::*; + Ok(photo_sets.select(id).order(id.desc()).first::<i32>(conn)? + 1) +} diff --git a/dichroism/src/repo/product_repo.rs b/dichroism/src/repo/product_repo.rs new file mode 100644 index 0000000..7b3aaac --- /dev/null +++ b/dichroism/src/repo/product_repo.rs @@ -0,0 +1,78 @@ +use super::entities::*; +use super::DBConn; +use crate::models; +use diesel::{insert_into, prelude::*, result::Error, update}; + +pub fn store(conn: &DBConn, mut product: models::Product) -> Result<models::Product, Error> { + use crate::schema::products::dsl::*; + if product.id.is_some() { + // update + let form = ProductForm::from(product.clone()); + update(products).set(&form).execute(conn)?; + } else { + // insert + product.id = Some(find_next_id(conn)?); + let form = ProductForm::from(product.clone()); + insert_into(products).values(&form).execute(conn)?; + } + + Ok(product) +} + +pub fn find_all(conn: &DBConn) -> Result<Vec<models::Product>, Error> { + use crate::schema::*; + let query = products::table.inner_join(photo_sets::table).select(( + products::id, + products::name, + products::description, + products::quantity, + products::cents, + products::featured, + photo_sets::id, + photo_sets::original, + photo_sets::fullsize, + photo_sets::base, + photo_sets::thumbnail, + )); + Ok(query + .load::<Product>(conn)? + .into_iter() + .map(|p| p.into()) + .collect::<Vec<models::Product>>()) +} + +pub fn find(conn: &DBConn, dbid: i32) -> Result<Option<models::Product>, Error> { + use crate::schema::*; + let query = products::table + .inner_join(photo_sets::table) + .filter(products::id.eq(dbid)) + .select(( + products::id, + products::name, + products::description, + products::quantity, + products::cents, + products::featured, + photo_sets::id, + photo_sets::original, + photo_sets::fullsize, + photo_sets::base, + photo_sets::thumbnail, + )); + let product = query.first::<Product>(conn).map(|p| p.into()); + match product { + Ok(p) => Ok(Some(p)), + Err(e) => { + if e == Error::NotFound { + Ok(None) + } else { + Err(e) + } + } + } +} + +fn find_next_id(conn: &DBConn) -> Result<i32, Error> { + use crate::schema::products::dsl::*; + Ok(products.select(id).order(id.desc()).first::<i32>(conn)? + 1) +} |