AppAvatar::init() create_dir_all failure now logs a warning instead of failing fatally. This fixes the email worker crash — it runs AppService but has no PVC mount, so /data/avatars is not accessible. Other services (app) have the PVC mounted and are unaffected.
97 lines
3.4 KiB
Rust
97 lines
3.4 KiB
Rust
use config::AppConfig;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct AppAvatar {
|
|
pub basic_path: PathBuf,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
pub struct AvatarLoad {
|
|
w: Option<u32>,
|
|
h: Option<u32>,
|
|
}
|
|
|
|
impl AppAvatar {
|
|
pub async fn init(cfg: &AppConfig) -> anyhow::Result<Self> {
|
|
let path = cfg.avatar_path()?;
|
|
if std::fs::read_dir(&path).is_err() {
|
|
if let Err(e) = std::fs::create_dir_all(&path) {
|
|
tracing::warn!(path = %path, error = %e, "Avatar directory not available, avatars disabled");
|
|
}
|
|
}
|
|
let basic_path = PathBuf::from(path);
|
|
Ok(Self { basic_path })
|
|
}
|
|
pub async fn upload(&self, file: Vec<u8>, file_name: String, ext: &str) -> anyhow::Result<()> {
|
|
// Validate file size (max 5MB)
|
|
if file.len() > 5 * 1024 * 1024 {
|
|
anyhow::bail!("File size exceeds 5MB limit");
|
|
}
|
|
|
|
// Validate extension
|
|
let allowed_exts = ["png", "jpg", "jpeg", "gif", "webp"];
|
|
if !allowed_exts.contains(&ext) {
|
|
anyhow::bail!("Invalid file extension: {}", ext);
|
|
}
|
|
|
|
// Sanitize filename to prevent path traversal
|
|
let sanitized_name = file_name.replace(['/', '\\', '.', ':'], "_");
|
|
if sanitized_name.is_empty() || sanitized_name.len() > 255 {
|
|
anyhow::bail!("Invalid filename");
|
|
}
|
|
|
|
let image = image::load_from_memory(&*file)?;
|
|
image.save(self.basic_path.join(format!("{}.{}", sanitized_name, ext)))?;
|
|
Ok(())
|
|
}
|
|
pub async fn load(&self, file_name: String, load: AvatarLoad) -> anyhow::Result<Vec<u8>> {
|
|
// Sanitize filename to prevent path traversal
|
|
let sanitized_name = file_name.replace(['/', '\\', '.', ':'], "_");
|
|
if sanitized_name.is_empty() || sanitized_name.len() > 255 {
|
|
anyhow::bail!("Invalid filename");
|
|
}
|
|
|
|
let path = self.basic_path.join(format!("{}.png", sanitized_name));
|
|
|
|
// Verify path is within basic_path
|
|
let canonical_path = path.canonicalize().unwrap_or(path.clone());
|
|
if !canonical_path.starts_with(&self.basic_path) {
|
|
anyhow::bail!("Path traversal detected");
|
|
}
|
|
|
|
let image = image::open(canonical_path)?;
|
|
let (w, h) = (
|
|
load.w.unwrap_or(image.width()),
|
|
load.h.unwrap_or(image.height()),
|
|
);
|
|
let image = image.resize(w, h, image::imageops::FilterType::Nearest);
|
|
Ok(image.as_bytes().to_vec())
|
|
}
|
|
pub async fn delete(&self, file_name: String, ext: &str) -> anyhow::Result<()> {
|
|
// Validate extension
|
|
let allowed_exts = ["png", "jpg", "jpeg", "gif", "webp"];
|
|
if !allowed_exts.contains(&ext) {
|
|
anyhow::bail!("Invalid file extension: {}", ext);
|
|
}
|
|
|
|
// Sanitize filename to prevent path traversal
|
|
let sanitized_name = file_name.replace(['/', '\\', '.', ':'], "_");
|
|
if sanitized_name.is_empty() || sanitized_name.len() > 255 {
|
|
anyhow::bail!("Invalid filename");
|
|
}
|
|
|
|
let path = self.basic_path.join(format!("{}.{}", sanitized_name, ext));
|
|
|
|
// Verify path is within basic_path
|
|
let canonical_path = path.canonicalize().unwrap_or(path.clone());
|
|
if !canonical_path.starts_with(&self.basic_path) {
|
|
anyhow::bail!("Path traversal detected");
|
|
}
|
|
|
|
std::fs::remove_file(canonical_path)?;
|
|
Ok(())
|
|
}
|
|
}
|