mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-11-10 06:25:00 +00:00
Add a concept of 'processors' to allow multiple image transformations
This commit is contained in:
parent
d71b86a81e
commit
fe0082a23b
3 changed files with 118 additions and 40 deletions
|
@ -55,6 +55,9 @@ pub enum UploadError {
|
||||||
|
|
||||||
#[error("Unable to send request, {0}")]
|
#[error("Unable to send request, {0}")]
|
||||||
SendRequest(String),
|
SendRequest(String),
|
||||||
|
|
||||||
|
#[error("No filename provided in request")]
|
||||||
|
MissingFilename,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<actix_web::client::SendRequestError> for UploadError {
|
impl From<actix_web::client::SendRequestError> for UploadError {
|
||||||
|
@ -96,7 +99,7 @@ impl ResponseError for UploadError {
|
||||||
UploadError::NoFiles | UploadError::ContentType(_) | UploadError::Upload(_) => {
|
UploadError::NoFiles | UploadError::ContentType(_) | UploadError::Upload(_) => {
|
||||||
StatusCode::BAD_REQUEST
|
StatusCode::BAD_REQUEST
|
||||||
}
|
}
|
||||||
UploadError::MissingAlias => StatusCode::NOT_FOUND,
|
UploadError::MissingAlias | UploadError::MissingFilename => StatusCode::NOT_FOUND,
|
||||||
UploadError::InvalidToken => StatusCode::FORBIDDEN,
|
UploadError::InvalidToken => StatusCode::FORBIDDEN,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
}
|
}
|
||||||
|
|
55
src/main.rs
55
src/main.rs
|
@ -13,6 +13,7 @@ use structopt::StructOpt;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod processor;
|
||||||
mod upload_manager;
|
mod upload_manager;
|
||||||
|
|
||||||
use self::{config::Config, error::UploadError, upload_manager::UploadManager};
|
use self::{config::Config, error::UploadError, upload_manager::UploadManager};
|
||||||
|
@ -151,39 +152,23 @@ async fn delete(
|
||||||
Ok(HttpResponse::NoContent().finish())
|
Ok(HttpResponse::NoContent().finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serve original files
|
/// Serve files
|
||||||
async fn serve(
|
async fn serve(
|
||||||
manager: web::Data<UploadManager>,
|
manager: web::Data<UploadManager>,
|
||||||
alias: web::Path<String>,
|
segments: web::Path<String>,
|
||||||
) -> Result<HttpResponse, UploadError> {
|
) -> Result<HttpResponse, UploadError> {
|
||||||
let filename = manager.from_alias(alias.into_inner()).await?;
|
let mut segments: Vec<String> = segments
|
||||||
let mut path = manager.image_dir();
|
.into_inner()
|
||||||
path.push(filename);
|
.split('/')
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
let alias = segments.pop().ok_or(UploadError::MissingFilename)?;
|
||||||
|
|
||||||
let ext = path
|
let chain = self::processor::build_chain(&segments);
|
||||||
.extension()
|
|
||||||
.ok_or(UploadError::MissingExtension)?
|
|
||||||
.to_owned();
|
|
||||||
let ext = from_ext(ext);
|
|
||||||
|
|
||||||
let stream = actix_fs::read_to_stream(path).await?;
|
|
||||||
|
|
||||||
Ok(srv_response(stream, ext))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serve resized files
|
|
||||||
async fn serve_resized(
|
|
||||||
manager: web::Data<UploadManager>,
|
|
||||||
path_entries: web::Path<(u32, String)>,
|
|
||||||
) -> Result<HttpResponse, UploadError> {
|
|
||||||
use image::GenericImageView;
|
|
||||||
|
|
||||||
let mut path = manager.image_dir();
|
|
||||||
|
|
||||||
let (size, alias) = path_entries.into_inner();
|
|
||||||
let name = manager.from_alias(alias).await?;
|
let name = manager.from_alias(alias).await?;
|
||||||
path.push(size.to_string());
|
let base = manager.image_dir();
|
||||||
path.push(name.clone());
|
let path = self::processor::build_path(base, &chain, name.clone());
|
||||||
|
|
||||||
let ext = path
|
let ext = path
|
||||||
.extension()
|
.extension()
|
||||||
|
@ -194,7 +179,7 @@ async fn serve_resized(
|
||||||
// If the thumbnail doesn't exist, we need to create it
|
// If the thumbnail doesn't exist, we need to create it
|
||||||
if let Err(e) = actix_fs::metadata(path.clone()).await {
|
if let Err(e) = actix_fs::metadata(path.clone()).await {
|
||||||
if e.kind() != Some(std::io::ErrorKind::NotFound) {
|
if e.kind() != Some(std::io::ErrorKind::NotFound) {
|
||||||
error!("Error looking up thumbnail, {}", e);
|
error!("Error looking up processed image, {}", e);
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,17 +197,12 @@ async fn serve_resized(
|
||||||
(img, format)
|
(img, format)
|
||||||
};
|
};
|
||||||
|
|
||||||
// return original image if resize target is larger
|
let img = self::processor::process_image(chain, img).await?;
|
||||||
if !img.in_bounds(size, size) {
|
|
||||||
drop(img);
|
|
||||||
let stream = actix_fs::read_to_stream(original_path).await?;
|
|
||||||
return Ok(srv_response(stream, ext));
|
|
||||||
}
|
|
||||||
|
|
||||||
// perform thumbnail operation in a blocking thread
|
// perform thumbnail operation in a blocking thread
|
||||||
let img_bytes: bytes::Bytes = web::block(move || {
|
let img_bytes: bytes::Bytes = web::block(move || {
|
||||||
let mut bytes = std::io::Cursor::new(vec![]);
|
let mut bytes = std::io::Cursor::new(vec![]);
|
||||||
img.thumbnail(size, size).write_to(&mut bytes, format)?;
|
img.write_to(&mut bytes, format)?;
|
||||||
Ok(bytes::Bytes::from(bytes.into_inner())) as Result<_, image::error::ImageError>
|
Ok(bytes::Bytes::from(bytes.into_inner())) as Result<_, image::error::ImageError>
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -319,14 +299,11 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
.route(web::post().to(upload)),
|
.route(web::post().to(upload)),
|
||||||
)
|
)
|
||||||
.service(web::resource("/download").route(web::get().to(download)))
|
.service(web::resource("/download").route(web::get().to(download)))
|
||||||
.service(web::resource("/{filename}").route(web::get().to(serve)))
|
|
||||||
.service(
|
.service(
|
||||||
web::resource("/delete/{delete_token}/{filename}")
|
web::resource("/delete/{delete_token}/{filename}")
|
||||||
.route(web::delete().to(delete)),
|
.route(web::delete().to(delete)),
|
||||||
)
|
)
|
||||||
.service(
|
.service(web::resource("/{tail:.*}").route(web::get().to(serve))),
|
||||||
web::resource("/{size}/{filename}").route(web::get().to(serve_resized)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.bind(config.bind_address())?
|
.bind(config.bind_address())?
|
||||||
|
|
98
src/processor.rs
Normal file
98
src/processor.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use crate::error::UploadError;
|
||||||
|
use actix_web::web;
|
||||||
|
use image::{DynamicImage, GenericImageView};
|
||||||
|
use log::warn;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub(crate) trait Processor {
|
||||||
|
fn path(&self, path: PathBuf) -> PathBuf;
|
||||||
|
fn process(&self, img: DynamicImage) -> Result<DynamicImage, UploadError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Identity;
|
||||||
|
|
||||||
|
impl Processor for Identity {
|
||||||
|
fn path(&self, mut path: PathBuf) -> PathBuf {
|
||||||
|
path.push("identity");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, img: DynamicImage) -> Result<DynamicImage, UploadError> {
|
||||||
|
Ok(img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Thumbnail(u32);
|
||||||
|
|
||||||
|
impl Processor for Thumbnail {
|
||||||
|
fn path(&self, mut path: PathBuf) -> PathBuf {
|
||||||
|
path.push("thumbnail");
|
||||||
|
path.push(self.0.to_string());
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, img: DynamicImage) -> Result<DynamicImage, UploadError> {
|
||||||
|
if img.in_bounds(self.0, self.0) {
|
||||||
|
Ok(img.thumbnail(self.0, self.0))
|
||||||
|
} else {
|
||||||
|
Ok(img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Blur(f32);
|
||||||
|
|
||||||
|
impl Processor for Blur {
|
||||||
|
fn path(&self, mut path: PathBuf) -> PathBuf {
|
||||||
|
path.push("blur");
|
||||||
|
path.push(self.0.to_string());
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, img: DynamicImage) -> Result<DynamicImage, UploadError> {
|
||||||
|
Ok(img.blur(self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_chain(args: &[String]) -> Vec<Box<dyn Processor + Send>> {
|
||||||
|
args.into_iter().fold(Vec::new(), |mut acc, arg| {
|
||||||
|
match arg.to_lowercase().as_str() {
|
||||||
|
"identity" => acc.push(Box::new(Identity)),
|
||||||
|
other if other.starts_with("blur") => {
|
||||||
|
if let Ok(sigma) = other.trim_start_matches("blur").parse() {
|
||||||
|
acc.push(Box::new(Blur(sigma)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
if let Ok(size) = other.parse() {
|
||||||
|
acc.push(Box::new(Thumbnail(size)));
|
||||||
|
} else {
|
||||||
|
warn!("Unknown processor {}", other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
acc
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_path(
|
||||||
|
base: PathBuf,
|
||||||
|
args: &[Box<dyn Processor + Send>],
|
||||||
|
filename: String,
|
||||||
|
) -> PathBuf {
|
||||||
|
let mut path = args.iter().fold(base, |acc, processor| processor.path(acc));
|
||||||
|
|
||||||
|
path.push(filename);
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn process_image(
|
||||||
|
args: Vec<Box<dyn Processor + Send>>,
|
||||||
|
mut img: DynamicImage,
|
||||||
|
) -> Result<DynamicImage, UploadError> {
|
||||||
|
for processor in args.into_iter() {
|
||||||
|
img = web::block(move || processor.process(img)).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(img)
|
||||||
|
}
|
Loading…
Reference in a new issue