nekrochan/src/files.rs

300 řádky
8.1 KiB
Rust
Spustitelný soubor

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<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 (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<String>,
) -> 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<String>,
) -> 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))
}