use std::{io::Cursor, ops::Deref, path::Path, str::FromStr}; use s3::Bucket; #[derive(Debug)] pub enum Error { ImageError(image::ImageError), WebpError(String), IoError(std::io::Error), CredentialsError(s3::creds::error::CredentialsError), S3Error(s3::error::S3Error), UploadError(String), ParseError(String), ParseIntError(std::num::ParseIntError), } impl From for Error { fn from(err: image::ImageError) -> Error { Error::ImageError(err) } } impl From for Error { fn from(err: std::io::Error) -> Error { Error::IoError(err) } } impl From for Error { fn from(err: s3::creds::error::CredentialsError) -> Error { Error::CredentialsError(err) } } impl From for Error { fn from(err: s3::error::S3Error) -> Error { Error::S3Error(err) } } impl From for Error { fn from(err: std::num::ParseIntError) -> Error { Error::ParseIntError(err) } } #[derive(Clone)] pub struct Image { image_type: ImageType, name: String, width: u32, data: Vec, } impl std::fmt::Debug for Image { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Image") .field("name", &self.name) .field("width", &self.width) .field("image_type", &self.image_type) .finish_non_exhaustive() } } impl Image { pub fn name(&self) -> String { if self.width == 0 { format!("{}.{}", self.name, self.image_type.extension()) } else { format!( "{}-{}.{}", self.name, self.width, self.image_type.extension() ) } } } impl FromStr for Image { type Err = Error; fn from_str(s: &str) -> Result { let mut parts = s.split('.'); let name = parts.next().unwrap().to_owned(); let image_type = parts.next().unwrap().parse()?; let r = name.split(|c| c == '-').collect::>(); let name = r[0].to_owned(); let width = if r.len() > 1 { r[1].parse()? } else { 0 }; Ok(Image { image_type, name, width, data: vec![], }) } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ImageType { Jpg, Webp, } impl FromStr for ImageType { type Err = Error; fn from_str(s: &str) -> Result { match s { "jpg" => Ok(ImageType::Jpg), "webp" => Ok(ImageType::Webp), _ => Err(Error::ParseError(s.to_owned())), } } } impl ImageType { pub fn extension(&self) -> &str { match self { ImageType::Jpg => "jpg", ImageType::Webp => "webp", } } pub fn content_type(&self) -> &str { match self { ImageType::Jpg => "image/jpeg", ImageType::Webp => "image/webp", } } pub fn options() -> Vec { vec![ImageType::Jpg, ImageType::Webp] } } impl Deref for Image { type Target = [u8]; fn deref(&self) -> &[u8] { &self.data } } pub type Images = Vec; pub const SIZES: [u32; 5] = [0, 150, 200, 300, 450]; pub fn fix_image(filename: impl AsRef, existing_images: &[Image]) -> Result { let img = image::open(filename.as_ref())?; let z = filename.as_ref().file_name(); let name = z.unwrap().to_str().unwrap().to_string(); let name = name.split(|c| c == '.').nth(0).unwrap().to_string(); let mut v = vec![]; for image_type in ImageType::options() { for width in SIZES { if existing_images .iter() .any(|img| img.name == name && img.width == width && img.image_type == image_type) { continue; } let img = img.resize( if width == 0 { 450 } else { width }, 999999, image::imageops::Lanczos3, ); let img = image::DynamicImage::ImageRgba8(img.to_rgba8()); let data = match image_type { ImageType::Jpg => { let mut jpg_buf = vec![]; img.write_to( &mut Cursor::new(&mut jpg_buf), image::ImageOutputFormat::Jpeg(75), )?; jpg_buf } ImageType::Webp => { let z = webp::Encoder::from_image(&img) .map_err(|e| Error::WebpError(e.to_string()))?; let d = z.encode(50.0); d.to_vec() } }; v.push(Image { image_type, name: name.clone(), width, data, }); } } Ok(v) } #[derive(Clone)] pub struct Uploader { bucket: Bucket, date: String, } impl Uploader { pub fn new( bucket: &str, access_key: &str, secret_key: &str, region: &str, endpoint: Option<&str>, ) -> Result { let mut region = s3::Region::from_str(region).unwrap(); if let Some(endpoint) = endpoint { region = s3::Region::Custom { endpoint: endpoint.to_string(), region: format!("{}", region), }; } let bucket = Bucket::new( bucket, region, s3::creds::Credentials::new(Some(access_key), Some(secret_key), None, None, None)?, )? .with_path_style(); Ok(Uploader { bucket, date: chrono::Local::now().date().format("%Y%m%d").to_string(), }) } pub async fn upload(&self, data: &Image) -> Result { let filename = format!("{}/{}", self.date, data.name()); let (data, code) = self .bucket .put_object_with_content_type(&filename, &data, data.image_type.content_type()) .await?; // println!("{}", ); if code != 200 { return Err(Error::UploadError( data.iter().map(|d| char::from(*d)).collect::(), )); } Ok(filename) } pub async fn existing_files(&self) -> Result, Error> { let z = self.bucket.list(format!("/{}", self.date), None).await?; let mut all_files = vec![]; for w in z { for x in w.contents { let name = &x.key; all_files.push(name.split_at(self.date.len() + 1).1.to_string().parse()?) } } Ok(all_files) } }