use actix_multipart::form::tempfile::TempFile; use chrono::Utc; use std::process::Command; use tokio::{ fs::{copy, remove_file}, 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 { 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 (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) }; let path = temp_file.file.path().to_string_lossy().to_string(); let (width, height) = if video { process_video(cfg, original_name.clone(), path.clone(), thumb_name).await? } else { process_image(cfg, original_name.clone(), path.clone(), thumb_name).await? }; copy(path, format!("./uploads/{timestamp}.{format}")).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, path: String, thumb_name: Option, ) -> Result<(u32, u32), NekrochanError> { let path_ = path.clone(); let identify_out = spawn_blocking(move || { Command::new("identify") .args(["-format", "%wx%h", &format!("{path_}[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(path) .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, path: String, thumb_name: Option, ) -> Result<(u32, u32), NekrochanError> { let path_ = path.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", &path_, ]) .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", &path, "-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)) }