gitdataai/libs/avatar/lib.rs
ZhenYi 14f6e1e500 feat(core): initialize project with access control and AI integration
- Add gitignore and prettier configuration files for project scaffolding
- Implement room access control service with project member verification
- Create user access key management with CRUD operations and activity logging
- Add accordion UI component for frontend expandable sections
- Implement room AI configuration with list, upsert, and delete operations
- Add AI event types for agent join/leave/status change tracking
- Create streaming AI processing services for mode and react patterns
- Build room AI service with model detection and idempotency handling
- Integrate chat service orchestration for AI message processing
- Add typing indicators and stream cancellation for AI interactions
- Implement mention parsing and context extraction for AI agents
2026-05-03 06:04:31 +08:00

95 lines
3.3 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() {
std::fs::create_dir_all(&path)?;
}
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(())
}
}