pict-rs/src/magick.rs
2024-02-23 22:12:19 -06:00

296 lines
8.7 KiB
Rust

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<ProcessError> 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<S, F, Fut>(
state: &State<S>,
process_args: Vec<String>,
input_format: ProcessableFormat,
format: ProcessableFormat,
quality: Option<u8>,
write_file: F,
) -> Result<ProcessRead, MagickError>
where
F: FnOnce(crate::file::File) -> Fut,
Fut: std::future::Future<Output = Result<crate::file::File, MagickError>>,
{
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::<OsStr>::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<S>(
state: &State<S>,
stream: LocalBoxStream<'static, std::io::Result<Bytes>>,
args: Vec<String>,
input_format: ProcessableFormat,
format: ProcessableFormat,
quality: Option<u8>,
) -> Result<ProcessRead, MagickError> {
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<S>(
state: &State<S>,
process_read: ProcessRead,
args: Vec<String>,
input_format: ProcessableFormat,
format: ProcessableFormat,
quality: Option<u8>,
) -> Result<ProcessRead, MagickError> {
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<PolicyDir>;
pub(crate) struct PolicyDir {
folder: TmpFolder,
}
impl PolicyDir {
pub(crate) async fn cleanup(self: Arc<Self>) -> std::io::Result<()> {
if let Some(this) = Arc::into_inner(self) {
this.folder.cleanup().await?;
}
Ok(())
}
}
impl AsRef<Path> 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<ArcPolicyDir> {
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#"<policymap>
<policy domain="resource" name="width" value="{width}P" />
<policy domain="resource" name="height" value="{height}P" />
<policy domain="resource" name="area" value="{area}P" />
<policy domain="resource" name="list-length" value="{frames}" />
<policy domain="resource" name="time" value="{timeout}" />
<policy domain="resource" name="memory" value="{memory}MiB" />
<policy domain="resource" name="map" value="{map}MiB" />
<policy domain="resource" name="disk" value="{disk}MiB" />
<policy domain="resource" name="file" value="768" />
<policy domain="resource" name="thread" value="2" />
<policy domain="cache" name="memory-map" value="anonymous"/>
<policy domain="cache" name="synchronize" value="true"/>
<policy domain="path" rights="none" pattern="@*" />
<policy domain="coder" rights="none" pattern="*" />
<policy domain="coder" rights="read | write" pattern="{{APNG,AVIF,GIF,HEIC,JPEG,JSON,JXL,PNG,RGB,RGBA,WEBP,MP4,WEBM,TMP,PAM}}" />
<policy domain="delegate" rights="none" pattern="*" />
<policy domain="delegate" rights="execute" pattern="FFMPEG" />
<policy domain="filter" rights="none" pattern="*" />
<policy domain="module" rights="none" pattern="*" />
<policy domain="module" rights="read | write" pattern="{{APNG,AVIF,GIF,HEIC,JPEG,JSON,JXL,PNG,RGB,RGBA,WEBP,TMP,PAM,PNM,VIDEO}}" />
<!-- indirect reads not permitted -->
<policy domain="system" name="max-memory-request" value="256MiB"/>
<policy domain="system" name="memory-map" value="anonymous"/>
<policy domain="system" name="precision" value="6" />
</policymap>"#
)
}