2021-09-14 01:22:42 +00:00
|
|
|
use crate::{
|
2022-03-28 04:27:07 +00:00
|
|
|
config::ImageFormat,
|
2021-09-14 01:22:42 +00:00
|
|
|
error::{Error, UploadError},
|
2021-10-21 00:00:41 +00:00
|
|
|
process::Process,
|
2022-03-26 21:49:23 +00:00
|
|
|
repo::Alias,
|
2021-10-23 04:48:56 +00:00
|
|
|
store::Store,
|
2021-09-14 01:22:42 +00:00
|
|
|
};
|
2021-09-04 00:53:53 +00:00
|
|
|
use actix_web::web::Bytes;
|
|
|
|
use tokio::{
|
2021-09-09 19:16:12 +00:00
|
|
|
io::{AsyncRead, AsyncReadExt},
|
2021-09-04 00:53:53 +00:00
|
|
|
process::Command,
|
|
|
|
};
|
2021-09-14 01:22:42 +00:00
|
|
|
use tracing::instrument;
|
2021-08-28 22:15:14 +00:00
|
|
|
|
2022-03-26 21:49:23 +00:00
|
|
|
pub(crate) fn details_hint(alias: &Alias) -> Option<ValidInputType> {
|
|
|
|
let ext = alias.extension()?;
|
|
|
|
if ext.ends_with(".mp4") {
|
2021-10-23 04:48:56 +00:00
|
|
|
Some(ValidInputType::Mp4)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-23 17:35:07 +00:00
|
|
|
pub(crate) fn image_webp() -> mime::Mime {
|
|
|
|
"image/webp".parse().unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn video_mp4() -> mime::Mime {
|
|
|
|
"video/mp4".parse().unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
2021-08-29 01:37:53 +00:00
|
|
|
pub(crate) enum ValidInputType {
|
|
|
|
Mp4,
|
|
|
|
Gif,
|
2021-08-28 22:15:14 +00:00
|
|
|
Png,
|
2021-08-29 01:37:53 +00:00
|
|
|
Jpeg,
|
2021-08-28 22:15:14 +00:00
|
|
|
Webp,
|
|
|
|
}
|
|
|
|
|
2021-10-23 04:48:56 +00:00
|
|
|
impl ValidInputType {
|
2021-10-28 04:06:03 +00:00
|
|
|
fn as_str(&self) -> &'static str {
|
2021-10-23 04:48:56 +00:00
|
|
|
match self {
|
|
|
|
Self::Mp4 => "MP4",
|
|
|
|
Self::Gif => "GIF",
|
|
|
|
Self::Png => "PNG",
|
|
|
|
Self::Jpeg => "JPEG",
|
|
|
|
Self::Webp => "WEBP",
|
|
|
|
}
|
|
|
|
}
|
2021-10-23 17:35:07 +00:00
|
|
|
|
2021-10-28 04:06:03 +00:00
|
|
|
pub(crate) fn as_ext(&self) -> &'static str {
|
2021-10-23 17:35:07 +00:00
|
|
|
match self {
|
|
|
|
Self::Mp4 => ".mp4",
|
|
|
|
Self::Gif => ".gif",
|
|
|
|
Self::Png => ".png",
|
|
|
|
Self::Jpeg => ".jpeg",
|
|
|
|
Self::Webp => ".webp",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-23 19:14:12 +00:00
|
|
|
fn is_mp4(&self) -> bool {
|
|
|
|
matches!(self, Self::Mp4)
|
|
|
|
}
|
|
|
|
|
2022-03-28 04:27:07 +00:00
|
|
|
pub(crate) fn from_format(format: ImageFormat) -> Self {
|
2021-10-23 17:35:07 +00:00
|
|
|
match format {
|
2022-03-28 04:27:07 +00:00
|
|
|
ImageFormat::Jpeg => ValidInputType::Jpeg,
|
|
|
|
ImageFormat::Png => ValidInputType::Png,
|
|
|
|
ImageFormat::Webp => ValidInputType::Webp,
|
2021-10-23 17:35:07 +00:00
|
|
|
}
|
|
|
|
}
|
2021-10-23 04:48:56 +00:00
|
|
|
}
|
|
|
|
|
2021-10-14 01:31:40 +00:00
|
|
|
#[derive(Debug)]
|
2021-08-29 03:05:49 +00:00
|
|
|
pub(crate) struct Details {
|
|
|
|
pub(crate) mime_type: mime::Mime,
|
|
|
|
pub(crate) width: usize,
|
|
|
|
pub(crate) height: usize,
|
|
|
|
}
|
|
|
|
|
2022-04-07 02:40:49 +00:00
|
|
|
#[tracing::instrument(name = "Clear Metadata", skip(input))]
|
2021-09-04 00:53:53 +00:00
|
|
|
pub(crate) fn clear_metadata_bytes_read(input: Bytes) -> std::io::Result<impl AsyncRead + Unpin> {
|
2021-09-14 01:22:42 +00:00
|
|
|
let process = Process::run("magick", &["convert", "-", "-strip", "-"])?;
|
2021-09-04 00:53:53 +00:00
|
|
|
|
2022-04-07 02:40:49 +00:00
|
|
|
Ok(process.bytes_read(input))
|
2021-09-04 00:53:53 +00:00
|
|
|
}
|
|
|
|
|
2022-04-07 02:40:49 +00:00
|
|
|
#[tracing::instrument(name = "Convert", skip(input))]
|
2021-10-23 19:14:12 +00:00
|
|
|
pub(crate) fn convert_bytes_read(
|
|
|
|
input: Bytes,
|
2022-03-28 04:27:07 +00:00
|
|
|
format: ImageFormat,
|
2021-10-23 19:14:12 +00:00
|
|
|
) -> std::io::Result<impl AsyncRead + Unpin> {
|
|
|
|
let process = Process::run(
|
|
|
|
"magick",
|
|
|
|
&[
|
|
|
|
"convert",
|
|
|
|
"-",
|
|
|
|
"-strip",
|
2021-10-28 04:06:03 +00:00
|
|
|
format!("{}:-", format.as_magick_format()).as_str(),
|
2021-10-23 19:14:12 +00:00
|
|
|
],
|
|
|
|
)?;
|
|
|
|
|
2022-04-07 02:40:49 +00:00
|
|
|
Ok(process.bytes_read(input))
|
2021-10-23 19:14:12 +00:00
|
|
|
}
|
|
|
|
|
2021-10-14 01:31:40 +00:00
|
|
|
#[instrument(name = "Getting details from input bytes", skip(input))]
|
2021-10-23 04:48:56 +00:00
|
|
|
pub(crate) async fn details_bytes(
|
|
|
|
input: Bytes,
|
|
|
|
hint: Option<ValidInputType>,
|
|
|
|
) -> Result<Details, Error> {
|
2021-10-23 19:14:12 +00:00
|
|
|
if hint.as_ref().map(|h| h.is_mp4()).unwrap_or(false) {
|
|
|
|
let input_file = crate::tmp_file::tmp_file(Some(".mp4"));
|
|
|
|
let input_file_str = input_file.to_str().ok_or(UploadError::Path)?;
|
|
|
|
crate::store::file_store::safe_create_parent(&input_file).await?;
|
|
|
|
|
|
|
|
let mut tmp_one = crate::file::File::create(&input_file).await?;
|
|
|
|
tmp_one.write_from_bytes(input).await?;
|
|
|
|
tmp_one.close().await?;
|
|
|
|
|
|
|
|
return details_file(input_file_str).await;
|
|
|
|
}
|
|
|
|
|
2021-10-23 04:48:56 +00:00
|
|
|
let last_arg = if let Some(expected_format) = hint {
|
2021-10-28 04:06:03 +00:00
|
|
|
format!("{}:-", expected_format.as_str())
|
2021-10-23 04:48:56 +00:00
|
|
|
} else {
|
|
|
|
"-".to_owned()
|
|
|
|
};
|
|
|
|
|
2021-09-14 01:22:42 +00:00
|
|
|
let process = Process::run(
|
|
|
|
"magick",
|
2021-10-23 04:48:56 +00:00
|
|
|
&["identify", "-ping", "-format", "%w %h | %m\n", &last_arg],
|
2021-09-14 01:22:42 +00:00
|
|
|
)?;
|
2021-09-04 00:53:53 +00:00
|
|
|
|
2022-04-07 02:40:49 +00:00
|
|
|
let mut reader = process.bytes_read(input);
|
2021-09-04 00:53:53 +00:00
|
|
|
|
|
|
|
let mut bytes = Vec::new();
|
|
|
|
reader.read_to_end(&mut bytes).await?;
|
|
|
|
let s = String::from_utf8_lossy(&bytes);
|
|
|
|
|
2021-09-04 17:42:40 +00:00
|
|
|
parse_details(s)
|
2021-08-31 02:19:47 +00:00
|
|
|
}
|
2021-08-29 19:16:55 +00:00
|
|
|
|
2022-03-27 01:45:12 +00:00
|
|
|
#[tracing::instrument(skip(store))]
|
2022-04-01 21:51:12 +00:00
|
|
|
pub(crate) async fn details_store<S: Store + 'static>(
|
2021-10-23 04:48:56 +00:00
|
|
|
store: S,
|
|
|
|
identifier: S::Identifier,
|
2021-10-23 19:14:12 +00:00
|
|
|
hint: Option<ValidInputType>,
|
2022-03-27 01:45:12 +00:00
|
|
|
) -> Result<Details, Error> {
|
2021-10-23 19:14:12 +00:00
|
|
|
if hint.as_ref().map(|h| h.is_mp4()).unwrap_or(false) {
|
|
|
|
let input_file = crate::tmp_file::tmp_file(Some(".mp4"));
|
|
|
|
let input_file_str = input_file.to_str().ok_or(UploadError::Path)?;
|
|
|
|
crate::store::file_store::safe_create_parent(&input_file).await?;
|
|
|
|
|
|
|
|
let mut tmp_one = crate::file::File::create(&input_file).await?;
|
|
|
|
tmp_one
|
|
|
|
.write_from_stream(store.to_stream(&identifier, None, None).await?)
|
|
|
|
.await?;
|
|
|
|
tmp_one.close().await?;
|
|
|
|
|
|
|
|
return details_file(input_file_str).await;
|
|
|
|
}
|
|
|
|
|
|
|
|
let last_arg = if let Some(expected_format) = hint {
|
2021-10-28 04:06:03 +00:00
|
|
|
format!("{}:-", expected_format.as_str())
|
2021-10-23 04:48:56 +00:00
|
|
|
} else {
|
|
|
|
"-".to_owned()
|
|
|
|
};
|
|
|
|
|
|
|
|
let process = Process::run(
|
|
|
|
"magick",
|
|
|
|
&["identify", "-ping", "-format", "%w %h | %m\n", &last_arg],
|
|
|
|
)?;
|
|
|
|
|
2022-04-07 02:40:49 +00:00
|
|
|
let mut reader = process.store_read(store, identifier);
|
2021-09-14 01:22:42 +00:00
|
|
|
|
2021-10-23 04:48:56 +00:00
|
|
|
let mut output = Vec::new();
|
|
|
|
reader.read_to_end(&mut output).await?;
|
2021-08-29 03:05:49 +00:00
|
|
|
|
2021-10-23 04:48:56 +00:00
|
|
|
let s = String::from_utf8_lossy(&output);
|
2021-10-23 19:14:12 +00:00
|
|
|
|
|
|
|
parse_details(s)
|
|
|
|
}
|
|
|
|
|
2022-03-27 01:45:12 +00:00
|
|
|
#[tracing::instrument]
|
2021-10-23 19:14:12 +00:00
|
|
|
pub(crate) async fn details_file(path_str: &str) -> Result<Details, Error> {
|
|
|
|
let process = Process::run(
|
|
|
|
"magick",
|
2021-10-28 04:06:03 +00:00
|
|
|
&["identify", "-ping", "-format", "%w %h | %m\n", path_str],
|
2021-10-23 19:14:12 +00:00
|
|
|
)?;
|
|
|
|
|
2022-04-07 02:40:49 +00:00
|
|
|
let mut reader = process.read();
|
2021-10-23 19:14:12 +00:00
|
|
|
|
|
|
|
let mut output = Vec::new();
|
|
|
|
reader.read_to_end(&mut output).await?;
|
|
|
|
tokio::fs::remove_file(path_str).await?;
|
|
|
|
|
|
|
|
let s = String::from_utf8_lossy(&output);
|
2021-08-29 03:05:49 +00:00
|
|
|
|
2021-08-31 02:19:47 +00:00
|
|
|
parse_details(s)
|
|
|
|
}
|
|
|
|
|
2021-09-14 01:22:42 +00:00
|
|
|
fn parse_details(s: std::borrow::Cow<'_, str>) -> Result<Details, Error> {
|
2021-08-29 03:05:49 +00:00
|
|
|
let mut lines = s.lines();
|
2021-09-14 01:22:42 +00:00
|
|
|
let first = lines.next().ok_or(UploadError::UnsupportedFormat)?;
|
2021-08-29 03:05:49 +00:00
|
|
|
|
|
|
|
let mut segments = first.split('|');
|
|
|
|
|
2021-09-14 01:22:42 +00:00
|
|
|
let dimensions = segments
|
|
|
|
.next()
|
|
|
|
.ok_or(UploadError::UnsupportedFormat)?
|
|
|
|
.trim();
|
2021-08-29 03:05:49 +00:00
|
|
|
tracing::debug!("dimensions: {}", dimensions);
|
|
|
|
let mut dims = dimensions.split(' ');
|
2021-09-14 01:22:42 +00:00
|
|
|
let width = dims
|
|
|
|
.next()
|
|
|
|
.ok_or(UploadError::UnsupportedFormat)?
|
|
|
|
.trim()
|
|
|
|
.parse()
|
|
|
|
.map_err(|_| UploadError::UnsupportedFormat)?;
|
|
|
|
let height = dims
|
|
|
|
.next()
|
|
|
|
.ok_or(UploadError::UnsupportedFormat)?
|
|
|
|
.trim()
|
|
|
|
.parse()
|
|
|
|
.map_err(|_| UploadError::UnsupportedFormat)?;
|
|
|
|
|
|
|
|
let format = segments
|
|
|
|
.next()
|
|
|
|
.ok_or(UploadError::UnsupportedFormat)?
|
|
|
|
.trim();
|
2021-08-29 03:05:49 +00:00
|
|
|
tracing::debug!("format: {}", format);
|
|
|
|
|
|
|
|
if !lines.all(|item| item.ends_with(format)) {
|
2021-09-14 01:22:42 +00:00
|
|
|
return Err(UploadError::UnsupportedFormat.into());
|
2021-08-29 03:05:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let mime_type = match format {
|
2021-10-23 17:35:07 +00:00
|
|
|
"MP4" => video_mp4(),
|
2021-08-29 03:05:49 +00:00
|
|
|
"GIF" => mime::IMAGE_GIF,
|
|
|
|
"PNG" => mime::IMAGE_PNG,
|
|
|
|
"JPEG" => mime::IMAGE_JPEG,
|
2021-10-23 17:35:07 +00:00
|
|
|
"WEBP" => image_webp(),
|
2021-09-14 01:22:42 +00:00
|
|
|
_ => return Err(UploadError::UnsupportedFormat.into()),
|
2021-08-29 03:05:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(Details {
|
|
|
|
mime_type,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-14 01:31:40 +00:00
|
|
|
#[instrument(name = "Getting input type from bytes", skip(input))]
|
2021-09-14 01:22:42 +00:00
|
|
|
pub(crate) async fn input_type_bytes(input: Bytes) -> Result<ValidInputType, Error> {
|
2021-10-23 04:48:56 +00:00
|
|
|
details_bytes(input, None).await?.validate_input()
|
2021-08-28 22:15:14 +00:00
|
|
|
}
|
|
|
|
|
2021-10-23 04:48:56 +00:00
|
|
|
#[instrument(name = "Spawning process command")]
|
2022-04-01 21:51:12 +00:00
|
|
|
pub(crate) fn process_image_store_read<S: Store + 'static>(
|
2021-10-23 04:48:56 +00:00
|
|
|
store: S,
|
|
|
|
identifier: S::Identifier,
|
2021-08-28 22:15:14 +00:00
|
|
|
args: Vec<String>,
|
2022-03-28 04:27:07 +00:00
|
|
|
format: ImageFormat,
|
2021-09-04 17:42:40 +00:00
|
|
|
) -> std::io::Result<impl AsyncRead + Unpin> {
|
2021-09-14 01:22:42 +00:00
|
|
|
let command = "magick";
|
|
|
|
let convert_args = ["convert", "-"];
|
2021-10-28 04:06:03 +00:00
|
|
|
let last_arg = format!("{}:-", format.as_magick_format());
|
2021-09-14 01:22:42 +00:00
|
|
|
|
2021-09-04 00:53:53 +00:00
|
|
|
let process = Process::spawn(
|
2021-09-14 01:22:42 +00:00
|
|
|
Command::new(command)
|
|
|
|
.args(convert_args)
|
2021-08-31 02:19:47 +00:00
|
|
|
.args(args)
|
2021-09-14 01:22:42 +00:00
|
|
|
.arg(last_arg),
|
2021-08-31 02:19:47 +00:00
|
|
|
)?;
|
|
|
|
|
2022-04-07 02:40:49 +00:00
|
|
|
Ok(process.store_read(store, identifier))
|
2021-08-26 02:46:11 +00:00
|
|
|
}
|
|
|
|
|
2021-09-09 19:16:12 +00:00
|
|
|
impl Details {
|
2021-10-14 01:31:40 +00:00
|
|
|
#[instrument(name = "Validating input type")]
|
2021-09-14 01:22:42 +00:00
|
|
|
fn validate_input(&self) -> Result<ValidInputType, Error> {
|
2022-03-28 04:27:07 +00:00
|
|
|
if self.width > crate::CONFIG.media.max_width
|
|
|
|
|| self.height > crate::CONFIG.media.max_height
|
|
|
|
|| self.width * self.height > crate::CONFIG.media.max_area
|
2021-10-28 04:06:03 +00:00
|
|
|
{
|
2021-09-14 01:22:42 +00:00
|
|
|
return Err(UploadError::Dimensions.into());
|
2021-09-09 19:16:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let input_type = match (self.mime_type.type_(), self.mime_type.subtype()) {
|
|
|
|
(mime::VIDEO, mime::MP4 | mime::MPEG) => ValidInputType::Mp4,
|
|
|
|
(mime::IMAGE, mime::GIF) => ValidInputType::Gif,
|
|
|
|
(mime::IMAGE, mime::PNG) => ValidInputType::Png,
|
|
|
|
(mime::IMAGE, mime::JPEG) => ValidInputType::Jpeg,
|
|
|
|
(mime::IMAGE, subtype) if subtype.as_str() == "webp" => ValidInputType::Webp,
|
2021-09-14 01:22:42 +00:00
|
|
|
_ => return Err(UploadError::UnsupportedFormat.into()),
|
2021-09-09 19:16:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(input_type)
|
|
|
|
}
|
|
|
|
}
|