diff --git a/geneit_backend/Cargo.lock b/geneit_backend/Cargo.lock index 8f49901..a609b20 100644 --- a/geneit_backend/Cargo.lock +++ b/geneit_backend/Cargo.lock @@ -482,6 +482,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -540,6 +546,12 @@ version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + [[package]] name = "byteorder" version = "1.4.3" @@ -640,6 +652,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.0" @@ -707,6 +725,55 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -913,6 +980,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "email-encoding" version = "0.2.0" @@ -978,6 +1051,22 @@ dependencies = [ "libc", ] +[[package]] +name = "exr" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e481eb11a482815d3e9d618db8c42a93207134662873809335a92327440c18" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fast_chemail" version = "0.9.6" @@ -996,6 +1085,15 @@ dependencies = [ "instant", ] +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + [[package]] name = "flate2" version = "1.0.26" @@ -1006,6 +1104,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1139,6 +1250,7 @@ dependencies = [ "diesel", "env_logger", "futures-util", + "image", "lazy_static", "lettre", "light-openid", @@ -1151,7 +1263,9 @@ dependencies = [ "serde", "serde_json", "serde_with", + "sha2", "thiserror", + "uuid", ] [[package]] @@ -1171,8 +1285,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", ] [[package]] @@ -1200,6 +1326,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1373,6 +1508,25 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "qoi", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1445,6 +1599,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.64" @@ -1466,6 +1629,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "lettre" version = "0.10.4" @@ -1594,6 +1763,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1622,6 +1800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -1636,6 +1815,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -1664,6 +1852,27 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -1835,6 +2044,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + [[package]] name = "pin-project-lite" version = "0.2.10" @@ -1853,6 +2082,19 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "png" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1891,6 +2133,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-xml" version = "0.26.0" @@ -1946,6 +2197,28 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redis" version = "0.23.0" @@ -2332,6 +2605,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "siphasher" version = "0.3.10" @@ -2374,6 +2653,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2468,6 +2756,17 @@ dependencies = [ "syn 2.0.23", ] +[[package]] +name = "tiff" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.22" @@ -2652,6 +2951,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -2768,6 +3076,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "winapi" version = "0.3.9" @@ -2918,3 +3232,12 @@ dependencies = [ "libc", "pkg-config", ] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/geneit_backend/Cargo.toml b/geneit_backend/Cargo.toml index 562be88..8329bf1 100644 --- a/geneit_backend/Cargo.toml +++ b/geneit_backend/Cargo.toml @@ -29,3 +29,6 @@ thiserror = "1.0.40" serde_with = "3.1.0" rust_iso3166 = "0.1.10" rust-s3 = "0.33.0" +sha2 = "0.10.7" +image = "0.24.6" +uuid = { version = "1.4.1", features = ["v4"] } \ No newline at end of file diff --git a/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql b/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql index a7e0910..5492d1a 100644 --- a/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql +++ b/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql @@ -32,7 +32,8 @@ CREATE TABLE memberships ( CREATE TABLE photos ( id SERIAL PRIMARY KEY, - time_create VARCHAR(130) NOT NULL, + file_id VARCHAR(36) NOT NULL, + time_create BIGINT NOT NULL, mime_type VARCHAR(150) NOT NULL, sha512 VARCHAR(130) NOT NULL, file_size INTEGER NOT NULL, diff --git a/geneit_backend/src/connections/s3_connection.rs b/geneit_backend/src/connections/s3_connection.rs index 4e75397..4a76a6a 100644 --- a/geneit_backend/src/connections/s3_connection.rs +++ b/geneit_backend/src/connections/s3_connection.rs @@ -45,3 +45,21 @@ pub async fn create_bucket_if_required() -> anyhow::Result<()> { Ok(()) } + +/// Upload a new file to the bucket +pub async fn upload_file(path: &str, content: &[u8]) -> anyhow::Result<()> { + let bucket = AppConfig::get().s3_bucket()?; + + bucket.put_object(path, content).await?; + + Ok(()) +} + +/// Delete a file, if it exists +pub async fn delete_file_if_exists(path: &str) -> anyhow::Result<()> { + let bucket = AppConfig::get().s3_bucket()?; + + bucket.delete_object(path).await?; + + Ok(()) +} diff --git a/geneit_backend/src/constants.rs b/geneit_backend/src/constants.rs index 016d8a5..1a33f6f 100644 --- a/geneit_backend/src/constants.rs +++ b/geneit_backend/src/constants.rs @@ -128,3 +128,9 @@ pub const ALLOWED_PHOTOS_MIMETYPES: [&str; 6] = [ /// Uploaded photos max size pub const PHOTOS_MAX_SIZE: usize = 10 * 1000 * 1000; + +/// Thumbnail width +pub const THUMB_WIDTH: u32 = 150; + +/// Thumbnail height +pub const THUMB_HEIGHT: u32 = 150; diff --git a/geneit_backend/src/controllers/members_controller.rs b/geneit_backend/src/controllers/members_controller.rs index 7058cdb..2df1c02 100644 --- a/geneit_backend/src/controllers/members_controller.rs +++ b/geneit_backend/src/controllers/members_controller.rs @@ -3,7 +3,7 @@ use crate::controllers::HttpResult; use crate::extractors::family_extractor::FamilyInPath; use crate::extractors::member_extractor::FamilyAndMemberInPath; use crate::models::{Member, MemberID, Sex}; -use crate::services::members_service; +use crate::services::{members_service, photos_service}; use crate::utils::countries_utils; use actix_multipart::form::tempfile::TempFile; use actix_multipart::form::MultipartForm; @@ -237,13 +237,13 @@ pub async fn create(f: FamilyInPath, req: web::Json) -> HttpResul if let Err(e) = req.0.to_member(&mut member).await { log::error!("Failed to apply member information! {e}"); - members_service::delete(&member).await?; + members_service::delete(&mut member).await?; return Ok(HttpResponse::BadRequest().body(e.to_string())); } if let Err(e) = members_service::update(&mut member).await { log::error!("Failed to update member information! {e}"); - members_service::delete(&member).await?; + members_service::delete(&mut member).await?; return Ok(HttpResponse::InternalServerError().finish()); } @@ -291,7 +291,7 @@ pub async fn update(m: FamilyAndMemberInPath, req: web::Json) -> /// Delete a member pub async fn delete(m: FamilyAndMemberInPath) -> HttpResult { - members_service::delete(&m).await?; + members_service::delete(&mut m.to_member()).await?; Ok(HttpResponse::Ok().finish()) } @@ -303,8 +303,24 @@ pub struct UploadPhotoForm { /// Upload a new photo for a user pub async fn set_photo( - _m: FamilyAndMemberInPath, - MultipartForm(_form): MultipartForm, + m: FamilyAndMemberInPath, + MultipartForm(form): MultipartForm, ) -> HttpResult { - todo!() + let photo = photos_service::finalize_upload(form.photo).await?; + let mut member = m.to_member(); + + members_service::remove_photo(&mut member).await?; + + member.set_photo_id(Some(photo.id())); + members_service::update(&mut member).await?; + + Ok(HttpResponse::Ok().finish()) +} + +/// Remove a photo +pub async fn remove_photo(m: FamilyAndMemberInPath) -> HttpResult { + let mut member = m.to_member(); + members_service::remove_photo(&mut member).await?; + + Ok(HttpResponse::Ok().finish()) } diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 51c7222..994fbdb 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -157,6 +157,10 @@ async fn main() -> std::io::Result<()> { "/family/{id}/member/{member_id}/photo", web::put().to(members_controller::set_photo), ) + .route( + "/family/{id}/member/{member_id}/photo", + web::delete().to(members_controller::remove_photo), + ) }) .bind(AppConfig::get().listen_address.as_str())? .run() diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index bdc3b64..3d4eb09 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -1,4 +1,4 @@ -use crate::schema::{families, members, memberships, users}; +use crate::schema::{families, members, memberships, photos, users}; use diesel::prelude::*; /// User ID holder @@ -121,6 +121,52 @@ pub struct FamilyMembership { #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] pub struct PhotoID(pub i32); +#[derive(Queryable, Debug, serde::Serialize)] +pub struct Photo { + id: i32, + pub file_id: String, + pub time_create: i64, + pub mime_type: String, + pub sha512: String, + pub file_size: i32, + pub thumb_sha512: String, +} + +impl Photo { + pub fn id(&self) -> PhotoID { + PhotoID(self.id) + } + + pub fn photo_path(&self) -> String { + format!("photo/{}", self.file_id) + } + + pub fn thumbnail_path(&self) -> String { + format!("thumbnail/{}", self.file_id) + } +} + +#[derive(Insertable)] +#[diesel(table_name = photos)] +pub struct NewPhoto { + pub file_id: String, + pub time_create: i64, + pub mime_type: String, + pub sha512: String, + pub file_size: i32, + pub thumb_sha512: String, +} + +impl NewPhoto { + pub fn photo_path(&self) -> String { + format!("photo/{}", self.file_id) + } + + pub fn thumbnail_path(&self) -> String { + format!("thumbnail/{}", self.file_id) + } +} + /// Member ID holder #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] pub struct MemberID(pub i32); @@ -187,6 +233,10 @@ impl Member { FamilyID(self.family_id) } + pub fn set_photo_id(&mut self, p: Option) { + self.photo_id = p.map(|p| p.0) + } + pub fn photo_id(&self) -> Option { self.photo_id.map(PhotoID) } diff --git a/geneit_backend/src/schema.rs b/geneit_backend/src/schema.rs index e8c490f..1044adf 100644 --- a/geneit_backend/src/schema.rs +++ b/geneit_backend/src/schema.rs @@ -64,7 +64,8 @@ diesel::table! { diesel::table! { photos (id) { id -> Int4, - time_create -> Varchar, + file_id -> Varchar, + time_create -> Int8, mime_type -> Varchar, sha512 -> Varchar, file_size -> Int4, diff --git a/geneit_backend/src/services/members_service.rs b/geneit_backend/src/services/members_service.rs index 1564542..96e6b4a 100644 --- a/geneit_backend/src/services/members_service.rs +++ b/geneit_backend/src/services/members_service.rs @@ -1,6 +1,7 @@ use crate::connections::db_connection; use crate::models::{FamilyID, Member, MemberID, NewMember}; use crate::schema::members; +use crate::services::photos_service; use crate::utils::time_utils::time; use diesel::prelude::*; use diesel::RunQueryDsl; @@ -85,10 +86,25 @@ pub async fn update(member: &mut Member) -> anyhow::Result<()> { Ok(()) } +/// Delete a member photo +pub async fn remove_photo(member: &mut Member) -> anyhow::Result<()> { + match member.photo_id() { + None => {} + Some(photo) => { + photos_service::delete(photo).await?; + member.set_photo_id(None); + update(member).await?; + } + } + + Ok(()) +} + /// Delete a member -pub async fn delete(member: &Member) -> anyhow::Result<()> { +pub async fn delete(member: &mut Member) -> anyhow::Result<()> { // TODO : remove associated couple - // TODO : remove user photo + + remove_photo(member).await?; // Remove the member db_connection::execute(|conn| { @@ -101,8 +117,8 @@ pub async fn delete(member: &Member) -> anyhow::Result<()> { /// Delete all the members of a family pub async fn delete_all_family(family_id: FamilyID) -> anyhow::Result<()> { - for m in get_all_of_family(family_id).await? { - delete(&m).await?; + for mut m in get_all_of_family(family_id).await? { + delete(&mut m).await?; } Ok(()) } diff --git a/geneit_backend/src/services/mod.rs b/geneit_backend/src/services/mod.rs index 5f183b8..02c2085 100644 --- a/geneit_backend/src/services/mod.rs +++ b/geneit_backend/src/services/mod.rs @@ -5,5 +5,6 @@ pub mod login_token_service; pub mod mail_service; pub mod members_service; pub mod openid_service; +pub mod photos_service; pub mod rate_limiter_service; pub mod users_service; diff --git a/geneit_backend/src/services/photos_service.rs b/geneit_backend/src/services/photos_service.rs index e69de29..e7ed50c 100644 --- a/geneit_backend/src/services/photos_service.rs +++ b/geneit_backend/src/services/photos_service.rs @@ -0,0 +1,92 @@ +use crate::connections::{db_connection, s3_connection}; +use crate::constants::{ALLOWED_PHOTOS_MIMETYPES, PHOTOS_MAX_SIZE, THUMB_HEIGHT, THUMB_WIDTH}; +use crate::models::{NewPhoto, Photo, PhotoID}; +use crate::schema::photos; +use crate::utils::crypt_utils::sha512; +use crate::utils::time_utils::time; +use actix_multipart::form::tempfile::TempFile; +use diesel::prelude::*; +use image::imageops::FilterType; +use image::ImageOutputFormat; +use std::io::{Cursor, Read}; +use uuid::Uuid; + +#[derive(thiserror::Error, Debug)] +enum PhotoServiceError { + #[error("Uploaded file is too large ({0} bytes)!")] + FileToLarge(usize), + + #[error("Mime type not specified in request!")] + MissingMimeType, + + #[error("Mimetype forbidden ({0})!")] + MimeTypeForbidden(String), +} + +/// Finalize upload of a photo +pub async fn finalize_upload(mut file: TempFile) -> anyhow::Result { + // Prerequisite checks + if file.size > PHOTOS_MAX_SIZE { + return Err(PhotoServiceError::FileToLarge(file.size).into()); + } + + let mime_type = file + .content_type + .ok_or(PhotoServiceError::MissingMimeType)?; + + if !ALLOWED_PHOTOS_MIMETYPES.contains(&mime_type.as_ref()) { + return Err(PhotoServiceError::MimeTypeForbidden(mime_type.to_string()).into()); + } + + let mut photo_img = Vec::with_capacity(file.size); + file.file.read_to_end(&mut photo_img)?; + + let thumbnail_image = image::load_from_memory(&photo_img)?.resize( + THUMB_WIDTH, + THUMB_HEIGHT, + FilterType::Triangle, + ); + + let mut thumb_cursor = Cursor::new(vec![]); + thumbnail_image.write_to(&mut thumb_cursor, ImageOutputFormat::Png)?; + let thumb_img = thumb_cursor.into_inner(); + + let photo = NewPhoto { + file_id: Uuid::new_v4().to_string(), + time_create: time() as i64, + mime_type: mime_type.to_string(), + sha512: sha512(&photo_img), + file_size: photo_img.len() as i32, + thumb_sha512: sha512(&thumb_img), + }; + + s3_connection::upload_file(&photo.photo_path(), &photo_img).await?; + s3_connection::upload_file(&photo.thumbnail_path(), &thumb_img).await?; + + db_connection::execute(|conn| { + let res: Photo = diesel::insert_into(photos::table) + .values(&photo) + .get_result(conn)?; + + Ok(res) + }) +} + +/// Get a photo by its ID +pub async fn get_by_id(id: PhotoID) -> anyhow::Result { + db_connection::execute(|conn| photos::table.filter(photos::dsl::id.eq(id.0)).first(conn)) +} + +/// Delete a photo +pub async fn delete(id: PhotoID) -> anyhow::Result<()> { + let photo = get_by_id(id).await?; + + s3_connection::delete_file_if_exists(&photo.photo_path()).await?; + s3_connection::delete_file_if_exists(&photo.thumbnail_path()).await?; + + db_connection::execute(|conn| { + diesel::delete(photos::dsl::photos.filter(photos::dsl::id.eq(photo.id().0))).execute(conn) + })?; + + Ok(()) +} diff --git a/geneit_backend/src/utils/crypt_utils.rs b/geneit_backend/src/utils/crypt_utils.rs index e69de29..3af3c3f 100644 --- a/geneit_backend/src/utils/crypt_utils.rs +++ b/geneit_backend/src/utils/crypt_utils.rs @@ -0,0 +1,9 @@ +use sha2::{Digest, Sha512}; + +/// Compute hash of a slice of bytes +pub fn sha512(bytes: &[u8]) -> String { + let mut hasher = Sha512::new(); + hasher.update(bytes); + let h = hasher.finalize(); + format!("{:x}", h) +} diff --git a/geneit_backend/src/utils/mod.rs b/geneit_backend/src/utils/mod.rs index 43fcc8d..b6479b6 100644 --- a/geneit_backend/src/utils/mod.rs +++ b/geneit_backend/src/utils/mod.rs @@ -1,5 +1,6 @@ //! # App utilities pub mod countries_utils; +pub mod crypt_utils; pub mod string_utils; pub mod time_utils;