mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-22 19:31:35 +00:00
It compiles and runs, but doesn't work
This commit is contained in:
parent
27451971a6
commit
2c22f2ee3a
18 changed files with 881 additions and 434 deletions
|
@ -18,14 +18,7 @@ targets = "info"
|
|||
path = "/mnt"
|
||||
|
||||
[media]
|
||||
max_width = 10000
|
||||
max_height = 10000
|
||||
max_area = 40000000
|
||||
max_file_size = 40
|
||||
max_frame_count = 900
|
||||
enable_silent_video = true
|
||||
enable_full_video = false
|
||||
video_codec = "vp9"
|
||||
filters = [
|
||||
"blur",
|
||||
"crop",
|
||||
|
@ -33,14 +26,30 @@ filters = [
|
|||
"resize",
|
||||
"thumbnail",
|
||||
]
|
||||
skip_validate_imports = false
|
||||
|
||||
[media.gif]
|
||||
max_width = 128
|
||||
max_height = 128
|
||||
max_area = 16384
|
||||
[media.image]
|
||||
max_width = 10000
|
||||
max_height = 10000
|
||||
max_area = 40000000
|
||||
max_file_size = 40
|
||||
|
||||
[media.animation]
|
||||
max_width = 256
|
||||
max_height = 256
|
||||
max_area = 65536
|
||||
max_file_size = 40
|
||||
max_frame_count = 100
|
||||
|
||||
[media.video]
|
||||
enable = true
|
||||
allow_audio = false
|
||||
max_width = 3840
|
||||
max_height = 3840
|
||||
max_area = 8294400
|
||||
max_file_size = 40
|
||||
max_frame_count = 900
|
||||
video_codec = "vp9"
|
||||
|
||||
[repo]
|
||||
type = "sled"
|
||||
path = "/mnt/sled-repo"
|
||||
|
|
229
pict-rs.toml
229
pict-rs.toml
|
@ -128,6 +128,11 @@ path = '/mnt'
|
|||
|
||||
## Media Processing Configuration
|
||||
[media]
|
||||
## Optional: max file size (in Megabytes)
|
||||
# environment variable: PICTRS__MEDIA__MAX_FILE_SIZE
|
||||
# default: 40
|
||||
max_file_size = 40
|
||||
|
||||
## Optional: preprocessing steps for uploaded images
|
||||
# environment variable: PICTRS__MEDIA__PREPROCESS_STEPS
|
||||
# default: empty
|
||||
|
@ -135,70 +140,35 @@ path = '/mnt'
|
|||
# This configuration is the same format as the process endpoint's query arguments
|
||||
preprocess_steps = 'crop=16x9&resize=1200&blur=0.2'
|
||||
|
||||
## Optional: max media width (in pixels)
|
||||
# environment variable: PICTRS__MEDIA__MAX_WIDTH
|
||||
# default: 10,000
|
||||
max_width = 10000
|
||||
|
||||
## Optional: max media height (in pixels)
|
||||
# environment variable: PICTRS__MEDIA__MAX_HEIGHT
|
||||
# default: 10,000
|
||||
max_height = 10000
|
||||
|
||||
## Optional: max media area (in pixels)
|
||||
# environment variable: PICTRS__MEDIA__MAX_AREA
|
||||
# default: 40,000,000
|
||||
max_area = 40000000
|
||||
|
||||
## Optional: max file size (in Megabytes)
|
||||
# environment variable: PICTRS__MEDIA__MAX_FILE_SIZE
|
||||
# default: 40
|
||||
max_file_size = 40
|
||||
|
||||
## Optional: max frame count
|
||||
# environment variable: PICTRS__MEDIA__MAX_FRAME_COUNT
|
||||
# default: # 900
|
||||
max_frame_count = 900
|
||||
|
||||
## Optional: enable GIF, MP4, and WEBM uploads (without sound)
|
||||
# environment variable: PICTRS__MEDIA__ENABLE_SILENT_VIDEO
|
||||
# default: true
|
||||
#
|
||||
# Set this to false to serve static images only
|
||||
enable_silent_video = true
|
||||
|
||||
## Optional: enable MP4, and WEBM uploads (with sound) and GIF (without sound)
|
||||
# environment variable: PICTRS__MEDIA__ENABLE_FULL_VIDEO
|
||||
# default: false
|
||||
enable_full_video = false
|
||||
|
||||
## Optional: set the default video codec
|
||||
# environment variable: PICTRS__MEDIA__VIDEO_CODEC
|
||||
# default: vp9
|
||||
#
|
||||
# available options: av1, h264, h265, vp8, vp9
|
||||
# this setting does nothing if video is not enabled
|
||||
video_codec = "vp9"
|
||||
|
||||
## Optional: set the default audio codec
|
||||
# environment variable: PICTRS__MEDIA__AUDIO_CODEC
|
||||
# default: empty
|
||||
#
|
||||
# available options: aac, opus, vorbis
|
||||
# The audio codec is automatically selected based on video codec, but can be overriden
|
||||
# av1, vp8, and vp9 map to opus
|
||||
# h264 and h265 map to aac
|
||||
# vorbis is not default for any codec
|
||||
# this setting does nothing if full video is not enabled
|
||||
audio_codec = "aac"
|
||||
|
||||
## Optional: set allowed filters for image processing
|
||||
# environment variable: PICTRS__MEDIA__FILTERS
|
||||
# default: ['blur', 'crop', 'identity', 'resize', 'thumbnail']
|
||||
filters = ['blur', 'crop', 'identity', 'resize', 'thumbnail']
|
||||
|
||||
## Optional: set file type for all uploads
|
||||
# environment variable: PICTRS__MEDIA__FORMAT
|
||||
|
||||
[media.image]
|
||||
## Optional: max media width (in pixels)
|
||||
# environment variable: PICTRS__MEDIA__IMAGE__MAX_WIDTH
|
||||
# default: 10,000
|
||||
max_width = 10000
|
||||
|
||||
## Optional: max media height (in pixels)
|
||||
# environment variable: PICTRS__MEDIA__IMAGE__MAX_HEIGHT
|
||||
# default: 10,000
|
||||
max_height = 10000
|
||||
|
||||
## Optional: max media area (in pixels)
|
||||
# environment variable: PICTRS__MEDIA__IMAGE__MAX_AREA
|
||||
# default: 40,000,000
|
||||
max_area = 40000000
|
||||
|
||||
## Optional: max file size (in Megabytes)
|
||||
# environment variable: PICTRS__MEDIA__IMAGE__MAX_FILE_SIZE
|
||||
# default: 40
|
||||
max_file_size = 40
|
||||
|
||||
## Optional: set file type for all images
|
||||
# environment variable: PICTRS__MEDIA__IMAGE__FORMAT
|
||||
# default: empty
|
||||
#
|
||||
# available options: avif, png, jpeg, jxl, webp
|
||||
|
@ -207,49 +177,126 @@ filters = ['blur', 'crop', 'identity', 'resize', 'thumbnail']
|
|||
# are stored in their original file type.
|
||||
format = "webp"
|
||||
|
||||
## Optional: whether to validate images uploaded through the `import` endpoint
|
||||
# environment variable: PICTRS__MEDIA__SKIP_VALIDATE_IMPORTS
|
||||
# default: false
|
||||
#
|
||||
# Set this to true if you want to avoid processing imported media
|
||||
skip_validate_imports = false
|
||||
|
||||
## Gif configuration
|
||||
[media.animation]
|
||||
## Optional: max animation width (in pixels)
|
||||
# environment variable: PICTRS__MEDIA__ANIMATION__MAX_WIDTH
|
||||
# default: 256
|
||||
#
|
||||
# Making any of these bounds 0 will disable gif uploads
|
||||
[media.gif]
|
||||
# Optional: Maximum width in pixels for uploaded gifs
|
||||
# environment variable: PICTRS__MEDIA__GIF__MAX_WIDTH
|
||||
# default: 128
|
||||
#
|
||||
# If a gif does not fit within this bound, it will either be transcoded to a video or rejected,
|
||||
# depending on whether video uploads are enabled
|
||||
max_width = 128
|
||||
# If an animation exceeds this value, it may be converted to a silent video
|
||||
max_width = 256
|
||||
|
||||
# Optional: Maximum height in pixels for uploaded gifs
|
||||
# environment variable: PICTRS__MEDIA__GIF__MAX_HEIGHT
|
||||
# default: 128
|
||||
## Optional: max animation height (in pixels)
|
||||
# environment variable: PICTRS__MEDIA__ANIMATION__MAX_HEIGHT
|
||||
# default: 256
|
||||
#
|
||||
# If a gif does not fit within this bound, it will either be transcoded to a video or rejected,
|
||||
# depending on whether video uploads are enabled
|
||||
max_height = 128
|
||||
# If an animation exceeds this value, it may be converted to a silent video
|
||||
max_height = 256
|
||||
|
||||
# Optional: Maximum area in pixels for uploaded gifs
|
||||
# environment variable: PICTRS__MEDIA__GIF__MAX_AREA
|
||||
# default: 16384 (128 * 128)
|
||||
## Optional: max animation area (in pixels)
|
||||
# environment variable: PICTRS__MEDIA__ANIMATION__MAX_AREA
|
||||
# default: 65,526
|
||||
#
|
||||
# If a gif does not fit within this bound, it will either be transcoded to a video or rejected,
|
||||
# depending on whether video uploads are enabled
|
||||
max_area = 16384
|
||||
# If an animation exceeds this value, it may be converted to a silent video
|
||||
max_area = 65536
|
||||
|
||||
# Optional: Maximum number of frames permitted in uploaded gifs
|
||||
# environment variable: PICTRS__MEDIA__GIF__MAX_FRAME_COUNT
|
||||
## Optional: max animation size (in Megabytes)
|
||||
# environment variable: PICTRS__MEDIA__ANIMATION__MAX_FILE_SIZE
|
||||
# default: 40
|
||||
#
|
||||
# If an animation exceeds this value, it may be converted to a silent video
|
||||
max_file_size = 40
|
||||
|
||||
## Optional: max frame count
|
||||
# environment variable: PICTRS__MEDIA__ANIMATION__MAX_FRAME_COUNT
|
||||
# default: 100
|
||||
#
|
||||
# If a gif does not fit within this bound, it will either be transcoded to a video or rejected,
|
||||
# depending on whether video uploads are enabled
|
||||
# If an animation exceeds this value, it may be converted to a silent video
|
||||
max_frame_count = 100
|
||||
|
||||
## Optional: set file type for all animations
|
||||
# environment variable: PICTRS__MEDIA__ANIMATION__FORMAT
|
||||
# default: empty
|
||||
#
|
||||
# available options: apng, avif, gif, webp
|
||||
# When set, all uploaded still images will be converted to this file type. For balancing quality vs
|
||||
# file size vs browser support, 'avif', 'jxl', and 'webp' should be considered. By default, images
|
||||
# are stored in their original file type.
|
||||
format = "webp"
|
||||
|
||||
|
||||
[media.video]
|
||||
## Optional: enable MP4 and WEBM uploads (without sound)
|
||||
# environment variable: PICTRS__MEDIA__VIDEO__ENABLE
|
||||
# default: true
|
||||
#
|
||||
# Set this to false to serve static images only
|
||||
enable = true
|
||||
|
||||
## Optional: enable Sound for MP4 and WEBM uploads
|
||||
# environment variable: PICTRS__MEDIA__VIDEO__ALLOW_AUDIO
|
||||
# default: false
|
||||
#
|
||||
# this setting does nothing if video is not enabled
|
||||
allow_audio = false
|
||||
|
||||
## Optional: max video width (in pixels)
|
||||
# environment variable: PICTRS__MEDIA__VIDEO__MAX_WIDTH
|
||||
# default: 3,840
|
||||
#
|
||||
# this setting does nothing if video is not enabled
|
||||
max_width = 3840
|
||||
|
||||
## Optional: max video height (in pixels)
|
||||
# environment variable: PICTRS__MEDIA__VIDEO__MAX_HEIGHT
|
||||
# default: 3,840
|
||||
#
|
||||
# this setting does nothing if video is not enabled
|
||||
max_height = 3840
|
||||
|
||||
## Optional: max video area (in pixels)
|
||||
# environment variable: PICTRS__MEDIA__VIDEO__MAX_AREA
|
||||
# default: 8,294,400
|
||||
#
|
||||
# this setting does nothing if video is not enabled
|
||||
max_area = 8294400
|
||||
|
||||
## Optional: max video size (in Megabytes)
|
||||
# environment variable: PICTRS__MEDIA__VIDEO__MAX_FILE_SIZE
|
||||
# default: 40
|
||||
#
|
||||
# this setting does nothing if video is not enabled
|
||||
max_file_size = 40
|
||||
|
||||
## Optional: max frame count
|
||||
# environment variable: PICTRS__MEDIA__VIDEO__MAX_FRAME_COUNT
|
||||
# default: 900
|
||||
#
|
||||
# this setting does nothing if video is not enabled
|
||||
max_frame_count = 900
|
||||
|
||||
## Optional: set the default video codec
|
||||
# environment variable: PICTRS__MEDIA__VIDEO__VIDEO_CODEC
|
||||
# default: vp9
|
||||
#
|
||||
# available options: av1, h264, h265, vp8, vp9
|
||||
# this setting does nothing if video is not enabled
|
||||
video_codec = "vp9"
|
||||
|
||||
## Optional: set the default audio codec
|
||||
# environment variable: PICTRS__MEDIA__VIDEO__AUDIO_CODEC
|
||||
# default: empty
|
||||
#
|
||||
# available options: aac, opus, vorbis
|
||||
# The audio codec is automatically selected based on video codec, but can be overriden to `vorbis`
|
||||
# for webm uploads
|
||||
# automatic mappings:
|
||||
# - av1, vp8, and vp9 map to opus
|
||||
# - h264 and h265 map to aac
|
||||
# - vorbis is not default for any codec
|
||||
# this setting does nothing if full video is not enabled
|
||||
audio_codec = "opus"
|
||||
|
||||
|
||||
## Database configuration
|
||||
[repo]
|
||||
|
|
|
@ -12,7 +12,8 @@ use defaults::Defaults;
|
|||
|
||||
pub(crate) use commandline::Operation;
|
||||
pub(crate) use file::{
|
||||
ConfigFile as Configuration, ObjectStorage, OpenTelemetry, Repo, Sled, Store, Tracing,
|
||||
Animation, ConfigFile as Configuration, Image, Media, ObjectStorage, OpenTelemetry, Repo, Sled,
|
||||
Store, Tracing, Video,
|
||||
};
|
||||
pub(crate) use primitives::{Filesystem, LogFormat};
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
config::primitives::{AudioCodec, LogFormat, Targets, VideoCodec},
|
||||
formats::ImageFormat,
|
||||
config::primitives::{LogFormat, Targets},
|
||||
formats::{AnimationFormat, AudioCodec, ImageFormat, VideoCodec},
|
||||
serde_str::Serde,
|
||||
};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
@ -48,21 +48,28 @@ impl Args {
|
|||
worker_id,
|
||||
client_pool_size,
|
||||
media_preprocess_steps,
|
||||
media_skip_validate_imports,
|
||||
media_max_width,
|
||||
media_max_height,
|
||||
media_max_area,
|
||||
media_max_file_size,
|
||||
media_max_frame_count,
|
||||
media_gif_max_width,
|
||||
media_gif_max_height,
|
||||
media_gif_max_area,
|
||||
media_enable_silent_video,
|
||||
media_enable_full_video,
|
||||
media_image_max_width,
|
||||
media_image_max_height,
|
||||
media_image_max_area,
|
||||
media_image_max_file_size,
|
||||
media_image_format,
|
||||
media_animation_max_width,
|
||||
media_animation_max_height,
|
||||
media_animation_max_area,
|
||||
media_animation_max_file_size,
|
||||
media_animation_max_frame_count,
|
||||
media_animation_format,
|
||||
media_video_enable,
|
||||
media_video_allow_audio,
|
||||
media_video_max_width,
|
||||
media_video_max_height,
|
||||
media_video_max_area,
|
||||
media_video_max_file_size,
|
||||
media_video_max_frame_count,
|
||||
media_video_codec,
|
||||
media_audio_codec,
|
||||
media_video_audio_codec,
|
||||
media_filters,
|
||||
media_format,
|
||||
store,
|
||||
}) => {
|
||||
let server = Server {
|
||||
|
@ -71,33 +78,43 @@ impl Args {
|
|||
worker_id,
|
||||
client_pool_size,
|
||||
};
|
||||
let gif = if media_gif_max_width.is_none()
|
||||
&& media_gif_max_height.is_none()
|
||||
&& media_gif_max_area.is_none()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(Gif {
|
||||
max_width: media_gif_max_width,
|
||||
max_height: media_gif_max_height,
|
||||
max_area: media_gif_max_area,
|
||||
})
|
||||
|
||||
let image = Image {
|
||||
max_file_size: media_image_max_file_size,
|
||||
max_width: media_image_max_width,
|
||||
max_height: media_image_max_height,
|
||||
max_area: media_image_max_area,
|
||||
format: media_image_format,
|
||||
};
|
||||
let media = Media {
|
||||
preprocess_steps: media_preprocess_steps,
|
||||
skip_validate_imports: media_skip_validate_imports,
|
||||
max_width: media_max_width,
|
||||
max_height: media_max_height,
|
||||
max_area: media_max_area,
|
||||
max_file_size: media_max_file_size,
|
||||
max_frame_count: media_max_frame_count,
|
||||
gif,
|
||||
enable_silent_video: media_enable_silent_video,
|
||||
enable_full_video: media_enable_full_video,
|
||||
|
||||
let animation = Animation {
|
||||
max_file_size: media_animation_max_file_size,
|
||||
max_width: media_animation_max_width,
|
||||
max_height: media_animation_max_height,
|
||||
max_area: media_animation_max_area,
|
||||
max_frame_count: media_animation_max_frame_count,
|
||||
format: media_animation_format,
|
||||
};
|
||||
|
||||
let video = Video {
|
||||
enable: media_video_enable,
|
||||
allow_audio: media_video_allow_audio,
|
||||
max_file_size: media_video_max_file_size,
|
||||
max_width: media_video_max_width,
|
||||
max_height: media_video_max_height,
|
||||
max_area: media_video_max_area,
|
||||
max_frame_count: media_video_max_frame_count,
|
||||
video_codec: media_video_codec,
|
||||
audio_codec: media_audio_codec,
|
||||
audio_codec: media_video_audio_codec,
|
||||
};
|
||||
|
||||
let media = Media {
|
||||
max_file_size: media_max_file_size,
|
||||
preprocess_steps: media_preprocess_steps,
|
||||
filters: media_filters,
|
||||
format: media_format,
|
||||
image: image.set(),
|
||||
animation: animation.set(),
|
||||
video: video.set(),
|
||||
};
|
||||
let operation = Operation::Run;
|
||||
|
||||
|
@ -335,45 +352,126 @@ struct OldDb {
|
|||
#[derive(Debug, Default, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
struct Media {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
preprocess_steps: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_width: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_height: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_area: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_file_size: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_frame_count: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
gif: Option<Gif>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
enable_silent_video: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
enable_full_video: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
video_codec: Option<VideoCodec>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
audio_codec: Option<AudioCodec>,
|
||||
preprocess_steps: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
filters: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
format: Option<ImageFormat>,
|
||||
image: Option<Image>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
skip_validate_imports: Option<bool>,
|
||||
animation: Option<Animation>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
video: Option<Video>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
struct Gif {
|
||||
struct Image {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_width: Option<usize>,
|
||||
max_width: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_height: Option<usize>,
|
||||
max_height: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_area: Option<usize>,
|
||||
max_area: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_file_size: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
format: Option<ImageFormat>,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
fn set(self) -> Option<Self> {
|
||||
let any_set = self.max_width.is_some()
|
||||
|| self.max_height.is_some()
|
||||
|| self.max_area.is_some()
|
||||
|| self.max_file_size.is_some()
|
||||
|| self.format.is_some();
|
||||
|
||||
if any_set {
|
||||
Some(self)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
struct Animation {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_width: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_height: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_area: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_frame_count: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_file_size: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
format: Option<AnimationFormat>,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
fn set(self) -> Option<Self> {
|
||||
let any_set = self.max_width.is_some()
|
||||
|| self.max_height.is_some()
|
||||
|| self.max_area.is_some()
|
||||
|| self.max_frame_count.is_some()
|
||||
|| self.max_file_size.is_some()
|
||||
|| self.format.is_some();
|
||||
|
||||
if any_set {
|
||||
Some(self)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
struct Video {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
enable: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
allow_audio: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_width: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_height: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_area: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_frame_count: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
max_file_size: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
video_codec: Option<VideoCodec>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
audio_codec: Option<AudioCodec>,
|
||||
}
|
||||
|
||||
impl Video {
|
||||
fn set(self) -> Option<Self> {
|
||||
let any_set = self.enable.is_some()
|
||||
|| self.allow_audio.is_some()
|
||||
|| self.max_width.is_some()
|
||||
|| self.max_height.is_some()
|
||||
|| self.max_area.is_some()
|
||||
|| self.max_frame_count.is_some()
|
||||
|| self.max_file_size.is_some()
|
||||
|| self.video_codec.is_some()
|
||||
|| self.audio_codec.is_some();
|
||||
|
||||
if any_set {
|
||||
Some(self)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the pict-rs application
|
||||
|
@ -457,60 +555,75 @@ struct Run {
|
|||
#[arg(long)]
|
||||
media_preprocess_steps: Option<String>,
|
||||
|
||||
/// Whether to validate media on the "import" endpoint
|
||||
/// Which media filters should be enabled on the `process` endpoint
|
||||
#[arg(long)]
|
||||
media_skip_validate_imports: Option<bool>,
|
||||
/// The maximum width, in pixels, for uploaded media
|
||||
#[arg(long)]
|
||||
media_max_width: Option<usize>,
|
||||
/// The maximum height, in pixels, for uploaded media
|
||||
#[arg(long)]
|
||||
media_max_height: Option<usize>,
|
||||
/// The maximum area, in pixels, for uploaded media
|
||||
#[arg(long)]
|
||||
media_max_area: Option<usize>,
|
||||
/// The maximum size, in megabytes, for uploaded media
|
||||
media_filters: Option<Vec<String>>,
|
||||
|
||||
/// The maximum size, in megabytes, for all uploaded media
|
||||
#[arg(long)]
|
||||
media_max_file_size: Option<usize>,
|
||||
/// The maximum number of frames allowed for uploaded GIF and MP4s.
|
||||
|
||||
/// The maximum width, in pixels, for uploaded images
|
||||
#[arg(long)]
|
||||
media_max_frame_count: Option<usize>,
|
||||
/// Maximum width allowed for gif uploads.
|
||||
///
|
||||
/// If an upload exceeds this value, it will be transcoded to a video format or rejected,
|
||||
/// depending on whether video uploads are enabled.
|
||||
media_image_max_width: Option<u16>,
|
||||
/// The maximum height, in pixels, for uploaded images
|
||||
#[arg(long)]
|
||||
media_gif_max_width: Option<usize>,
|
||||
/// Maximum height allowed for gif uploads
|
||||
///
|
||||
/// If an upload exceeds this value, it will be transcoded to a video format or rejected,
|
||||
/// depending on whether video uploads are enabled.
|
||||
media_image_max_height: Option<u16>,
|
||||
/// The maximum area, in pixels, for uploaded images
|
||||
#[arg(long)]
|
||||
media_gif_max_height: Option<usize>,
|
||||
/// Maximum area allowed for gif uploads
|
||||
///
|
||||
/// If an upload exceeds this value, it will be transcoded to a video format or rejected,
|
||||
/// depending on whether video uploads are enabled.
|
||||
media_image_max_area: Option<u32>,
|
||||
/// The maximum size, in megabytes, for uploaded images
|
||||
#[arg(long)]
|
||||
media_gif_max_area: Option<usize>,
|
||||
/// Whether to enable GIF and silent video uploads
|
||||
media_image_max_file_size: Option<usize>,
|
||||
/// Enforce a specific format for uploaded images
|
||||
#[arg(long)]
|
||||
media_enable_silent_video: Option<bool>,
|
||||
/// Whether to enable full video uploads
|
||||
media_image_format: Option<ImageFormat>,
|
||||
|
||||
/// The maximum width, in pixels, for uploaded animations
|
||||
#[arg(long)]
|
||||
media_enable_full_video: Option<bool>,
|
||||
media_animation_max_width: Option<u16>,
|
||||
/// The maximum height, in pixels, for uploaded animations
|
||||
#[arg(long)]
|
||||
media_animation_max_height: Option<u16>,
|
||||
/// The maximum area, in pixels, for uploaded animations
|
||||
#[arg(long)]
|
||||
media_animation_max_area: Option<u32>,
|
||||
/// The maximum number of frames allowed for uploaded animations
|
||||
#[arg(long)]
|
||||
media_animation_max_frame_count: Option<u32>,
|
||||
/// The maximum size, in megabytes, for uploaded animations
|
||||
#[arg(long)]
|
||||
media_animation_max_file_size: Option<usize>,
|
||||
/// Enforce a specific format for uploaded animations
|
||||
#[arg(long)]
|
||||
media_animation_format: Option<AnimationFormat>,
|
||||
|
||||
/// Whether to enable video uploads
|
||||
#[arg(long)]
|
||||
media_video_enable: Option<bool>,
|
||||
/// Whether to enable audio in video uploads
|
||||
media_video_allow_audio: Option<bool>,
|
||||
/// The maximum width, in pixels, for uploaded videos
|
||||
#[arg(long)]
|
||||
media_video_max_width: Option<u16>,
|
||||
/// The maximum height, in pixels, for uploaded videos
|
||||
#[arg(long)]
|
||||
media_video_max_height: Option<u16>,
|
||||
/// The maximum area, in pixels, for uploaded videos
|
||||
#[arg(long)]
|
||||
media_video_max_area: Option<u32>,
|
||||
/// The maximum number of frames allowed for uploaded videos
|
||||
#[arg(long)]
|
||||
media_video_max_frame_count: Option<u32>,
|
||||
/// The maximum size, in megabytes, for uploaded videos
|
||||
#[arg(long)]
|
||||
media_video_max_file_size: Option<usize>,
|
||||
/// Enforce a specific video codec for uploaded videos
|
||||
#[arg(long)]
|
||||
media_video_codec: Option<VideoCodec>,
|
||||
/// Enforce a specific audio codec for uploaded videos
|
||||
#[arg(long)]
|
||||
media_audio_codec: Option<AudioCodec>,
|
||||
/// Which media filters should be enabled on the `process` endpoint
|
||||
#[arg(long)]
|
||||
media_filters: Option<Vec<String>>,
|
||||
/// Enforce uploaded media is transcoded to the provided format
|
||||
#[arg(long)]
|
||||
media_format: Option<ImageFormat>,
|
||||
media_video_audio_codec: Option<AudioCodec>,
|
||||
|
||||
#[command(subcommand)]
|
||||
store: Option<RunStore>,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{
|
||||
config::primitives::{LogFormat, Targets, VideoCodec},
|
||||
config::primitives::{LogFormat, Targets},
|
||||
formats::VideoCodec,
|
||||
serde_str::Serde,
|
||||
};
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
|
@ -62,26 +63,43 @@ struct OldDbDefaults {
|
|||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
struct MediaDefaults {
|
||||
max_width: usize,
|
||||
max_height: usize,
|
||||
max_area: usize,
|
||||
max_file_size: usize,
|
||||
max_frame_count: usize,
|
||||
gif: GifDefaults,
|
||||
enable_silent_video: bool,
|
||||
enable_full_video: bool,
|
||||
video_codec: VideoCodec,
|
||||
filters: Vec<String>,
|
||||
skip_validate_imports: bool,
|
||||
image: ImageDefaults,
|
||||
animation: AnimationDefaults,
|
||||
video: VideoDefaults,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
struct GifDefaults {
|
||||
max_height: usize,
|
||||
max_width: usize,
|
||||
max_area: usize,
|
||||
max_frame_count: usize,
|
||||
struct ImageDefaults {
|
||||
max_width: u16,
|
||||
max_height: u16,
|
||||
max_area: u32,
|
||||
max_file_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
struct AnimationDefaults {
|
||||
max_width: u16,
|
||||
max_height: u16,
|
||||
max_area: u32,
|
||||
max_frame_count: u32,
|
||||
max_file_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
struct VideoDefaults {
|
||||
enable: bool,
|
||||
allow_audio: bool,
|
||||
max_height: u16,
|
||||
max_width: u16,
|
||||
max_area: u32,
|
||||
max_frame_count: u32,
|
||||
max_file_size: usize,
|
||||
video_codec: VideoCodec,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
|
@ -175,15 +193,7 @@ impl Default for OldDbDefaults {
|
|||
impl Default for MediaDefaults {
|
||||
fn default() -> Self {
|
||||
MediaDefaults {
|
||||
max_width: 10_000,
|
||||
max_height: 10_000,
|
||||
max_area: 40_000_000,
|
||||
max_file_size: 40,
|
||||
max_frame_count: 900,
|
||||
gif: Default::default(),
|
||||
enable_silent_video: true,
|
||||
enable_full_video: false,
|
||||
video_codec: VideoCodec::Vp9,
|
||||
filters: vec![
|
||||
"blur".into(),
|
||||
"crop".into(),
|
||||
|
@ -191,18 +201,47 @@ impl Default for MediaDefaults {
|
|||
"resize".into(),
|
||||
"thumbnail".into(),
|
||||
],
|
||||
skip_validate_imports: false,
|
||||
image: Default::default(),
|
||||
animation: Default::default(),
|
||||
video: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GifDefaults {
|
||||
impl Default for ImageDefaults {
|
||||
fn default() -> Self {
|
||||
GifDefaults {
|
||||
max_height: 128,
|
||||
max_width: 128,
|
||||
max_area: 16384,
|
||||
ImageDefaults {
|
||||
max_width: 10_000,
|
||||
max_height: 10_000,
|
||||
max_area: 40_000_000,
|
||||
max_file_size: 40,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AnimationDefaults {
|
||||
fn default() -> Self {
|
||||
AnimationDefaults {
|
||||
max_height: 256,
|
||||
max_width: 256,
|
||||
max_area: 65_536,
|
||||
max_frame_count: 100,
|
||||
max_file_size: 40,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VideoDefaults {
|
||||
fn default() -> Self {
|
||||
VideoDefaults {
|
||||
enable: true,
|
||||
allow_audio: false,
|
||||
max_height: 3_840,
|
||||
max_width: 3_840,
|
||||
max_area: 8_294_400,
|
||||
max_frame_count: 900,
|
||||
max_file_size: 40,
|
||||
video_codec: VideoCodec::Vp9,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
config::primitives::{AudioCodec, Filesystem, LogFormat, Targets, VideoCodec},
|
||||
formats::ImageFormat,
|
||||
config::primitives::{Filesystem, LogFormat, Targets},
|
||||
formats::{AnimationFormat, AudioCodec, ImageFormat, VideoCodec},
|
||||
serde_str::Serde,
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
|
@ -144,47 +144,70 @@ pub(crate) struct OldDb {
|
|||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) struct Media {
|
||||
pub(crate) max_file_size: usize,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) preprocess_steps: Option<String>,
|
||||
|
||||
pub(crate) max_width: usize,
|
||||
pub(crate) filters: BTreeSet<String>,
|
||||
|
||||
pub(crate) max_height: usize,
|
||||
pub(crate) image: Image,
|
||||
|
||||
pub(crate) max_area: usize,
|
||||
pub(crate) animation: Animation,
|
||||
|
||||
pub(crate) video: Video,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub(crate) struct Image {
|
||||
pub(crate) max_width: u16,
|
||||
|
||||
pub(crate) max_height: u16,
|
||||
|
||||
pub(crate) max_area: u32,
|
||||
|
||||
pub(crate) max_file_size: usize,
|
||||
|
||||
pub(crate) max_frame_count: usize,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) format: Option<ImageFormat>,
|
||||
}
|
||||
|
||||
pub(crate) gif: Gif,
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub(crate) struct Animation {
|
||||
pub(crate) max_width: u16,
|
||||
|
||||
pub(crate) enable_silent_video: bool,
|
||||
pub(crate) max_height: u16,
|
||||
|
||||
pub(crate) enable_full_video: bool,
|
||||
pub(crate) max_area: u32,
|
||||
|
||||
pub(crate) max_file_size: usize,
|
||||
|
||||
pub(crate) max_frame_count: u32,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) format: Option<AnimationFormat>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub(crate) struct Video {
|
||||
pub(crate) enable: bool,
|
||||
|
||||
pub(crate) allow_audio: bool,
|
||||
|
||||
pub(crate) max_width: u16,
|
||||
|
||||
pub(crate) max_height: u16,
|
||||
|
||||
pub(crate) max_area: u32,
|
||||
|
||||
pub(crate) max_file_size: usize,
|
||||
|
||||
pub(crate) max_frame_count: u32,
|
||||
|
||||
pub(crate) video_codec: VideoCodec,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) audio_codec: Option<AudioCodec>,
|
||||
|
||||
pub(crate) filters: BTreeSet<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) format: Option<ImageFormat>,
|
||||
|
||||
pub(crate) skip_validate_imports: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub(crate) struct Gif {
|
||||
pub(crate) max_width: usize,
|
||||
|
||||
pub(crate) max_height: usize,
|
||||
|
||||
pub(crate) max_area: usize,
|
||||
|
||||
pub(crate) max_frame_count: usize,
|
||||
}
|
||||
|
||||
impl Media {
|
||||
|
|
|
@ -24,48 +24,6 @@ pub(crate) enum LogFormat {
|
|||
Pretty,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
ValueEnum,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum VideoCodec {
|
||||
H264,
|
||||
H265,
|
||||
Av1,
|
||||
Vp8,
|
||||
Vp9,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
ValueEnum,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum AudioCodec {
|
||||
Aac,
|
||||
Opus,
|
||||
Vorbis,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Targets {
|
||||
pub(crate) targets: tracing_subscriber::filter::Targets,
|
||||
|
|
|
@ -6,7 +6,7 @@ use std::{collections::HashSet, sync::OnceLock};
|
|||
use crate::{
|
||||
ffmpeg::FfMpegError,
|
||||
formats::{
|
||||
AnimationFormat, AnimationInput, ImageFormat, ImageInput, InputFile, InternalFormat,
|
||||
AnimationFormat, ImageFormat, ImageInput, InputFile, InternalFormat,
|
||||
InternalVideoFormat, VideoFormat,
|
||||
},
|
||||
process::Process,
|
||||
|
@ -169,7 +169,7 @@ where
|
|||
frames,
|
||||
})),
|
||||
InternalFormat::Animation(format) => Ok(Some(Discovery {
|
||||
input: InputFile::Animation(AnimationInput { format }),
|
||||
input: InputFile::Animation(format),
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
|
|
|
@ -6,7 +6,7 @@ use futures_util::Stream;
|
|||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::{
|
||||
formats::{AnimationFormat, AnimationInput, ImageFormat, ImageInput, InputFile, VideoFormat},
|
||||
formats::{AnimationFormat, ImageFormat, ImageInput, InputFile, VideoFormat},
|
||||
magick::MagickError,
|
||||
process::Process,
|
||||
};
|
||||
|
@ -78,9 +78,7 @@ pub(super) async fn confirm_bytes(
|
|||
match discovery {
|
||||
Some(Discovery {
|
||||
input:
|
||||
InputFile::Animation(AnimationInput {
|
||||
format: AnimationFormat::Avif,
|
||||
}),
|
||||
InputFile::Animation( AnimationFormat::Avif,),
|
||||
width,
|
||||
height,
|
||||
..
|
||||
|
@ -94,9 +92,7 @@ pub(super) async fn confirm_bytes(
|
|||
.await?;
|
||||
|
||||
return Ok(Discovery {
|
||||
input: InputFile::Animation(AnimationInput {
|
||||
format: AnimationFormat::Avif,
|
||||
}),
|
||||
input: InputFile::Animation( AnimationFormat::Avif,),
|
||||
width,
|
||||
height,
|
||||
frames: Some(frames),
|
||||
|
@ -104,9 +100,7 @@ pub(super) async fn confirm_bytes(
|
|||
}
|
||||
Some(Discovery {
|
||||
input:
|
||||
InputFile::Animation(AnimationInput {
|
||||
format: AnimationFormat::Webp,
|
||||
}),
|
||||
InputFile::Animation( AnimationFormat::Webp,),
|
||||
..
|
||||
}) => {
|
||||
// continue
|
||||
|
@ -265,9 +259,7 @@ fn parse_discovery(output: Vec<MagickDiscovery>) -> Result<Discovery, MagickErro
|
|||
"AVIF" => {
|
||||
if frames > 1 {
|
||||
Ok(Discovery {
|
||||
input: InputFile::Animation(AnimationInput {
|
||||
format: AnimationFormat::Avif,
|
||||
}),
|
||||
input: InputFile::Animation( AnimationFormat::Avif,),
|
||||
width,
|
||||
height,
|
||||
frames: Some(frames),
|
||||
|
@ -285,17 +277,13 @@ fn parse_discovery(output: Vec<MagickDiscovery>) -> Result<Discovery, MagickErro
|
|||
}
|
||||
}
|
||||
"APNG" => Ok(Discovery {
|
||||
input: InputFile::Animation(AnimationInput {
|
||||
format: AnimationFormat::Apng,
|
||||
}),
|
||||
input: InputFile::Animation( AnimationFormat::Apng,),
|
||||
width,
|
||||
height,
|
||||
frames: Some(frames),
|
||||
}),
|
||||
"GIF" => Ok(Discovery {
|
||||
input: InputFile::Animation(AnimationInput {
|
||||
format: AnimationFormat::Gif,
|
||||
}),
|
||||
input: InputFile::Animation( AnimationFormat::Gif,),
|
||||
width,
|
||||
height,
|
||||
frames: Some(frames),
|
||||
|
@ -336,9 +324,7 @@ fn parse_discovery(output: Vec<MagickDiscovery>) -> Result<Discovery, MagickErro
|
|||
"WEBP" => {
|
||||
if frames > 1 {
|
||||
Ok(Discovery {
|
||||
input: InputFile::Animation(AnimationInput {
|
||||
format: AnimationFormat::Webp,
|
||||
}),
|
||||
input: InputFile::Animation( AnimationFormat::Webp,),
|
||||
width,
|
||||
height,
|
||||
frames: Some(frames),
|
||||
|
|
|
@ -57,6 +57,9 @@ pub(crate) enum UploadError {
|
|||
#[error("Error interacting with filesystem")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Error validating upload")]
|
||||
Validation(#[from] crate::validate::ValidationError),
|
||||
|
||||
#[error("Error generating path")]
|
||||
PathGenerator(#[from] storage_path_generator::PathError),
|
||||
|
||||
|
@ -166,6 +169,7 @@ impl ResponseError for Error {
|
|||
crate::repo::RepoError::AlreadyClaimed,
|
||||
))
|
||||
| UploadError::Repo(crate::repo::RepoError::AlreadyClaimed)
|
||||
| UploadError::Validation(_)
|
||||
| UploadError::UnsupportedProcessExtension,
|
||||
) => StatusCode::BAD_REQUEST,
|
||||
Some(UploadError::Magick(e)) if e.is_client_error() => StatusCode::BAD_REQUEST,
|
||||
|
|
|
@ -5,22 +5,21 @@ mod video;
|
|||
|
||||
use std::str::FromStr;
|
||||
|
||||
pub(crate) use animation::{AnimationFormat, AnimationInput, AnimationOutput};
|
||||
pub(crate) use animation::{AnimationFormat, AnimationOutput};
|
||||
pub(crate) use image::{ImageFormat, ImageInput, ImageOutput};
|
||||
pub(crate) use video::{InternalVideoFormat, OutputVideoFormat, VideoFormat};
|
||||
pub(crate) use video::{InternalVideoFormat, OutputVideoFormat, VideoFormat, VideoCodec, AudioCodec};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct PrescribedFormats {
|
||||
pub(crate) image: Option<ImageFormat>,
|
||||
pub(crate) animation: Option<AnimationFormat>,
|
||||
pub(crate) video: Option<OutputVideoFormat>,
|
||||
pub(crate) allow_audio: bool,
|
||||
pub(crate) struct Validations<'a> {
|
||||
pub(crate) image: &'a crate::config::Image,
|
||||
pub(crate) animation: &'a crate::config::Animation,
|
||||
pub(crate) video: &'a crate::config::Video,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum InputFile {
|
||||
Image(ImageInput),
|
||||
Animation(AnimationInput),
|
||||
Animation(AnimationFormat),
|
||||
Video(VideoFormat),
|
||||
}
|
||||
|
||||
|
@ -61,7 +60,7 @@ impl InputFile {
|
|||
pub(crate) const fn internal_format(&self) -> InternalFormat {
|
||||
match self {
|
||||
Self::Image(ImageInput { format, .. }) => InternalFormat::Image(*format),
|
||||
Self::Animation(AnimationInput { format }) => InternalFormat::Animation(*format),
|
||||
Self::Animation(format) => InternalFormat::Animation(*format),
|
||||
Self::Video(format) => InternalFormat::Video(format.internal_format()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
#[derive(
|
||||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
clap::ValueEnum,
|
||||
)]
|
||||
pub(crate) enum AnimationFormat {
|
||||
#[serde(rename = "apng")]
|
||||
|
@ -12,24 +22,16 @@ pub(crate) enum AnimationFormat {
|
|||
Webp,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
||||
pub(crate) struct AnimationInput {
|
||||
pub(crate) format: AnimationFormat,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
||||
pub(crate) struct AnimationOutput {
|
||||
pub(crate) format: AnimationFormat,
|
||||
pub(crate) needs_transcode: bool,
|
||||
}
|
||||
|
||||
impl AnimationInput {
|
||||
pub(crate) const fn build_output(
|
||||
&self,
|
||||
prescribed: Option<AnimationFormat>,
|
||||
) -> AnimationOutput {
|
||||
impl AnimationFormat {
|
||||
pub(crate) const fn build_output(self, prescribed: Option<AnimationFormat>) -> AnimationOutput {
|
||||
if let Some(prescribed) = prescribed {
|
||||
let needs_transcode = !self.format.const_eq(prescribed);
|
||||
let needs_transcode = !self.const_eq(prescribed);
|
||||
|
||||
return AnimationOutput {
|
||||
format: prescribed,
|
||||
|
@ -38,13 +40,11 @@ impl AnimationInput {
|
|||
}
|
||||
|
||||
AnimationOutput {
|
||||
format: self.format,
|
||||
format: self,
|
||||
needs_transcode: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationFormat {
|
||||
const fn const_eq(self, rhs: Self) -> bool {
|
||||
match (self, rhs) {
|
||||
(Self::Apng, Self::Apng)
|
||||
|
|
|
@ -18,7 +18,19 @@ pub(crate) enum OutputVideoFormat {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
clap::ValueEnum,
|
||||
)]
|
||||
pub(crate) enum VideoCodec {
|
||||
#[serde(rename = "av1")]
|
||||
Av1,
|
||||
|
@ -33,7 +45,17 @@ pub(crate) enum VideoCodec {
|
|||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
clap::ValueEnum,
|
||||
)]
|
||||
pub(crate) enum AudioCodec {
|
||||
#[serde(rename = "aac")]
|
||||
|
@ -120,23 +142,79 @@ impl VideoFormat {
|
|||
|
||||
pub(crate) const fn build_output(
|
||||
self,
|
||||
prescribed: Option<OutputVideoFormat>,
|
||||
video_codec: VideoCodec,
|
||||
audio_codec: Option<AudioCodec>,
|
||||
allow_audio: bool,
|
||||
) -> OutputVideoFormat {
|
||||
match (prescribed, self) {
|
||||
(
|
||||
Some(OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec { codec, .. }),
|
||||
audio_codec,
|
||||
match (video_codec, self) {
|
||||
(VideoCodec::Vp8, Self::Webm { alpha }) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha,
|
||||
codec: WebmAlphaCodec::Vp8,
|
||||
}),
|
||||
Self::Webm { alpha },
|
||||
) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec { alpha, codec }),
|
||||
audio_codec,
|
||||
audio_codec: if allow_audio {
|
||||
match audio_codec {
|
||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
||||
_ => Some(WebmAudioCodec::Opus),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
(Some(prescribed), _) => prescribed,
|
||||
(None, format) => match format {
|
||||
VideoFormat::Mp4 => OutputVideoFormat::Mp4 {
|
||||
},
|
||||
(VideoCodec::Vp8, _) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp8,
|
||||
}),
|
||||
audio_codec: if allow_audio {
|
||||
match audio_codec {
|
||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
||||
_ => Some(WebmAudioCodec::Opus),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
(VideoCodec::Vp9, Self::Webm { alpha }) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha,
|
||||
codec: WebmAlphaCodec::Vp9,
|
||||
}),
|
||||
audio_codec: if allow_audio {
|
||||
match audio_codec {
|
||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
||||
_ => Some(WebmAudioCodec::Opus),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
(VideoCodec::Vp9, _) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha: false,
|
||||
codec: WebmAlphaCodec::Vp9,
|
||||
}),
|
||||
audio_codec: if allow_audio {
|
||||
match audio_codec {
|
||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
||||
_ => Some(WebmAudioCodec::Opus),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
(VideoCodec::Av1, _) => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Av1,
|
||||
audio_codec: if allow_audio {
|
||||
match audio_codec {
|
||||
Some(AudioCodec::Vorbis) => Some(WebmAudioCodec::Vorbis),
|
||||
_ => Some(WebmAudioCodec::Opus),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
(VideoCodec::H264, _) => OutputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::H264,
|
||||
audio_codec: if allow_audio {
|
||||
Some(Mp4AudioCodec::Aac)
|
||||
|
@ -144,24 +222,20 @@ impl VideoFormat {
|
|||
None
|
||||
},
|
||||
},
|
||||
VideoFormat::Webm { alpha } => OutputVideoFormat::Webm {
|
||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||
alpha,
|
||||
codec: WebmAlphaCodec::Vp9,
|
||||
}),
|
||||
(VideoCodec::H265, _) => OutputVideoFormat::Mp4 {
|
||||
video_codec: Mp4Codec::H265,
|
||||
audio_codec: if allow_audio {
|
||||
Some(WebmAudioCodec::Opus)
|
||||
Some(Mp4AudioCodec::Aac)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputVideoFormat {
|
||||
pub(super) const fn from_parts(
|
||||
pub(crate) const fn from_parts(
|
||||
video_codec: VideoCodec,
|
||||
audio_codec: Option<AudioCodec>,
|
||||
allow_audio: bool,
|
||||
|
@ -240,6 +314,13 @@ impl OutputVideoFormat {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn magick_format(self) -> &'static str {
|
||||
match self {
|
||||
Self::Mp4 { .. } => "MP4",
|
||||
Self::Webm { .. } => "WEBM",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
||||
match self {
|
||||
Self::Mp4 { .. } => "mp4",
|
||||
|
|
|
@ -2,10 +2,9 @@ use crate::{
|
|||
bytes_stream::BytesStream,
|
||||
either::Either,
|
||||
error::{Error, UploadError},
|
||||
formats::{InternalFormat, PrescribedFormats},
|
||||
formats::{InternalFormat, Validations},
|
||||
repo::{Alias, AliasRepo, AlreadyExists, DeleteToken, FullRepo, HashRepo},
|
||||
store::Store,
|
||||
CONFIG,
|
||||
};
|
||||
use actix_web::web::Bytes;
|
||||
use futures_util::{Stream, StreamExt};
|
||||
|
@ -47,6 +46,7 @@ pub(crate) async fn ingest<R, S>(
|
|||
store: &S,
|
||||
stream: impl Stream<Item = Result<Bytes, Error>> + Unpin + 'static,
|
||||
declared_alias: Option<Alias>,
|
||||
media: &crate::config::Media,
|
||||
) -> Result<Session<R, S>, Error>
|
||||
where
|
||||
R: FullRepo + 'static,
|
||||
|
@ -57,18 +57,16 @@ where
|
|||
let bytes = aggregate(stream).await?;
|
||||
|
||||
// TODO: load from config
|
||||
let prescribed = PrescribedFormats {
|
||||
image: None,
|
||||
animation: None,
|
||||
video: None,
|
||||
allow_audio: true,
|
||||
let prescribed = Validations {
|
||||
image: &media.image,
|
||||
animation: &media.animation,
|
||||
video: &media.video,
|
||||
};
|
||||
|
||||
tracing::trace!("Validating bytes");
|
||||
let (input_type, validated_reader) =
|
||||
crate::validate::validate_bytes(bytes, &prescribed).await?;
|
||||
let (input_type, validated_reader) = crate::validate::validate_bytes(bytes, prescribed).await?;
|
||||
|
||||
let processed_reader = if let Some(operations) = CONFIG.media.preprocess_steps() {
|
||||
let processed_reader = if let Some(operations) = media.preprocess_steps() {
|
||||
if let Some(format) = input_type.processable_format() {
|
||||
let (_, magick_args) =
|
||||
crate::processor::build_chain(operations, format.file_extension())?;
|
||||
|
|
|
@ -169,7 +169,9 @@ impl<R: FullRepo, S: Store + 'static> FormData for Upload<R, S> {
|
|||
let stream = stream.map_err(Error::from);
|
||||
|
||||
Box::pin(
|
||||
async move { ingest::ingest(&**repo, &**store, stream, None).await }
|
||||
async move {
|
||||
ingest::ingest(&**repo, &**store, stream, None, &CONFIG.media).await
|
||||
}
|
||||
.instrument(span),
|
||||
)
|
||||
})),
|
||||
|
@ -221,6 +223,7 @@ impl<R: FullRepo, S: Store + 'static> FormData for Import<R, S> {
|
|||
&**store,
|
||||
stream,
|
||||
Some(Alias::from_existing(&filename)),
|
||||
&CONFIG.media,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
@ -475,7 +478,7 @@ async fn do_download_inline<R: FullRepo + 'static, S: Store + 'static>(
|
|||
repo: web::Data<R>,
|
||||
store: web::Data<S>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let mut session = ingest::ingest(&repo, &store, stream, None).await?;
|
||||
let mut session = ingest::ingest(&repo, &store, stream, None, &CONFIG.media).await?;
|
||||
|
||||
let alias = session.alias().expect("alias should exist").to_owned();
|
||||
let delete_token = session.delete_token().await?;
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::{
|
|||
repo::{Alias, DeleteToken, FullRepo, UploadId, UploadResult},
|
||||
serde_str::Serde,
|
||||
store::{Identifier, Store},
|
||||
CONFIG,
|
||||
};
|
||||
use futures_util::TryStreamExt;
|
||||
use std::path::PathBuf;
|
||||
|
@ -33,6 +34,7 @@ where
|
|||
identifier,
|
||||
Serde::into_inner(upload_id),
|
||||
declared_alias.map(Serde::into_inner),
|
||||
&CONFIG.media,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
@ -69,6 +71,7 @@ async fn process_ingest<R, S>(
|
|||
unprocessed_identifier: Vec<u8>,
|
||||
upload_id: UploadId,
|
||||
declared_alias: Option<Alias>,
|
||||
media: &crate::config::Media,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
R: FullRepo + 'static,
|
||||
|
@ -82,7 +85,7 @@ where
|
|||
.await?
|
||||
.map_err(Error::from);
|
||||
|
||||
let session = crate::ingest::ingest(repo, store, stream, declared_alias).await?;
|
||||
let session = crate::ingest::ingest(repo, store, stream, declared_alias, media).await?;
|
||||
|
||||
let token = session.delete_token().await?;
|
||||
|
||||
|
|
221
src/validate.rs
221
src/validate.rs
|
@ -3,66 +3,235 @@ mod ffmpeg;
|
|||
mod magick;
|
||||
|
||||
use crate::{
|
||||
discover::Discovery,
|
||||
either::Either,
|
||||
error::Error,
|
||||
formats::{AnimationOutput, ImageOutput, InputFile, InternalFormat, PrescribedFormats},
|
||||
formats::{
|
||||
AnimationFormat, AnimationOutput, ImageInput, ImageOutput, InputFile, InternalFormat,
|
||||
OutputVideoFormat, Validations, VideoFormat,
|
||||
},
|
||||
};
|
||||
use actix_web::web::Bytes;
|
||||
use tokio::io::AsyncRead;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum ValidationError {
|
||||
#[error("Too wide")]
|
||||
Width,
|
||||
|
||||
#[error("Too tall")]
|
||||
Height,
|
||||
|
||||
#[error("Too many pixels")]
|
||||
Area,
|
||||
|
||||
#[error("Too many frames")]
|
||||
Frames,
|
||||
|
||||
#[error("Filesize too large")]
|
||||
Filesize,
|
||||
|
||||
#[error("Video is disabled")]
|
||||
VideoDisabled,
|
||||
}
|
||||
|
||||
const MEGABYTES: usize = 1024 * 1024;
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub(crate) async fn validate_bytes(
|
||||
bytes: Bytes,
|
||||
prescribed: &PrescribedFormats,
|
||||
validations: Validations<'_>,
|
||||
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
|
||||
let discovery = crate::discover::discover_bytes(bytes.clone()).await?;
|
||||
let Discovery {
|
||||
input,
|
||||
width,
|
||||
height,
|
||||
frames,
|
||||
} = crate::discover::discover_bytes(bytes.clone()).await?;
|
||||
|
||||
match &discovery.input {
|
||||
match &input {
|
||||
InputFile::Image(input) => {
|
||||
let (format, read) =
|
||||
process_image(bytes, *input, width, height, validations.image).await?;
|
||||
|
||||
Ok((format, Either::left(read)))
|
||||
}
|
||||
InputFile::Animation(input) => {
|
||||
let (format, read) = process_animation(
|
||||
bytes,
|
||||
*input,
|
||||
width,
|
||||
height,
|
||||
frames.unwrap_or(1),
|
||||
&validations,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((format, Either::right(Either::left(read))))
|
||||
}
|
||||
InputFile::Video(input) => {
|
||||
let (format, read) = process_video(
|
||||
bytes,
|
||||
*input,
|
||||
width,
|
||||
height,
|
||||
frames.unwrap_or(1),
|
||||
validations.video,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((format, Either::right(Either::right(read))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(bytes, validations))]
|
||||
async fn process_image(
|
||||
bytes: Bytes,
|
||||
input: ImageInput,
|
||||
width: u16,
|
||||
height: u16,
|
||||
validations: &crate::config::Image,
|
||||
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
|
||||
if width > validations.max_width {
|
||||
return Err(ValidationError::Width.into());
|
||||
}
|
||||
if height > validations.max_height {
|
||||
return Err(ValidationError::Height.into());
|
||||
}
|
||||
if u32::from(width) * u32::from(height) > validations.max_area {
|
||||
return Err(ValidationError::Area.into());
|
||||
}
|
||||
if bytes.len() > validations.max_file_size * MEGABYTES {
|
||||
return Err(ValidationError::Filesize.into());
|
||||
}
|
||||
|
||||
let ImageOutput {
|
||||
format,
|
||||
needs_transcode,
|
||||
} = input.build_output(prescribed.image);
|
||||
} = input.build_output(validations.format);
|
||||
|
||||
let read = if needs_transcode {
|
||||
Either::left(Either::left(magick::convert_image(
|
||||
input.format,
|
||||
format,
|
||||
bytes,
|
||||
)?))
|
||||
Either::left(magick::convert_image(input.format, format, bytes)?)
|
||||
} else {
|
||||
Either::left(Either::right(exiftool::clear_metadata_bytes_read(bytes)?))
|
||||
Either::right(exiftool::clear_metadata_bytes_read(bytes)?)
|
||||
};
|
||||
|
||||
Ok((InternalFormat::Image(format), read))
|
||||
}
|
||||
InputFile::Animation(input) => {
|
||||
|
||||
fn validate_animation(
|
||||
size: usize,
|
||||
width: u16,
|
||||
height: u16,
|
||||
frames: u32,
|
||||
validations: &crate::config::Animation,
|
||||
) -> Result<(), ValidationError> {
|
||||
if width > validations.max_width {
|
||||
return Err(ValidationError::Width);
|
||||
}
|
||||
if height > validations.max_height {
|
||||
return Err(ValidationError::Height);
|
||||
}
|
||||
if u32::from(width) * u32::from(height) > validations.max_area {
|
||||
return Err(ValidationError::Area);
|
||||
}
|
||||
if frames > validations.max_frame_count {
|
||||
return Err(ValidationError::Frames);
|
||||
}
|
||||
if size > validations.max_file_size * MEGABYTES {
|
||||
return Err(ValidationError::Filesize);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(bytes, validations))]
|
||||
async fn process_animation(
|
||||
bytes: Bytes,
|
||||
input: AnimationFormat,
|
||||
width: u16,
|
||||
height: u16,
|
||||
frames: u32,
|
||||
validations: &Validations<'_>,
|
||||
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
|
||||
match validate_animation(bytes.len(), width, height, frames, validations.animation) {
|
||||
Ok(()) => {
|
||||
let AnimationOutput {
|
||||
format,
|
||||
needs_transcode,
|
||||
} = input.build_output(prescribed.animation);
|
||||
} = input.build_output(validations.animation.format);
|
||||
|
||||
let read = if needs_transcode {
|
||||
Either::right(Either::left(magick::convert_animation(
|
||||
input.format,
|
||||
format,
|
||||
bytes,
|
||||
)?))
|
||||
Either::left(magick::convert_animation(input, format, bytes)?)
|
||||
} else {
|
||||
Either::right(Either::right(Either::left(
|
||||
exiftool::clear_metadata_bytes_read(bytes)?,
|
||||
)))
|
||||
Either::right(Either::left(exiftool::clear_metadata_bytes_read(bytes)?))
|
||||
};
|
||||
|
||||
Ok((InternalFormat::Animation(format), read))
|
||||
}
|
||||
InputFile::Video(input) => {
|
||||
let output = input.build_output(prescribed.video, prescribed.allow_audio);
|
||||
let read = Either::right(Either::right(Either::right(
|
||||
ffmpeg::transcode_bytes(*input, output, bytes).await?,
|
||||
)));
|
||||
Err(_) if validate_video(bytes.len(), width, height, frames, validations.video).is_ok() => {
|
||||
let output = OutputVideoFormat::from_parts(
|
||||
validations.video.video_codec,
|
||||
validations.video.audio_codec,
|
||||
validations.video.allow_audio,
|
||||
);
|
||||
|
||||
let read = Either::right(Either::right(magick::convert_video(input, output, bytes)?));
|
||||
|
||||
Ok((InternalFormat::Video(output.internal_format()), read))
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_video(
|
||||
size: usize,
|
||||
width: u16,
|
||||
height: u16,
|
||||
frames: u32,
|
||||
validations: &crate::config::Video,
|
||||
) -> Result<(), ValidationError> {
|
||||
if !validations.enable {
|
||||
return Err(ValidationError::VideoDisabled);
|
||||
}
|
||||
if width > validations.max_width {
|
||||
return Err(ValidationError::Width);
|
||||
}
|
||||
if height > validations.max_height {
|
||||
return Err(ValidationError::Height);
|
||||
}
|
||||
if u32::from(width) * u32::from(height) > validations.max_area {
|
||||
return Err(ValidationError::Area);
|
||||
}
|
||||
if frames > validations.max_frame_count {
|
||||
return Err(ValidationError::Frames);
|
||||
}
|
||||
if size > validations.max_file_size * MEGABYTES {
|
||||
return Err(ValidationError::Filesize);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(bytes, validations))]
|
||||
async fn process_video(
|
||||
bytes: Bytes,
|
||||
input: VideoFormat,
|
||||
width: u16,
|
||||
height: u16,
|
||||
frames: u32,
|
||||
validations: &crate::config::Video,
|
||||
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
|
||||
validate_video(bytes.len(), width, height, frames, validations)?;
|
||||
|
||||
let output = input.build_output(
|
||||
validations.video_codec,
|
||||
validations.audio_codec,
|
||||
validations.allow_audio,
|
||||
);
|
||||
|
||||
let read = ffmpeg::transcode_bytes(input, output, bytes).await?;
|
||||
|
||||
Ok((InternalFormat::Video(output.internal_format()), read))
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use actix_web::web::Bytes;
|
|||
use tokio::io::AsyncRead;
|
||||
|
||||
use crate::{
|
||||
formats::{AnimationFormat, ImageFormat},
|
||||
formats::{AnimationFormat, ImageFormat, OutputVideoFormat},
|
||||
magick::MagickError,
|
||||
process::Process,
|
||||
};
|
||||
|
@ -37,3 +37,17 @@ pub(super) fn convert_animation(
|
|||
|
||||
Ok(process.bytes_read(bytes))
|
||||
}
|
||||
|
||||
pub(super) fn convert_video(
|
||||
input: AnimationFormat,
|
||||
output: OutputVideoFormat,
|
||||
bytes: Bytes,
|
||||
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
||||
let input_arg = format!("{}:-", input.magick_format());
|
||||
let output_arg = format!("{}:-", output.magick_format());
|
||||
|
||||
let process = Process::run("magick", &["-strip", &input_arg, "-coalesce", &output_arg])
|
||||
.map_err(MagickError::Process)?;
|
||||
|
||||
Ok(process.bytes_read(bytes))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue