use crate::{config::Format, stream::Process}; use actix_web::web::Bytes; use std::process::Stdio; use tokio::{ io::{AsyncRead, AsyncReadExt, AsyncWriteExt}, process::Command, }; #[derive(Debug, thiserror::Error)] pub(crate) enum MagickError { #[error("{0}")] IO(#[from] std::io::Error), #[error("Magick semaphore is closed")] Closed, #[error("Invalid format")] Format, } pub(crate) enum ValidInputType { Mp4, Gif, Png, Jpeg, Webp, } pub(crate) struct Details { pub(crate) mime_type: mime::Mime, pub(crate) width: usize, pub(crate) height: usize, } static MAX_CONVERSIONS: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); fn semaphore() -> &'static tokio::sync::Semaphore { MAX_CONVERSIONS .get_or_init(|| tokio::sync::Semaphore::new(num_cpus::get().saturating_sub(1).max(1))) } pub(crate) fn clear_metadata_bytes_read(input: Bytes) -> std::io::Result { let process = Process::spawn(Command::new("magick").args(["convert", "-", "-strip", "-"]))?; Ok(process.bytes_read(input).unwrap()) } pub(crate) fn clear_metadata_write_read( input: impl AsyncRead + Unpin + 'static, ) -> std::io::Result { let process = Process::spawn(Command::new("magick").args(["convert", "-", "-strip", "-"]))?; Ok(process.write_read(input).unwrap()) } pub(crate) async fn details_write_read( input: impl AsyncRead + Unpin + 'static, ) -> Result { let process = Process::spawn(Command::new("magick").args([ "identify", "-ping", "-format", "%w %h | %m\n", "-", ]))?; let mut reader = process.write_read(input).unwrap(); let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; let s = String::from_utf8_lossy(&bytes); parse_details(s) } pub(crate) fn convert_write_read( input: impl AsyncRead + Unpin + 'static, format: Format, ) -> std::io::Result { let process = Process::spawn(Command::new("magick").args([ "convert", "-", format!("{}:-", format.to_magick_format()).as_str(), ]))?; Ok(process.write_read(input).unwrap()) } pub(crate) fn clear_metadata_stream( input: S, ) -> std::io::Result>> where S: futures::stream::Stream> + Unpin + 'static, E: From + 'static, { let process = Process::spawn(Command::new("magick").args(["convert", "-", "-strip", "-"]))?; Ok(Box::pin(process.sink_stream(input).unwrap())) } pub(crate) fn convert_bytes_read( input: Bytes, format: Format, ) -> std::io::Result { let process = Process::spawn(Command::new("magick").args([ "convert", "-", format!("{}:-", format.to_magick_format()).as_str(), ]))?; Ok(process.bytes_read(input).unwrap()) } pub(crate) fn convert_stream( input: S, format: Format, ) -> std::io::Result>> where S: futures::stream::Stream> + Unpin + 'static, E: From + 'static, { let process = Process::spawn(Command::new("magick").args([ "convert", "-", format!("{}:-", format.to_magick_format()).as_str(), ]))?; Ok(Box::pin(process.sink_stream(input).unwrap())) } pub(crate) async fn details_stream(input: S) -> Result where S: futures::stream::Stream> + Unpin, E1: From, E2: From + From + From, { use futures::stream::StreamExt; let permit = semaphore().acquire().await.map_err(MagickError::from)?; let mut process = Process::spawn(Command::new("magick").args([ "identify", "-ping", "-format", "%w %h | %m\n", "-", ]))?; process.take_sink().unwrap().send(input).await?; let mut stream = process.take_stream().unwrap(); let mut buf = actix_web::web::BytesMut::new(); while let Some(res) = stream.next().await { let bytes = res?; buf.extend_from_slice(&bytes); } drop(permit); let s = String::from_utf8_lossy(&buf); Ok(parse_details(s)?) } pub(crate) async fn details

