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 mime_guess::Mime; use std::fs::File; use std::io::{Cursor, Read, Seek, Write}; 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), } pub struct UploadedFile { pub size: usize, pub content_type: Option, pub file: File, } impl From for UploadedFile { fn from(value: TempFile) -> Self { Self { size: value.size, content_type: value.content_type, file: value.file.into_file(), } } } impl UploadedFile { pub fn from_memory(buff: &[u8], content_type: Option) -> anyhow::Result { let mut file = tempfile::tempfile()?; file.write_all(buff)?; file.rewind()?; Ok(Self { size: buff.len(), content_type, file, }) } } /// Finalize upload of a photo pub async fn finalize_upload(mut file: UploadedFile) -> 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(()) }