remote-image-compression/src/main.rs
2025-03-14 22:01:03 +11:00

168 lines
4.7 KiB
Rust

use std::{
io::Read,
os::unix::fs::MetadataExt,
process::Command,
};
use actix_multipart::form::{MultipartForm, tempfile::TempFile};
use actix_web::{
App, Either, Error, HttpResponse, HttpServer, Responder, get, post,
};
use anyhow::format_err;
use tempfile::NamedTempFile;
use tracing::{Level, event, instrument};
// use serde::Deserialize;
// #[derive(Debug, Deserialize)]
// struct Metadata {
// name: String,
// }
type _HandledResult = Either<HttpResponse, anyhow::Result<(), Error>>;
#[derive(Debug, MultipartForm)]
struct UploadImageForm {
#[multipart(limit = "100MB")]
file: TempFile,
// json: MpJson<Metadata>,
}
#[instrument]
#[get("/")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello world!")
}
#[instrument]
#[post("/echo-image")]
async fn echo_image(MultipartForm(form): MultipartForm<UploadImageForm>) -> impl Responder {
match is_file_image(&form.file.content_type) {
Some(x) => return x,
None => {}
}
let mut reader = std::io::BufReader::new(form.file.file.as_file());
let file_contents = &mut vec![];
let _ = reader.read_to_end(file_contents);
return HttpResponse::Ok()
.content_type(form.file.content_type.unwrap())
.body(file_contents.clone());
}
#[instrument]
#[post("/jpeg")]
async fn jpeg(MultipartForm(form): MultipartForm<UploadImageForm>) -> impl Responder {
match is_file_image(&form.file.content_type) {
Some(x) => return x,
None => {}
}
let mut reader = std::io::BufReader::new(form.file.file.as_file());
let input_file_contents = &mut vec![];
let _ = reader.read_to_end(input_file_contents);
drop(reader);
// let img = ImageReader::new(Cursor::new(input_file_contents)).with_guessed_format().unwrap().decode().unwrap();
let jpeg = tempfile::NamedTempFile::new().unwrap();
let status = encode_jpeg(&form.file.file, &jpeg, Some(5)).await;
if status.is_err() {
event!(Level::ERROR, "jpeg compression failed:\n{:?}", status.unwrap());
return HttpResponse::InternalServerError().body("Compression failed");
}
let mut reader = std::io::BufReader::new(&jpeg);
let output_file_contents = &mut vec![];
let _ = reader.read_to_end(output_file_contents);
let size_percentage = jpeg.as_file().metadata().unwrap().size() * 100 / form.file.size as u64;
event!(
Level::INFO,
"{} is now {}% of origional size as an {}!",
&form.file.content_type.unwrap(),
size_percentage,
"image/jpeg"
);
return HttpResponse::Ok()
.content_type(mime::IMAGE_JPEG)
.append_header((
"content-disposition",
format!("filename=\"{}.jpeg\"", form.file.file_name.unwrap()),
))
.body(output_file_contents.clone());
}
#[instrument]
async fn encode_jpeg(input: &NamedTempFile, output: &NamedTempFile, quality: Option<u8>) -> Result<(), anyhow::Error> {
let jpeg_quality = match quality {
Some(x) => {
if x > 100 {
return Err(format_err!("Invalid quality value"));
} else {
x
}
}
None => 75,
};
let jpeg_command = format!(
"magick {} ppm:- | cjpeg -outfile {} -quality {}",
input.path().display(),
output.path().display(),
jpeg_quality
);
let optimise_command = format!(
"jpegoptim {}",
output.path().display()
);
let _output = Command::new("sh")
.arg("-c")
.arg(jpeg_command)
.output()
.expect("failed to execute process");
// event!(Level::INFO, "{:?}", _output);
let _output = Command::new("sh")
.arg("-c")
.arg(optimise_command)
.output()
.expect("failed to execute process");
event!(Level::INFO, "{:?}", _output);
Ok(())
}
/// Returns `None` if file content_type starts with 'image' otherwise returns with an error response to serve to client
#[instrument]
fn is_file_image(input_mime: &Option<mime::Mime>) -> Option<HttpResponse> {
if input_mime.is_none() {
return Some(HttpResponse::BadRequest().body("Bad file content type?"));
}
// This long ass line is just to get the first half of the content type. with 'image/jpeg' it would produce 'image' and then get cast it as a str for comparison
if input_mime.clone().unwrap().type_().as_str() != "image" {
return Some(HttpResponse::BadRequest().body("File uploaded in not a supported format!"));
}
return None;
}
#[actix_web::main]
#[instrument]
async fn main() -> std::io::Result<()> {
tracing_subscriber::fmt::init();
HttpServer::new(|| App::new().service(hello).service(echo_image).service(jpeg))
.bind(("127.0.0.1", 3970))?
.run()
.await
}