Initial Commit
This commit is contained in:
commit
d5849b7203
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1923
Cargo.lock
generated
Normal file
1923
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "cdn-upload"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
rust-s3 = "0.31.0"
|
||||
webp = "0.2.2"
|
||||
image = "0.24.2"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
chrono = "0.4.19"
|
||||
clap = "3.1.18"
|
72
src/bin/upload.rs
Normal file
72
src/bin/upload.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
use clap::Command;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let bucket = clap::Arg::new("bucket")
|
||||
.help("The bucket to upload to")
|
||||
.long("bucket")
|
||||
.short('b')
|
||||
.takes_value(true)
|
||||
.required(true);
|
||||
let access_key = clap::Arg::new("access-key")
|
||||
.help("The access key to use")
|
||||
.long("access-key")
|
||||
.short('a')
|
||||
.takes_value(true)
|
||||
.required(true);
|
||||
let secret_key = clap::Arg::new("secret-key")
|
||||
.help("The secret key to use")
|
||||
.long("secret-key")
|
||||
.short('s')
|
||||
.takes_value(true)
|
||||
.required(true);
|
||||
let region = clap::Arg::new("region")
|
||||
.help("The region to use")
|
||||
.long("region")
|
||||
.short('r')
|
||||
.takes_value(true)
|
||||
.default_value("us-east-1");
|
||||
let endpoint = clap::Arg::new("endpoint")
|
||||
.help("The endpoint to use")
|
||||
.long("endpoint")
|
||||
.short('e')
|
||||
.takes_value(true);
|
||||
let input = clap::Arg::new("input")
|
||||
.help("The input directory")
|
||||
.index(1)
|
||||
.multiple_values(true);
|
||||
|
||||
let matches = Command::new("S3 Image Uploader")
|
||||
.version("1.0")
|
||||
.author("Julius")
|
||||
.about("used to upload an image to s3")
|
||||
.args(&[bucket, access_key, secret_key, region, endpoint, input])
|
||||
.get_matches();
|
||||
|
||||
let u = cdn_upload::Uploader::new(
|
||||
matches.value_of("bucket").unwrap(),
|
||||
matches.value_of("access-key").unwrap(),
|
||||
matches.value_of("secret-key").unwrap(),
|
||||
matches.value_of("region").unwrap(),
|
||||
matches.value_of("endpoint"),
|
||||
)
|
||||
.unwrap();
|
||||
let r = u.existing_files().await.unwrap();
|
||||
let m = matches.values_of("input").unwrap().map(ToString::to_string);
|
||||
let handles = m.map(|arg| {
|
||||
let u = u.clone();
|
||||
let r = r.clone();
|
||||
tokio::spawn(async move {
|
||||
let filename = arg;
|
||||
let data = cdn_upload::fix_image(filename.clone(), &r).unwrap();
|
||||
for img in data {
|
||||
u.upload(&img).await.unwrap();
|
||||
println!("Uploaded {} as {}", filename, img.name());
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for handle in handles {
|
||||
handle.await.unwrap();
|
||||
}
|
||||
}
|
258
src/lib.rs
Normal file
258
src/lib.rs
Normal file
|
@ -0,0 +1,258 @@
|
|||
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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue