diff --git a/src/error.rs b/src/error.rs index d53b3bd..5f5623e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -55,6 +55,9 @@ pub enum UploadError { #[error("Unable to send request, {0}")] SendRequest(String), + + #[error("No filename provided in request")] + MissingFilename, } impl From for UploadError { @@ -96,7 +99,7 @@ impl ResponseError for UploadError { UploadError::NoFiles | UploadError::ContentType(_) | UploadError::Upload(_) => { StatusCode::BAD_REQUEST } - UploadError::MissingAlias => StatusCode::NOT_FOUND, + UploadError::MissingAlias | UploadError::MissingFilename => StatusCode::NOT_FOUND, UploadError::InvalidToken => StatusCode::FORBIDDEN, _ => StatusCode::INTERNAL_SERVER_ERROR, } diff --git a/src/main.rs b/src/main.rs index 38e0f1c..575e1ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use structopt::StructOpt; mod config; mod error; +mod processor; mod upload_manager; use self::{config::Config, error::UploadError, upload_manager::UploadManager}; @@ -151,39 +152,23 @@ async fn delete( Ok(HttpResponse::NoContent().finish()) } -/// Serve original files +/// Serve files async fn serve( manager: web::Data, - alias: web::Path, + segments: web::Path, ) -> Result { - let filename = manager.from_alias(alias.into_inner()).await?; - let mut path = manager.image_dir(); - path.push(filename); + let mut segments: Vec = segments + .into_inner() + .split('/') + .map(|s| s.to_string()) + .collect(); + let alias = segments.pop().ok_or(UploadError::MissingFilename)?; - let ext = path - .extension() - .ok_or(UploadError::MissingExtension)? - .to_owned(); - let ext = from_ext(ext); + let chain = self::processor::build_chain(&segments); - let stream = actix_fs::read_to_stream(path).await?; - - Ok(srv_response(stream, ext)) -} - -/// Serve resized files -async fn serve_resized( - manager: web::Data, - path_entries: web::Path<(u32, String)>, -) -> Result { - use image::GenericImageView; - - let mut path = manager.image_dir(); - - let (size, alias) = path_entries.into_inner(); let name = manager.from_alias(alias).await?; - path.push(size.to_string()); - path.push(name.clone()); + let base = manager.image_dir(); + let path = self::processor::build_path(base, &chain, name.clone()); let ext = path .extension() @@ -194,7 +179,7 @@ async fn serve_resized( // If the thumbnail doesn't exist, we need to create it if let Err(e) = actix_fs::metadata(path.clone()).await { 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()); } @@ -212,17 +197,12 @@ async fn serve_resized( (img, format) }; - // return original image if resize target is larger - if !img.in_bounds(size, size) { - drop(img); - let stream = actix_fs::read_to_stream(original_path).await?; - return Ok(srv_response(stream, ext)); - } + let img = self::processor::process_image(chain, img).await?; // perform thumbnail operation in a blocking thread let img_bytes: bytes::Bytes = web::block(move || { 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> }) .await?; @@ -319,14 +299,11 @@ async fn main() -> Result<(), anyhow::Error> { .route(web::post().to(upload)), ) .service(web::resource("/download").route(web::get().to(download))) - .service(web::resource("/{filename}").route(web::get().to(serve))) .service( web::resource("/delete/{delete_token}/{filename}") .route(web::delete().to(delete)), ) - .service( - web::resource("/{size}/{filename}").route(web::get().to(serve_resized)), - ), + .service(web::resource("/{tail:.*}").route(web::get().to(serve))), ) }) .bind(config.bind_address())? diff --git a/src/processor.rs b/src/processor.rs new file mode 100644 index 0000000..a568519 --- /dev/null +++ b/src/processor.rs @@ -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; +} + +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 { + 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 { + 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 { + Ok(img.blur(self.0)) + } +} + +pub(crate) fn build_chain(args: &[String]) -> Vec> { + 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], + 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>, + mut img: DynamicImage, +) -> Result { + for processor in args.into_iter() { + img = web::block(move || processor.process(img)).await?; + } + + Ok(img) +}