use std::{ffi::OsStr, ops::Deref, path::Path, sync::Arc}; use actix_web::web::Bytes; use crate::{ config::Media, error_code::ErrorCode, formats::ProcessableFormat, process::{Process, ProcessError, ProcessRead}, state::State, stream::LocalBoxStream, tmp_file::{TmpDir, TmpFolder}, }; pub(crate) const MAGICK_TEMPORARY_PATH: &str = "MAGICK_TEMPORARY_PATH"; pub(crate) const MAGICK_CONFIGURE_PATH: &str = "MAGICK_CONFIGURE_PATH"; #[derive(Debug, thiserror::Error)] pub(crate) enum MagickError { #[error("Error in imagemagick process")] Process(#[source] ProcessError), #[error("Invalid output format: {0}")] Json(String, #[source] serde_json::Error), #[error("Error writing bytes")] Write(#[source] std::io::Error), #[error("Error creating file")] CreateFile(#[source] std::io::Error), #[error("Error creating directory")] CreateDir(#[source] crate::store::file_store::FileError), #[error("Error creating temporary directory")] CreateTemporaryDirectory(#[source] std::io::Error), #[error("Error closing file")] CloseFile(#[source] std::io::Error), #[error("Error in metadata discovery")] Discover(#[source] crate::discover::DiscoverError), #[error("Invalid media file provided")] CommandFailed(ProcessError), #[error("Error cleaning up after command")] Cleanup(#[source] std::io::Error), #[error("Command output is empty")] Empty, } impl From for MagickError { fn from(value: ProcessError) -> Self { match value { e @ ProcessError::Status(_, _) => Self::CommandFailed(e), otherwise => Self::Process(otherwise), } } } impl MagickError { pub(crate) const fn error_code(&self) -> ErrorCode { match self { Self::CommandFailed(_) => ErrorCode::COMMAND_FAILURE, Self::Process(e) => e.error_code(), Self::Json(_, _) | Self::Write(_) | Self::CreateFile(_) | Self::CreateDir(_) | Self::CreateTemporaryDirectory(_) | Self::CloseFile(_) | Self::Discover(_) | Self::Cleanup(_) | Self::Empty => ErrorCode::COMMAND_ERROR, } } pub(crate) fn is_client_error(&self) -> bool { // Failing validation or imagemagick bailing probably means bad input matches!( self, Self::CommandFailed(_) | Self::Process(ProcessError::Timeout(_)) ) } } async fn process_image( state: &State, process_args: Vec, input_format: ProcessableFormat, format: ProcessableFormat, quality: Option, write_file: F, ) -> Result where F: FnOnce(crate::file::File) -> Fut, Fut: std::future::Future>, { let temporary_path = state .tmp_dir .tmp_folder() .await .map_err(MagickError::CreateTemporaryDirectory)?; let input_file = state.tmp_dir.tmp_file(None); crate::store::file_store::safe_create_parent(&input_file) .await .map_err(MagickError::CreateDir)?; let tmp_one = crate::file::File::create(&input_file) .await .map_err(MagickError::CreateFile)?; let tmp_one = (write_file)(tmp_one).await?; tmp_one.close().await.map_err(MagickError::CloseFile)?; let input_arg = [ input_format.magick_format().as_ref(), input_file.as_os_str(), ] .join(":".as_ref()); let output_arg = format!("{}:-", format.magick_format()); let quality = quality.map(|q| q.to_string()); let len = 3 + if input_format.coalesce() { 1 } else { 0 } + if quality.is_some() { 1 } else { 0 } + process_args.len(); let mut args: Vec<&OsStr> = Vec::with_capacity(len); args.push("convert".as_ref()); args.push(&input_arg); if input_format.coalesce() { args.push("-coalesce".as_ref()); } args.extend(process_args.iter().map(AsRef::::as_ref)); if let Some(quality) = &quality { args.extend(["-quality".as_ref(), quality.as_ref()] as [&OsStr; 2]); } args.push(output_arg.as_ref()); let envs = [ (MAGICK_TEMPORARY_PATH, temporary_path.as_os_str()), (MAGICK_CONFIGURE_PATH, state.policy_dir.as_os_str()), ]; let reader = Process::run("magick", &args, &envs, state.config.media.process_timeout)? .read() .add_extras(input_file) .add_extras(temporary_path); Ok(reader) } pub(crate) async fn process_image_stream_read( state: &State, stream: LocalBoxStream<'static, std::io::Result>, args: Vec, input_format: ProcessableFormat, format: ProcessableFormat, quality: Option, ) -> Result { process_image( state, args, input_format, format, quality, |mut tmp_file| async move { tmp_file .write_from_stream(stream) .await .map_err(MagickError::Write)?; Ok(tmp_file) }, ) .await } pub(crate) async fn process_image_process_read( state: &State, process_read: ProcessRead, args: Vec, input_format: ProcessableFormat, format: ProcessableFormat, quality: Option, ) -> Result { process_image( state, args, input_format, format, quality, |mut tmp_file| async move { process_read .with_stdout(|stdout| async { tmp_file .write_from_async_read(stdout) .await .map_err(MagickError::Write) }) .await??; Ok(tmp_file) }, ) .await } pub(crate) type ArcPolicyDir = Arc; pub(crate) struct PolicyDir { folder: TmpFolder, } impl PolicyDir { pub(crate) async fn cleanup(self: Arc) -> std::io::Result<()> { if let Some(this) = Arc::into_inner(self) { this.folder.cleanup().await?; } Ok(()) } } impl AsRef for PolicyDir { fn as_ref(&self) -> &Path { &self.folder } } impl Deref for PolicyDir { type Target = Path; fn deref(&self) -> &Self::Target { &self.folder } } pub(super) async fn write_magick_policy( media: &Media, tmp_dir: &TmpDir, ) -> std::io::Result { let folder = tmp_dir.tmp_folder().await?; let file = folder.join("policy.xml"); let res = tokio::fs::write(&file, generate_policy(media)).await; if let Err(e) = res { folder.cleanup().await?; return Err(e); } Ok(Arc::new(PolicyDir { folder })) } fn generate_policy(media: &Media) -> String { let width = media.magick.max_width; let height = media.magick.max_height; let area = media.magick.max_area; let memory = media.magick.memory; let map = media.magick.map; let disk = media.magick.disk; let frames = media.animation.max_frame_count; let timeout = media.process_timeout; format!( r#" "# ) }