(file: P) -> Result where P: AsRef, { let permit = semaphore().acquire().await?; let output = Command::new("magick") .args([&"identify", &"-ping", &"-format", &"%w %h | %m\n"]) .arg(&file.as_ref()) .output() .await?; drop(permit); let s = String::from_utf8_lossy(&output.stdout); parse_details(s) } fn parse_details(s: std::borrow::Cow<'_, str>) -> Result { let mut lines = s.lines(); let first = lines.next().ok_or_else(|| MagickError::Format)?; let mut segments = first.split('|'); let dimensions = segments.next().ok_or_else(|| MagickError::Format)?.trim(); tracing::debug!("dimensions: {}", dimensions); let mut dims = dimensions.split(' '); let width = dims .next() .ok_or_else(|| MagickError::Format)? .trim() .parse()?; let height = dims .next() .ok_or_else(|| MagickError::Format)? .trim() .parse()?; let format = segments.next().ok_or_else(|| MagickError::Format)?.trim(); tracing::debug!("format: {}", format); if !lines.all(|item| item.ends_with(format)) { return Err(MagickError::Format); } let mime_type = match format { "MP4" => crate::validate::video_mp4(), "GIF" => mime::IMAGE_GIF, "PNG" => mime::IMAGE_PNG, "JPEG" => mime::IMAGE_JPEG, "WEBP" => crate::validate::image_webp(), _ => return Err(MagickError::Format), }; Ok(Details { mime_type, width, height, }) } pub(crate) async fn input_type_bytes(mut input: Bytes) -> Result { let permit = semaphore().acquire().await.map_err(MagickError::from)?; let mut child = Command::new("magick") .args(["identify", "-ping", "-format", "%m\n", "-"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; let mut stdin = child.stdin.take().unwrap(); let mut stdout = child.stdout.take().unwrap(); stdin.write_all_buf(&mut input).await?; let mut vec = Vec::new(); stdout.read_to_end(&mut vec).await?; drop(stdin); child.wait().await?; drop(permit); let s = String::from_utf8_lossy(&vec); parse_input_type(s) } pub(crate) async fn input_type_stream(input: S) -> Result where S: futures::stream::Stream> + Unpin, E1: From, E2: From + From + From, { use futures::stream::StreamExt; let permit = semaphore().acquire().await.map_err(MagickError::from)?; let mut process = Process::spawn(Command::new("magick").args(["identify", "-ping", "-format", "%m\n", "-"]))?; process.take_sink().unwrap().send(input).await?; let mut stream = process.take_stream().unwrap(); let mut buf = actix_web::web::BytesMut::new(); while let Some(res) = stream.next().await { let bytes = res?; buf.extend_from_slice(&bytes); } drop(permit); let s = String::from_utf8_lossy(&buf); Ok(parse_input_type(s)?) } fn parse_input_type(s: std::borrow::Cow<'_, str>) -> Result { let mut lines = s.lines(); let first = lines.next(); let opt = lines.fold(first, |acc, item| match acc { Some(prev) if prev == item => Some(prev), _ => None, }); match opt { Some("MP4") => Ok(ValidInputType::Mp4), Some("GIF") => Ok(ValidInputType::Gif), Some("PNG") => Ok(ValidInputType::Png), Some("JPEG") => Ok(ValidInputType::Jpeg), Some("WEBP") => Ok(ValidInputType::Webp), _ => Err(MagickError::Format), } } pub(crate) fn process_image_stream( input: S, args: Vec, format: Format, ) -> std::io::Result>> where S: futures::stream::Stream> + Unpin + 'static, E: From + 'static, { let process = Process::spawn( Command::new("magick") .args([&"convert", &"-"]) .args(args) .arg(format!("{}:-", format.to_magick_format())), )?; Ok(Box::pin(process.sink_stream(input).unwrap())) } impl From for MagickError { fn from(_: tokio::sync::AcquireError) -> MagickError { MagickError::Closed } } impl From for MagickError { fn from(_: std::num::ParseIntError) -> MagickError { MagickError::Format } }