use bytes::Bytes; use db::conn::sea_orm::{ActiveModelTrait, EntityTrait, Set}; use db::user::users as user_entity; use object_store::path::Path as ObjectPath; use time::OffsetDateTime; use time::format_description::well_known::Rfc3339; use uuid::Uuid; use crate::error::{UserError, UserResult}; use crate::features::UserService; use super::types::{decode_upload, AvatarFormat, AvatarView, UploadAvatarCmd}; const AVATAR_PREFIX: &str = "avatars"; fn avatar_path(uid: Uuid, fmt: AvatarFormat) -> ObjectPath { ObjectPath::from(format!("{}/{}.{}", AVATAR_PREFIX, uid, fmt.extension())) } fn avatar_meta_path(uid: Uuid) -> ObjectPath { ObjectPath::from(format!("{}/{}.meta.json", AVATAR_PREFIX, uid)) } pub async fn upload(svc: &UserService, caller: Uuid, cmd: UploadAvatarCmd) -> UserResult { let (fmt, bytes) = decode_upload(&cmd)?; let new_path = avatar_path(caller, fmt); svc.storage .put(&new_path, Bytes::from(bytes.clone())) .await?; let now = OffsetDateTime::now_utc(); let stored_url = format!("storage://{}/{}.{}", AVATAR_PREFIX, caller, fmt.extension()); let writer = svc.db.writer(); let user = user_entity::Entity::find_by_id(caller) .one(&writer) .await? .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?; let mut active: user_entity::ActiveModel = user.into(); active.avatar = Set(stored_url.clone()); active.updated_at = Set(now); active.update(&writer).await?; let meta = serde_json::json!({ "uid": caller.to_string(), "format": fmt.extension(), "bytes": bytes.len(), "uploaded_at": now.format(&Rfc3339).unwrap_or_default(), }); let meta_bytes = serde_json::to_vec(&meta).unwrap_or_default(); let meta_path = avatar_meta_path(caller); let _ = svc.storage.put(&meta_path, Bytes::from(meta_bytes)).await; Ok(AvatarView { uid: caller, url: stored_url, format: fmt, bytes: bytes.len() as u64, updated_at: now.format(&Rfc3339).unwrap_or_default(), }) } pub async fn delete(svc: &UserService, caller: Uuid) -> UserResult<()> { let writer = svc.db.writer(); let user = user_entity::Entity::find_by_id(caller) .one(&writer) .await? .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?; let current = user.avatar.clone(); let now = OffsetDateTime::now_utc(); let mut active: user_entity::ActiveModel = user.into(); active.avatar = Set(String::new()); active.updated_at = Set(now); active.update(&writer).await?; if current.starts_with(&format!("storage://{}/", AVATAR_PREFIX)) { let rel = current.trim_start_matches("storage://").trim_start_matches(AVATAR_PREFIX).trim_start_matches('/'); let path = ObjectPath::from(format!("{}/{}", AVATAR_PREFIX, rel)); let _ = svc.storage.delete(&path).await; let _ = svc.storage.delete(&avatar_meta_path(caller)).await; } Ok(()) } pub async fn current(svc: &UserService, caller: Uuid) -> UserResult> { let db = svc.db.reader(); let user = user_entity::Entity::find_by_id(caller) .one(&db) .await? .ok_or_else(|| UserError::NotFound(format!("user {caller}")))?; if user.avatar.is_empty() { return Ok(None); } let fmt = detect_format_from_url(&user.avatar).unwrap_or(AvatarFormat::Jpg); Ok(Some(AvatarView { uid: caller, url: user.avatar.clone(), format: fmt, bytes: 0, updated_at: OffsetDateTime::now_utc().format(&Rfc3339).unwrap_or_default(), })) } fn detect_format_from_url(url: &str) -> Option { let ext = url.rsplit('.').next()?; AvatarFormat::from_extension(ext) } #[allow(dead_code)] fn _ensure_query(_q: &db::conn::sea_orm::Select) {}