cdn-uploader/src/lib.rs

259 lines
6.7 KiB
Rust

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<image::ImageError> for Error {
fn from(err: image::ImageError) -> Error {
Error::ImageError(err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error {
Error::IoError(err)
}
}
impl From<s3::creds::error::CredentialsError> for Error {
fn from(err: s3::creds::error::CredentialsError) -> Error {
Error::CredentialsError(err)
}
}
impl From<s3::error::S3Error> for Error {
fn from(err: s3::error::S3Error) -> Error {
Error::S3Error(err)
}
}
impl From<std::num::ParseIntError> 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<u8>,
}
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<Self, Self::Err> {
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::<Vec<_>>();
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<Self, Self::Err> {
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<ImageType> {
vec![ImageType::Jpg, ImageType::Webp]
}
}
impl Deref for Image {
type Target = [u8];
fn deref(&self) -> &[u8] {
&self.data
}
}
pub type Images = Vec<Image>;
pub const SIZES: [u32; 5] = [0, 150, 200, 300, 450];
pub fn fix_image(filename: impl AsRef<Path>, existing_images: &[Image]) -> Result<Images, Error> {
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<Uploader, Error> {
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<String, Error> {
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::<String>(),
));
}
Ok(filename)
}
pub async fn existing_files(&self) -> Result<Vec<Image>, 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)
}
}