feat: implement thumbnail processing for image uploads
							parent
							
								
									52478379a0
								
							
						
					
					
						commit
						efb5b9ebea
					
				|  | @ -247,6 +247,12 @@ version = "1.6.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" | checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "bitflags" | ||||||
|  | version = "1.3.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "bitflags" | name = "bitflags" | ||||||
| version = "2.6.0" | version = "2.6.0" | ||||||
|  | @ -308,12 +314,24 @@ version = "3.16.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "bytemuck" | ||||||
|  | version = "1.21.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "byteorder" | name = "byteorder" | ||||||
| version = "1.5.0" | version = "1.5.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "byteorder-lite" | ||||||
|  | version = "0.1.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "bytes" | name = "bytes" | ||||||
| version = "1.9.0" | version = "1.9.0" | ||||||
|  | @ -332,6 +350,7 @@ dependencies = [ | ||||||
|  "diesel", |  "diesel", | ||||||
|  "diesel_migrations", |  "diesel_migrations", | ||||||
|  "futures", |  "futures", | ||||||
|  |  "image", | ||||||
|  "rand", |  "rand", | ||||||
|  "serde", |  "serde", | ||||||
|  "tera", |  "tera", | ||||||
|  | @ -434,6 +453,12 @@ version = "0.7.4" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "color_quant" | ||||||
|  | version = "1.1.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "colorchoice" | name = "colorchoice" | ||||||
| version = "1.0.3" | version = "1.0.3" | ||||||
|  | @ -652,6 +677,15 @@ version = "1.0.1" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "fdeflate" | ||||||
|  | version = "0.3.7" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" | ||||||
|  | dependencies = [ | ||||||
|  |  "simd-adler32", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "flate2" | name = "flate2" | ||||||
| version = "1.0.35" | version = "1.0.35" | ||||||
|  | @ -787,6 +821,16 @@ dependencies = [ | ||||||
|  "wasi", |  "wasi", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "gif" | ||||||
|  | version = "0.13.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" | ||||||
|  | dependencies = [ | ||||||
|  |  "color_quant", | ||||||
|  |  "weezl", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "gimli" | name = "gimli" | ||||||
| version = "0.31.1" | version = "0.31.1" | ||||||
|  | @ -812,7 +856,7 @@ version = "0.9.1" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" | checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "bitflags", |  "bitflags 2.6.0", | ||||||
|  "ignore", |  "ignore", | ||||||
|  "walkdir", |  "walkdir", | ||||||
| ] | ] | ||||||
|  | @ -970,6 +1014,22 @@ dependencies = [ | ||||||
|  "winapi-util", |  "winapi-util", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "image" | ||||||
|  | version = "0.25.5" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" | ||||||
|  | dependencies = [ | ||||||
|  |  "bytemuck", | ||||||
|  |  "byteorder-lite", | ||||||
|  |  "color_quant", | ||||||
|  |  "gif", | ||||||
|  |  "num-traits", | ||||||
|  |  "png", | ||||||
|  |  "zune-core", | ||||||
|  |  "zune-jpeg", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "indexmap" | name = "indexmap" | ||||||
| version = "2.7.0" | version = "2.7.0" | ||||||
|  | @ -1102,6 +1162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" | checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "adler2", |  "adler2", | ||||||
|  |  "simd-adler32", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -1328,6 +1389,19 @@ version = "0.3.31" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "png" | ||||||
|  | version = "0.17.16" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" | ||||||
|  | dependencies = [ | ||||||
|  |  "bitflags 1.3.2", | ||||||
|  |  "crc32fast", | ||||||
|  |  "fdeflate", | ||||||
|  |  "flate2", | ||||||
|  |  "miniz_oxide", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "powerfmt" | name = "powerfmt" | ||||||
| version = "0.2.0" | version = "0.2.0" | ||||||
|  | @ -1408,7 +1482,7 @@ version = "0.5.8" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "bitflags", |  "bitflags 2.6.0", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -1580,6 +1654,12 @@ dependencies = [ | ||||||
|  "libc", |  "libc", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "simd-adler32" | ||||||
|  | version = "0.3.7" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "siphasher" | name = "siphasher" | ||||||
| version = "0.3.11" | version = "0.3.11" | ||||||
|  | @ -1838,7 +1918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" | checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "async-compression", |  "async-compression", | ||||||
|  "bitflags", |  "bitflags 2.6.0", | ||||||
|  "bytes", |  "bytes", | ||||||
|  "futures-core", |  "futures-core", | ||||||
|  "futures-util", |  "futures-util", | ||||||
|  | @ -2096,6 +2176,12 @@ version = "0.2.99" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" | checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "weezl" | ||||||
|  | version = "0.1.8" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "winapi" | name = "winapi" | ||||||
| version = "0.3.9" | version = "0.3.9" | ||||||
|  | @ -2247,3 +2333,18 @@ dependencies = [ | ||||||
|  "quote", |  "quote", | ||||||
|  "syn", |  "syn", | ||||||
| ] | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "zune-core" | ||||||
|  | version = "0.4.12" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "zune-jpeg" | ||||||
|  | version = "0.4.14" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" | ||||||
|  | dependencies = [ | ||||||
|  |  "zune-core", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ clap = { version = "4.5.26", features = ["derive", "env"] } | ||||||
| diesel = { version = "2.2.6", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "r2d2", "chrono"] } | diesel = { version = "2.2.6", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "r2d2", "chrono"] } | ||||||
| diesel_migrations = { version = "2.2.0", features = ["sqlite"] } | diesel_migrations = { version = "2.2.0", features = ["sqlite"] } | ||||||
| futures = "0.3.31" | futures = "0.3.31" | ||||||
|  | image = { version = "0.25.5", default-features = false, features = ["gif", "jpeg", "png"] } | ||||||
| rand = "0.8.5" | rand = "0.8.5" | ||||||
| # this dependency is needed soly because the r2d2_sqlite crate doesn't export | # this dependency is needed soly because the r2d2_sqlite crate doesn't export | ||||||
| # the 'chrono' feature flag | # the 'chrono' feature flag | ||||||
|  |  | ||||||
|  | @ -5,5 +5,6 @@ create table images ( | ||||||
|         -- Keep entries in the database so the files can be removed later |         -- Keep entries in the database so the files can be removed later | ||||||
|         on delete set null, |         on delete set null, | ||||||
|     date_taken date not null, |     date_taken date not null, | ||||||
|  |     mime_type text not null, | ||||||
|     note text |     note text | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ pub struct Image { | ||||||
|     pub id: i32, |     pub id: i32, | ||||||
|     pub plant_id: i32, |     pub plant_id: i32, | ||||||
|     pub date_taken: NaiveDate, |     pub date_taken: NaiveDate, | ||||||
|  |     pub mime_type: String, | ||||||
|     pub note: Option<String>, |     pub note: Option<String>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -20,14 +21,21 @@ pub struct Image { | ||||||
| pub struct NewImage { | pub struct NewImage { | ||||||
|     plant_id: i32, |     plant_id: i32, | ||||||
|     date_taken: NaiveDate, |     date_taken: NaiveDate, | ||||||
|  |     mime_type: String, | ||||||
|     note: Option<String>, |     note: Option<String>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl NewImage { | impl NewImage { | ||||||
|     pub fn new(plant_id: i32, date_taken: NaiveDate, note: Option<String>) -> Self { |     pub fn new( | ||||||
|  |         plant_id: i32, | ||||||
|  |         date_taken: NaiveDate, | ||||||
|  |         mime_type: String, | ||||||
|  |         note: Option<String>, | ||||||
|  |     ) -> Self { | ||||||
|         Self { |         Self { | ||||||
|             plant_id, |             plant_id, | ||||||
|             date_taken, |             date_taken, | ||||||
|  |             mime_type, | ||||||
|             note, |             note, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ diesel::table! { | ||||||
|         id -> Integer, |         id -> Integer, | ||||||
|         plant_id -> Integer, |         plant_id -> Integer, | ||||||
|         date_taken -> Date, |         date_taken -> Date, | ||||||
|  |         mime_type -> Text, | ||||||
|         note -> Nullable<Text>, |         note -> Nullable<Text>, | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								src/main.rs
								
								
								
								
							
							
						
						
									
										20
									
								
								src/main.rs
								
								
								
								
							|  | @ -10,6 +10,7 @@ use std::{ | ||||||
| 
 | 
 | ||||||
| use clap::Parser; | use clap::Parser; | ||||||
| use tera::Tera; | use tera::Tera; | ||||||
|  | use tokio::sync::Mutex; | ||||||
| use tower_http::compression::CompressionLayer; | use tower_http::compression::CompressionLayer; | ||||||
| 
 | 
 | ||||||
| use cli::UserCmd; | use cli::UserCmd; | ||||||
|  | @ -17,12 +18,26 @@ use db::DbError; | ||||||
| 
 | 
 | ||||||
| const DB_FILENAME: &str = "db.sqlite3"; | const DB_FILENAME: &str = "db.sqlite3"; | ||||||
| const IMG_DIR: &str = "imgs"; | const IMG_DIR: &str = "imgs"; | ||||||
|  | const TMP_DIR: &str = "tmp"; | ||||||
| 
 | 
 | ||||||
| #[derive(Clone)] | #[derive(Clone)] | ||||||
| pub struct Context { | pub struct Context { | ||||||
|     pool: db::DbPool, |     pool: db::DbPool, | ||||||
|     tera: Arc<Tera>, |     tera: Arc<Tera>, | ||||||
|     data_dir: PathBuf, |     data_dir: PathBuf, | ||||||
|  |     tmp_dir_counter: Arc<Mutex<u16>>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Context { | ||||||
|  |     pub async fn tmp_file_path(&self) -> PathBuf { | ||||||
|  |         let mut guard = self.tmp_dir_counter.lock().await; | ||||||
|  | 
 | ||||||
|  |         let path = self.data_dir.join(TMP_DIR).join(guard.to_string()); | ||||||
|  | 
 | ||||||
|  |         *guard = guard.wrapping_add(1); | ||||||
|  | 
 | ||||||
|  |         path | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn run_user_cli(data_dir: impl AsRef<Path>, cmd: UserCmd) -> Result<(), DbError> { | fn run_user_cli(data_dir: impl AsRef<Path>, cmd: UserCmd) -> Result<(), DbError> { | ||||||
|  | @ -69,6 +84,10 @@ async fn main() { | ||||||
|                 fs::create_dir(args.data_dir.join(IMG_DIR)).unwrap(); |                 fs::create_dir(args.data_dir.join(IMG_DIR)).unwrap(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             if !fs::exists(args.data_dir.join(TMP_DIR)).unwrap() { | ||||||
|  |                 fs::create_dir(args.data_dir.join(TMP_DIR)).unwrap(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             let pool = db::initialize_db(args.data_dir.join(DB_FILENAME), true).unwrap(); |             let pool = db::initialize_db(args.data_dir.join(DB_FILENAME), true).unwrap(); | ||||||
| 
 | 
 | ||||||
|             let tera = |             let tera = | ||||||
|  | @ -78,6 +97,7 @@ async fn main() { | ||||||
|                 pool, |                 pool, | ||||||
|                 tera: Arc::new(tera), |                 tera: Arc::new(tera), | ||||||
|                 data_dir: args.data_dir.clone(), |                 data_dir: args.data_dir.clone(), | ||||||
|  |                 tmp_dir_counter: Arc::new(Mutex::new(0)), | ||||||
|             }; |             }; | ||||||
|             let app = server::app(ctx, &args.static_dir) |             let app = server::app(ctx, &args.static_dir) | ||||||
|                 .layer(CompressionLayer::new().br(true).gzip(true)); |                 .layer(CompressionLayer::new().br(true).gzip(true)); | ||||||
|  |  | ||||||
|  | @ -83,6 +83,15 @@ impl From<std::io::Error> for AppError { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | impl From<image::ImageError> for AppError { | ||||||
|  |     fn from(value: image::ImageError) -> Self { | ||||||
|  |         match value { | ||||||
|  |             image::ImageError::IoError(err) => Self::IO(err), | ||||||
|  |             _ => Self::BadRequest, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| impl IntoResponse for AppError { | impl IntoResponse for AppError { | ||||||
|     fn into_response(self) -> axum::response::Response { |     fn into_response(self) -> axum::response::Response { | ||||||
|         match self { |         match self { | ||||||
|  |  | ||||||
|  | @ -1,18 +1,30 @@ | ||||||
| use axum::{ | use axum::{ | ||||||
|     extract::{multipart::Field, Multipart, State}, |     extract::{DefaultBodyLimit, Multipart, State}, | ||||||
|  |     handler::Handler, | ||||||
|     response::Html, |     response::Html, | ||||||
|     routing::post, |     routing::post, | ||||||
|     Router, |     Router, | ||||||
| }; | }; | ||||||
| use chrono::NaiveDate; | use chrono::NaiveDate; | ||||||
| use futures::TryStreamExt; | use futures::TryStreamExt; | ||||||
|  | use image::{codecs::jpeg::JpegEncoder, ImageReader}; | ||||||
| use tokio_util::io::StreamReader; | use tokio_util::io::StreamReader; | ||||||
| 
 | 
 | ||||||
|  | use std::{io::BufWriter, path::PathBuf}; | ||||||
|  | 
 | ||||||
| use super::error::AppError; | use super::error::AppError; | ||||||
| use crate::db::NewImage; | use crate::{ | ||||||
|  |     db::{Image, NewImage}, | ||||||
|  |     IMG_DIR, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const THUMBNAIL_EXT: &str = "thumb"; | ||||||
| 
 | 
 | ||||||
| pub fn app() -> axum::Router<crate::Context> { | pub fn app() -> axum::Router<crate::Context> { | ||||||
|     Router::new().route("/", post(post_image)) |     Router::new().route( | ||||||
|  |         "/", | ||||||
|  |         post(post_image.layer(DefaultBodyLimit::max(1024 * 1024 * 20))), | ||||||
|  |     ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async fn post_image( | async fn post_image( | ||||||
|  | @ -23,6 +35,9 @@ async fn post_image( | ||||||
|     let mut date_taken: Option<NaiveDate> = None; |     let mut date_taken: Option<NaiveDate> = None; | ||||||
|     let mut note: Option<String> = None; |     let mut note: Option<String> = None; | ||||||
| 
 | 
 | ||||||
|  |     let mut img_paths: Option<(PathBuf, PathBuf)> = None; | ||||||
|  |     let mut mime_type: Option<&'static str> = None; | ||||||
|  | 
 | ||||||
|     while let Some(field) = mt.next_field().await? { |     while let Some(field) = mt.next_field().await? { | ||||||
|         match field.name() { |         match field.name() { | ||||||
|             Some("plant_id") => { |             Some("plant_id") => { | ||||||
|  | @ -47,15 +62,21 @@ async fn post_image( | ||||||
|                 note = Some(field.text().await?); |                 note = Some(field.text().await?); | ||||||
|             } |             } | ||||||
|             Some("image") => { |             Some("image") => { | ||||||
|                 // These fields are required to be provided before the image field (note is
 |                 let path = ctx.tmp_file_path().await; | ||||||
|                 // optional)
 |  | ||||||
|                 if plant_id.is_none() || date_taken.is_none() { |  | ||||||
|                     return Err(AppError::BadRequest); |  | ||||||
|                 } |  | ||||||
| 
 | 
 | ||||||
|                 receive_image(ctx, plant_id.unwrap(), date_taken.unwrap(), note, field).await?; |                 let mut f = tokio::fs::File::create(&path).await?; | ||||||
|  |                 let mut r = StreamReader::new(field.map_err(std::io::Error::other)); | ||||||
| 
 | 
 | ||||||
|                 return Ok(Html(String::new())); |                 tokio::io::copy(&mut r, &mut f).await?; | ||||||
|  | 
 | ||||||
|  |                 let jpeg_path = ctx.tmp_file_path().await; | ||||||
|  |                 img_paths = Some((path.clone(), jpeg_path.clone())); | ||||||
|  | 
 | ||||||
|  |                 mime_type = Some( | ||||||
|  |                     tokio::task::spawn_blocking(move || process_image(path, jpeg_path)) | ||||||
|  |                         .await | ||||||
|  |                         .unwrap()?, | ||||||
|  |                 ); | ||||||
|             } |             } | ||||||
|             _ => { |             _ => { | ||||||
|                 return Err(AppError::BadRequest); |                 return Err(AppError::BadRequest); | ||||||
|  | @ -63,31 +84,47 @@ async fn post_image( | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Err(AppError::BadRequest) |     if let (Some(plant_id), Some(date_taken), Some((img_path, jpeg_path)), Some(mime_type)) = | ||||||
| } |         (plant_id, date_taken, img_paths, mime_type) | ||||||
| 
 |     { | ||||||
| async fn receive_image<'a>( |         let image = NewImage::new(plant_id, date_taken, mime_type.to_string(), note); | ||||||
|     ctx: crate::Context, |         let image = | ||||||
|     plant_id: i32, |             tokio::task::spawn_blocking(move || insert_image(&ctx, image, img_path, jpeg_path)) | ||||||
|     date_taken: NaiveDate, |  | ||||||
|     note: Option<String>, |  | ||||||
|     field: Field<'a>, |  | ||||||
| ) -> super::Result<()> { |  | ||||||
|     let image = tokio::task::spawn_blocking(move || { |  | ||||||
|         NewImage::new(plant_id, date_taken, note).insert(&ctx.pool) |  | ||||||
|     }) |  | ||||||
|                 .await |                 .await | ||||||
|                 .unwrap()?; |                 .unwrap()?; | ||||||
| 
 | 
 | ||||||
|     let mut r = StreamReader::new(field.map_err(std::io::Error::other)); |         Ok(Html(String::new())) | ||||||
|     let mut f = tokio::fs::File::create( |     } else { | ||||||
|         ctx.data_dir |         Err(AppError::BadRequest) | ||||||
|             .join(crate::IMG_DIR) |     } | ||||||
|             .join(image.id.to_string()) | } | ||||||
|             .with_extension("png"), | 
 | ||||||
|     ) | fn process_image(path: PathBuf, jpg_path: PathBuf) -> super::Result<&'static str> { | ||||||
|     .await?; |     let reader = ImageReader::open(path)?.with_guessed_format()?; | ||||||
|     tokio::io::copy(&mut r, &mut f).await?; |     let mime_type = reader.format().ok_or(AppError::BadRequest)?.to_mime_type(); | ||||||
| 
 | 
 | ||||||
|     Ok(()) |     // Decode image and resize to fit into 720 x 720 square
 | ||||||
|  |     let img = reader.decode()?.thumbnail(720, 720); | ||||||
|  | 
 | ||||||
|  |     // Write image as compressed JPEG
 | ||||||
|  |     let jpeg_f = BufWriter::new(std::fs::File::create(jpg_path)?); | ||||||
|  |     let encoder = JpegEncoder::new_with_quality(jpeg_f, 60); | ||||||
|  |     img.write_with_encoder(encoder)?; | ||||||
|  | 
 | ||||||
|  |     Ok(mime_type) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn insert_image( | ||||||
|  |     ctx: &crate::Context, | ||||||
|  |     image: NewImage, | ||||||
|  |     img_path: PathBuf, | ||||||
|  |     jpeg_path: PathBuf, | ||||||
|  | ) -> super::Result<Image> { | ||||||
|  |     let image = image.insert(&ctx.pool)?; | ||||||
|  | 
 | ||||||
|  |     let dest_path = ctx.data_dir.join(IMG_DIR).join(image.id.to_string()); | ||||||
|  |     std::fs::rename(img_path, &dest_path)?; | ||||||
|  |     std::fs::rename(jpeg_path, dest_path.with_extension(THUMBNAIL_EXT))?; | ||||||
|  | 
 | ||||||
|  |     Ok(image) | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue