303 řádky
8.3 KiB
Rust
Spustitelný soubor
303 řádky
8.3 KiB
Rust
Spustitelný soubor
use std::process::Command;
|
|
|
|
use actix_multipart::form::tempfile::TempFile;
|
|
use chrono::Utc;
|
|
use tokio::{
|
|
fs::{remove_file, rename},
|
|
task::spawn_blocking,
|
|
};
|
|
|
|
use crate::{cfg::Cfg, db::models::File, error::NekrochanError};
|
|
|
|
impl File {
|
|
pub async fn new(
|
|
cfg: &Cfg,
|
|
temp_file: TempFile,
|
|
spoiler: bool,
|
|
thumb: bool,
|
|
) -> Result<Self, NekrochanError> {
|
|
let original_name = temp_file.file_name.unwrap_or_else(|| "unknown".into());
|
|
|
|
let mime = temp_file
|
|
.content_type
|
|
.ok_or(NekrochanError::FileError(
|
|
original_name.clone(),
|
|
"žádný mime typ",
|
|
))?
|
|
.to_string();
|
|
|
|
let (video, format) = match mime.as_str() {
|
|
"image/jpeg" => (false, "jpg"),
|
|
"image/pjpeg" => (false, "jpg"),
|
|
"image/png" => (false, "png"),
|
|
"image/bmp" => (false, "bmp"),
|
|
"image/gif" => (false, "gif"),
|
|
"image/webp" => (false, "webp"),
|
|
"image/apng" => (false, "apng"),
|
|
"video/mpeg" => (true, "mpeg"),
|
|
"video/quicktime" => (true, "mov"),
|
|
"video/mp4" => (true, "mp4"),
|
|
"video/webm" => (true, "webm"),
|
|
"video/x-matroska" => (true, "mkv"),
|
|
"video/ogg" => (true, "ogg"),
|
|
_ => {
|
|
return Err(NekrochanError::FileError(
|
|
original_name,
|
|
"nepodporovaný formát",
|
|
))
|
|
}
|
|
};
|
|
|
|
if video && !cfg.files.videos {
|
|
return Err(NekrochanError::FileError(
|
|
original_name,
|
|
"videa nejsou podporovaná",
|
|
));
|
|
}
|
|
|
|
let size = temp_file.size;
|
|
|
|
if size / 1_000_000 > cfg.files.max_size_mb {
|
|
return Err(NekrochanError::FileError(
|
|
original_name,
|
|
"soubor je příliš velký",
|
|
));
|
|
}
|
|
|
|
let timestamp = Utc::now().timestamp_micros();
|
|
let format = format.to_owned();
|
|
|
|
let new_name = format!("{timestamp}.{format}");
|
|
|
|
let (thumb_format, thumb_name) = if thumb {
|
|
let format = if video { "png".into() } else { format.clone() };
|
|
|
|
(Some(format.clone()), Some(format!("{timestamp}.{format}")))
|
|
} else {
|
|
(None, None)
|
|
};
|
|
|
|
rename(temp_file.file.path(), format!("/tmp/{new_name}")).await?;
|
|
|
|
let (width, height) = if video {
|
|
process_video(cfg, original_name.clone(), new_name.clone(), thumb_name).await?
|
|
} else {
|
|
process_image(cfg, original_name.clone(), new_name.clone(), thumb_name).await?
|
|
};
|
|
|
|
rename(format!("/tmp/{new_name}"), format!("./uploads/{new_name}")).await?;
|
|
|
|
let file = File {
|
|
original_name,
|
|
format,
|
|
thumb_format,
|
|
spoiler,
|
|
width,
|
|
height,
|
|
timestamp,
|
|
size,
|
|
};
|
|
|
|
Ok(file)
|
|
}
|
|
|
|
pub async fn delete(&self) {
|
|
remove_file(format!("./uploads/{}.{}", self.timestamp, self.format))
|
|
.await
|
|
.ok();
|
|
|
|
if let Some(thumb_format) = &self.thumb_format {
|
|
remove_file(format!(
|
|
"./uploads/thumb/{}.{}",
|
|
self.timestamp, thumb_format
|
|
))
|
|
.await
|
|
.ok();
|
|
}
|
|
}
|
|
|
|
pub fn file_url(&self) -> String {
|
|
format!("/uploads/{}.{}", self.timestamp, self.format)
|
|
}
|
|
|
|
pub fn thumb_url(&self) -> String {
|
|
if self.spoiler {
|
|
"/static/spoiler.png".into()
|
|
} else if let Some(thumb_format) = &self.thumb_format {
|
|
format!("/uploads/thumb/{}.{}", self.timestamp, thumb_format)
|
|
} else {
|
|
self.file_url()
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn process_image(
|
|
cfg: &Cfg,
|
|
original_name: String,
|
|
new_name: String,
|
|
thumb_name: Option<String>,
|
|
) -> Result<(u32, u32), NekrochanError> {
|
|
let new_name_ = new_name.clone();
|
|
|
|
let identify_out = spawn_blocking(move || {
|
|
Command::new("identify")
|
|
.args(["-format", "%wx%h", &format!("/tmp/{new_name_}[0]")])
|
|
.output()
|
|
})
|
|
.await??;
|
|
|
|
let invalid_dimensions = "imagemagick vrátil neplatné rozměry";
|
|
let out_string = String::from_utf8_lossy(&identify_out.stdout);
|
|
|
|
let (width, height) = out_string
|
|
.trim()
|
|
.split_once('x')
|
|
.ok_or(NekrochanError::FileError(
|
|
original_name.clone(),
|
|
invalid_dimensions,
|
|
))?;
|
|
|
|
let (width, height) = (
|
|
width
|
|
.parse()
|
|
.map_err(|_| NekrochanError::FileError(original_name.clone(), invalid_dimensions))?,
|
|
height
|
|
.parse()
|
|
.map_err(|_| NekrochanError::FileError(original_name.clone(), invalid_dimensions))?,
|
|
);
|
|
|
|
if width > cfg.files.max_width || height > cfg.files.max_height {
|
|
return Err(NekrochanError::FileError(
|
|
original_name,
|
|
"rozměry obrázku jsou příliš velké",
|
|
));
|
|
}
|
|
|
|
let Some(thumb_name) = thumb_name else {
|
|
return Ok((width, height));
|
|
};
|
|
|
|
let thumb_size = cfg.files.thumb_size;
|
|
|
|
let output = spawn_blocking(move || {
|
|
Command::new("convert")
|
|
.arg(&format!("/tmp/{new_name}"))
|
|
.arg("-coalesce")
|
|
.arg("-thumbnail")
|
|
.arg(&format!("{thumb_size}x{thumb_size}>"))
|
|
.arg(&format!("./uploads/thumb/{thumb_name}"))
|
|
.output()
|
|
})
|
|
.await??;
|
|
|
|
if !output.status.success() {
|
|
println!("{}", String::from_utf8_lossy(&output.stderr));
|
|
|
|
return Err(NekrochanError::FileError(
|
|
original_name,
|
|
"nepodařilo se vytvořit náhled obrázku",
|
|
));
|
|
}
|
|
|
|
Ok((width, height))
|
|
}
|
|
|
|
async fn process_video(
|
|
cfg: &Cfg,
|
|
original_name: String,
|
|
new_name: String,
|
|
thumb_name: Option<String>,
|
|
) -> Result<(u32, u32), NekrochanError> {
|
|
let new_name_ = new_name.clone();
|
|
|
|
let ffprobe_out = spawn_blocking(move || {
|
|
Command::new("ffprobe")
|
|
.args([
|
|
"-v",
|
|
"error",
|
|
"-select_streams",
|
|
"v:0",
|
|
"-show_entries",
|
|
"stream=width,height",
|
|
"-of",
|
|
"csv=s=x:p=0",
|
|
&format!("/tmp/{new_name_}"),
|
|
])
|
|
.output()
|
|
})
|
|
.await??;
|
|
|
|
if !ffprobe_out.status.success() {
|
|
return Err(NekrochanError::FileError(
|
|
original_name,
|
|
"nepodařilo se získat rozměry videa",
|
|
));
|
|
}
|
|
|
|
let invalid_dimensions = "ffmpeg vrátil neplatné rozměry";
|
|
let out_string = String::from_utf8_lossy(&ffprobe_out.stdout);
|
|
|
|
let (width, height) = out_string
|
|
.trim()
|
|
.split_once('x')
|
|
.ok_or(NekrochanError::FileError(
|
|
original_name.clone(),
|
|
invalid_dimensions,
|
|
))?;
|
|
|
|
let (width, height) = (
|
|
width
|
|
.parse()
|
|
.map_err(|_| NekrochanError::FileError(original_name.clone(), invalid_dimensions))?,
|
|
height
|
|
.parse()
|
|
.map_err(|_| NekrochanError::FileError(original_name.clone(), invalid_dimensions))?,
|
|
);
|
|
|
|
if width > cfg.files.max_width || height > cfg.files.max_height {
|
|
return Err(NekrochanError::FileError(
|
|
original_name,
|
|
"rozměry videa jsou příliš velké",
|
|
));
|
|
}
|
|
|
|
let Some(thumb_name) = thumb_name else {
|
|
return Ok((width, height));
|
|
};
|
|
|
|
let thumb_size = cfg.files.thumb_size;
|
|
|
|
let output = spawn_blocking(move || {
|
|
Command::new("ffmpeg")
|
|
.args([
|
|
"-i",
|
|
&format!("/tmp/{new_name}"),
|
|
"-ss",
|
|
"00:00:00.50",
|
|
"-vframes",
|
|
"1",
|
|
"-vf",
|
|
&format!(
|
|
"scale={}",
|
|
if width > height {
|
|
format!("{thumb_size}:-2")
|
|
} else {
|
|
format!("-2:{thumb_size}")
|
|
}
|
|
),
|
|
&format!("./uploads/thumb/{thumb_name}"),
|
|
])
|
|
.output()
|
|
})
|
|
.await??;
|
|
|
|
if !output.status.success() {
|
|
return Err(NekrochanError::FileError(
|
|
original_name,
|
|
"nepodařilo se vytvořit náhled videa",
|
|
));
|
|
}
|
|
|
|
Ok((width, height))
|
|
}
|