Can delete member image
This commit is contained in:
@ -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(())
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<MemberRequest>) -> 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<MemberRequest>) ->
|
||||
|
||||
/// 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<UploadPhotoForm>,
|
||||
m: FamilyAndMemberInPath,
|
||||
MultipartForm(form): MultipartForm<UploadPhotoForm>,
|
||||
) -> 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())
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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<PhotoID>) {
|
||||
self.photo_id = p.map(|p| p.0)
|
||||
}
|
||||
|
||||
pub fn photo_id(&self) -> Option<PhotoID> {
|
||||
self.photo_id.map(PhotoID)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<Photo> {
|
||||
// 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<Photo> {
|
||||
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(())
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
//! # App utilities
|
||||
|
||||
pub mod countries_utils;
|
||||
pub mod crypt_utils;
|
||||
pub mod string_utils;
|
||||
pub mod time_utils;
|
||||
|
Reference in New Issue
Block a user