diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..df13059 --- /dev/null +++ b/.env.sample @@ -0,0 +1,2 @@ +PORT=3021 +RUST_LOG=info \ No newline at end of file diff --git a/.gitignore b/.gitignore index ea8c4bf..0b745e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 2d24243..192991d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.70" @@ -122,6 +131,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cexpr" version = "0.6.0" @@ -148,6 +163,24 @@ dependencies = [ "libloading", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dotenvy_macro" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424" +dependencies = [ + "dotenvy", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "either" version = "1.8.1" @@ -163,6 +196,40 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fnv" version = "1.0.7" @@ -232,6 +299,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "http" version = "0.2.9" @@ -266,6 +339,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.25" @@ -289,6 +368,29 @@ dependencies = [ "want", ] +[[package]] +name = "io-lifetimes" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys", +] + [[package]] name = "itoa" version = "1.0.6" @@ -323,13 +425,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" + [[package]] name = "liquid-rescale-api" version = "0.1.0" dependencies = [ "anyhow", "axum", + "dotenvy", + "dotenvy_macro", + "env_logger", + "log", "magick_rust", + "serde", "tokio", ] @@ -423,7 +536,7 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] @@ -507,6 +620,8 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] @@ -522,6 +637,20 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "0.37.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d097081ed288dfe45699b72f5b5d648e5f15d64d900c7080273baa20c16a6849" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.12" @@ -539,6 +668,20 @@ name = "serde" version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.13", +] [[package]] name = "serde_json" @@ -622,6 +765,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + [[package]] name = "tokio" version = "1.27.0" @@ -759,6 +911,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index e033a59..35cda95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,10 @@ version = "0.1.0" [dependencies] anyhow = "1.0.70" axum = {version = "0.6", features = ["multipart", "macros"]} +dotenvy = "0.15.7" +dotenvy_macro = "0.15.7" +env_logger = "0.10.0" +log = "0.4.17" magick_rust = "0.17.0" +serde = { version = "1.0.159", features = ["derive"] } tokio = {version = "1", features = ["macros", "rt-multi-thread"]} diff --git a/src/lib.rs b/src/lib.rs index 29d3678..94347ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,19 @@ -use anyhow::Result; -use magick_rust::{magick_wand_genesis, MagickWand}; -use std::sync::Once; +mod processors; +mod router; +mod routes; +mod utilities; -static START: Once = Once::new(); +use dotenvy_macro::dotenv; +use router::create_router; +use std::net::SocketAddr; -pub fn liquid_rescale_image(image: &Vec) -> Result> { - START.call_once(|| { - magick_wand_genesis(); - }); +pub async fn run() { + let port = dotenv!("PORT"); + let app = create_router(); + let address = SocketAddr::from(([0, 0, 0, 0], port.parse().unwrap())); - let wand = MagickWand::new(); - wand.read_image_blob(&image)?; - wand.fit(640, 640); - wand.liquid_rescale_image(320, 320, 1.0, 0.0)?; - wand.fit(640, 640); - let bytes = wand.write_image_blob("JPEG")?; - Ok(bytes) + axum::Server::bind(&address) + .serve(app.into_make_service()) + .await + .unwrap(); } diff --git a/src/main.rs b/src/main.rs index 011269f..fa91e62 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,44 +1,11 @@ -use anyhow::Result; -use axum::body::Full; -use axum::extract::DefaultBodyLimit; -use axum::http::Response; -use axum::response::IntoResponse; -use axum::{extract::Multipart, routing::post, Router}; - -use liquid_rescale_api::liquid_rescale_image; +use env_logger::Env; +use liquid_rescale_api::run; +use log::info; #[tokio::main] async fn main() { - let app = Router::new() - .route("/liquid", post(liquid_rescale_route)) - .layer(DefaultBodyLimit::max(1024 * 10_000 /* 10 MiB */)); - - axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) - .serve(app.into_make_service()) - .await - .unwrap(); -} - -async fn liquid_rescale_route(multipart: Multipart) -> impl IntoResponse { - let image = parse_image(multipart).await.unwrap(); - let image_data = liquid_rescale_image(&image).unwrap(); - - let body = Full::from(image_data); - - Response::builder() - .header("Content-Type", "image/jpeg") - .body(body) - .unwrap() -} - -async fn parse_image(mut multipart: Multipart) -> Result> { - while let Some(field) = multipart.next_field().await.unwrap() { - if let Some("file") = field.name() { - let bytes = field.bytes().await?; - let image_data = bytes.to_vec(); - return Ok(image_data); - } - } - - Err(anyhow::anyhow!("Missing file field")) + dotenvy::dotenv().ok(); + env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); + info!("Starting app on {}", dotenvy::var("PORT").unwrap()); + run().await; } diff --git a/src/processors/liquid_rescale_processor.rs b/src/processors/liquid_rescale_processor.rs new file mode 100644 index 0000000..f64fa79 --- /dev/null +++ b/src/processors/liquid_rescale_processor.rs @@ -0,0 +1,26 @@ +use axum::{extract::Multipart, http::StatusCode}; +use log::error; + +use crate::utilities::{ + app_error::AppError, liquid_rescale::liquid_rescale_image, parse_image::parse_image, +}; + +pub async fn liquid_rescale_processor(multipart: Multipart) -> Result, AppError> { + let image = parse_image(multipart).await.map_err(|err| { + error!("Error parse image: {:?}", err); + AppError::new( + StatusCode::INTERNAL_SERVER_ERROR, + "There was a error parse your image", + ) + })?; + + let image_data = liquid_rescale_image(&image).map_err(|err| { + error!("Imagemagick parse image error: {:?}", err); + AppError::new( + StatusCode::INTERNAL_SERVER_ERROR, + "There was a error read your image", + ) + })?; + + Ok(image_data) +} diff --git a/src/processors/mod.rs b/src/processors/mod.rs new file mode 100644 index 0000000..9ed4179 --- /dev/null +++ b/src/processors/mod.rs @@ -0,0 +1 @@ +pub mod liquid_rescale_processor; diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..078fb8b --- /dev/null +++ b/src/router.rs @@ -0,0 +1,9 @@ +use axum::{extract::DefaultBodyLimit, routing::post, Router}; + +use crate::routes::liquid::liquid_rescale_route; + +pub fn create_router() -> Router { + Router::new() + .route("/liquid", post(liquid_rescale_route)) + .layer(DefaultBodyLimit::max(1024 * 10_000 /* 10 MiB */)) +} diff --git a/src/routes/liquid.rs b/src/routes/liquid.rs new file mode 100644 index 0000000..c493636 --- /dev/null +++ b/src/routes/liquid.rs @@ -0,0 +1,10 @@ +use axum::{extract::Multipart, Json}; + +use crate::{ + processors::liquid_rescale_processor::liquid_rescale_processor, utilities::app_error::AppError, +}; + +pub async fn liquid_rescale_route(multipart: Multipart) -> Result>, AppError> { + let image_data = liquid_rescale_processor(multipart).await?; + Ok(Json(image_data)) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..cd5d61a --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1 @@ +pub mod liquid; diff --git a/src/utilities/app_error.rs b/src/utilities/app_error.rs new file mode 100644 index 0000000..e0dcceb --- /dev/null +++ b/src/utilities/app_error.rs @@ -0,0 +1,33 @@ +use axum::{http::StatusCode, response::IntoResponse, Json}; +use serde::{Deserialize, Serialize}; + +pub struct AppError { + code: StatusCode, + message: String, +} + +impl AppError { + pub fn new(code: StatusCode, message: impl Into) -> Self { + Self { + code, + message: message.into(), + } + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> axum::response::Response { + ( + self.code, + Json(ErrorResponse { + error: self.message.clone(), + }), + ) + .into_response() + } +} + +#[derive(Serialize, Deserialize)] +struct ErrorResponse { + error: String, +} diff --git a/src/utilities/liquid_rescale.rs b/src/utilities/liquid_rescale.rs new file mode 100644 index 0000000..aa21646 --- /dev/null +++ b/src/utilities/liquid_rescale.rs @@ -0,0 +1,20 @@ +use std::sync::Once; + +use anyhow::Result; +use magick_rust::{magick_wand_genesis, MagickWand}; + +static START: Once = Once::new(); + +pub fn liquid_rescale_image(image: &Vec) -> Result> { + START.call_once(|| { + magick_wand_genesis(); + }); + + let wand = MagickWand::new(); + wand.read_image_blob(&image)?; + wand.fit(640, 640); + wand.liquid_rescale_image(320, 320, 1.0, 0.0)?; + wand.fit(640, 640); + let bytes = wand.write_image_blob("JPEG")?; + Ok(bytes) +} diff --git a/src/utilities/mod.rs b/src/utilities/mod.rs new file mode 100644 index 0000000..0e44d90 --- /dev/null +++ b/src/utilities/mod.rs @@ -0,0 +1,3 @@ +pub mod app_error; +pub mod liquid_rescale; +pub mod parse_image; diff --git a/src/utilities/parse_image.rs b/src/utilities/parse_image.rs new file mode 100644 index 0000000..2811457 --- /dev/null +++ b/src/utilities/parse_image.rs @@ -0,0 +1,14 @@ +use anyhow::Result; +use axum::extract::Multipart; + +pub async fn parse_image(mut multipart: Multipart) -> Result> { + while let Some(field) = multipart.next_field().await.unwrap() { + if let Some("file") = field.name() { + let bytes = field.bytes().await?; + let image_data = bytes.to_vec(); + return Ok(image_data); + } + } + + Err(anyhow::anyhow!("Missing file field")) +}