d5939db
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<AvatarView> {
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<Option<AvatarView>> {
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<AvatarFormat> {
let ext = url.rsplit('.').next()?;
AvatarFormat::from_extension(ext)
}
#[allow(dead_code)]
fn _ensure_query(_q: &db::conn::sea_orm::Select<user_entity::Entity>) {}