1use async_trait::async_trait;
4use axum::{body::Bytes, extract::State, http::StatusCode, Json};
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7use std::sync::Arc;
8
9use crate::prelude::*;
10use cloudillo_core::extract::Auth;
11use cloudillo_core::scheduler::{Task, TaskId};
12use cloudillo_file::{image, preset};
13use cloudillo_types::meta_adapter;
14
15#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
17pub enum TenantImageType {
18 ProfilePic,
19 CoverPic,
20}
21
22#[derive(Debug, Serialize, Deserialize)]
24pub struct TenantImageUpdaterTask {
25 tn_id: TnId,
26 f_id: u64,
27 image_type: TenantImageType,
28}
29
30impl TenantImageUpdaterTask {
31 pub fn new(tn_id: TnId, f_id: u64, image_type: TenantImageType) -> Arc<Self> {
32 Arc::new(Self { tn_id, f_id, image_type })
33 }
34}
35
36#[async_trait]
37impl Task<App> for TenantImageUpdaterTask {
38 fn kind() -> &'static str {
39 "tenant.image-update"
40 }
41 fn kind_of(&self) -> &'static str {
42 Self::kind()
43 }
44
45 fn build(_id: TaskId, ctx: &str) -> ClResult<Arc<dyn Task<App>>> {
46 let task: TenantImageUpdaterTask = serde_json::from_str(ctx)
47 .map_err(|_| Error::Internal("invalid TenantImageUpdaterTask context".into()))?;
48 Ok(Arc::new(task))
49 }
50
51 fn serialize(&self) -> String {
52 serde_json::to_string(self).unwrap_or_default()
53 }
54
55 async fn run(&self, app: &App) -> ClResult<()> {
56 let file_id = app.meta_adapter.get_file_id(self.tn_id, self.f_id).await?;
58
59 let update = match self.image_type {
61 TenantImageType::ProfilePic => meta_adapter::UpdateTenantData {
62 profile_pic: Patch::Value(file_id.to_string()),
63 ..Default::default()
64 },
65 TenantImageType::CoverPic => meta_adapter::UpdateTenantData {
66 cover_pic: Patch::Value(file_id.to_string()),
67 ..Default::default()
68 },
69 };
70
71 app.meta_adapter.update_tenant(self.tn_id, &update).await?;
72
73 info!("Updated tenant {} {:?} to {}", self.tn_id, self.image_type, file_id);
74 Ok(())
75 }
76}
77
78pub async fn put_profile_image(
80 State(app): State<App>,
81 Auth(auth): Auth,
82 body: Bytes,
83) -> ClResult<(StatusCode, Json<serde_json::Value>)> {
84 let image_data = body.to_vec();
86
87 if image_data.is_empty() {
88 return Err(Error::ValidationError("No image data provided".into()));
89 }
90
91 let content_type = image::detect_image_type(&image_data)
93 .ok_or_else(|| Error::ValidationError("Invalid or unsupported image format".into()))?;
94
95 let dim = image::get_image_dimensions(&image_data).await?;
97 info!("Profile image dimensions: {}x{}", dim.0, dim.1);
98
99 let preset = preset::presets::profile_picture();
101
102 let f_id = app
104 .meta_adapter
105 .create_file(
106 auth.tn_id,
107 meta_adapter::CreateFile {
108 preset: Some("profile-picture".into()),
109 creator_tag: Some(auth.id_tag.as_ref().into()),
110 content_type: content_type.into(),
111 file_name: format!("{}-profile-pic.jpg", auth.id_tag).into(),
112 file_tp: Some("BLOB".into()),
113 tags: Some(vec!["profile".into()]),
114 x: Some(json!({ "dim": dim })),
115 visibility: Some('P'), ..Default::default()
117 },
118 )
119 .await?;
120
121 let f_id = match f_id {
123 meta_adapter::FileId::FId(fid) => fid,
124 meta_adapter::FileId::FileId(fid) => {
125 app.meta_adapter
127 .update_tenant(
128 auth.tn_id,
129 &meta_adapter::UpdateTenantData {
130 profile_pic: Patch::Value(fid.to_string()),
131 ..Default::default()
132 },
133 )
134 .await?;
135 info!("User {} uploaded profile image (existing): {}", auth.id_tag, fid);
136 return Ok((
137 StatusCode::OK,
138 Json(json!({
139 "fileId": fid,
140 "type": "profile-pic"
141 })),
142 ));
143 }
144 };
145
146 let result =
148 image::generate_image_variants(&app, auth.tn_id, f_id, &image_data, &preset).await?;
149
150 app.scheduler
152 .task(TenantImageUpdaterTask::new(auth.tn_id, f_id, TenantImageType::ProfilePic))
153 .depend_on(vec![result.file_id_task])
154 .schedule()
155 .await?;
156
157 let pending_file_id = format!("@{}", f_id);
159
160 info!("User {} uploaded profile image: {}", auth.id_tag, pending_file_id);
161
162 Ok((
163 StatusCode::OK,
164 Json(json!({
165 "fileId": pending_file_id,
166 "type": "profile-pic"
167 })),
168 ))
169}
170
171pub async fn put_cover_image(
173 State(app): State<App>,
174 Auth(auth): Auth,
175 body: Bytes,
176) -> ClResult<(StatusCode, Json<serde_json::Value>)> {
177 let image_data = body.to_vec();
179
180 if image_data.is_empty() {
181 return Err(Error::ValidationError("No image data provided".into()));
182 }
183
184 let content_type = image::detect_image_type(&image_data)
186 .ok_or_else(|| Error::ValidationError("Invalid or unsupported image format".into()))?;
187
188 let dim = image::get_image_dimensions(&image_data).await?;
190 info!("Cover image dimensions: {}x{}", dim.0, dim.1);
191
192 let preset = preset::presets::cover();
194
195 let f_id = app
197 .meta_adapter
198 .create_file(
199 auth.tn_id,
200 meta_adapter::CreateFile {
201 preset: Some("cover".into()),
202 creator_tag: Some(auth.id_tag.as_ref().into()),
203 content_type: content_type.into(),
204 file_name: format!("{}-cover.jpg", auth.id_tag).into(),
205 file_tp: Some("BLOB".into()),
206 tags: Some(vec!["cover".into()]),
207 x: Some(json!({ "dim": dim })),
208 visibility: Some('P'), ..Default::default()
210 },
211 )
212 .await?;
213
214 let f_id = match f_id {
216 meta_adapter::FileId::FId(fid) => fid,
217 meta_adapter::FileId::FileId(fid) => {
218 app.meta_adapter
220 .update_tenant(
221 auth.tn_id,
222 &meta_adapter::UpdateTenantData {
223 cover_pic: Patch::Value(fid.to_string()),
224 ..Default::default()
225 },
226 )
227 .await?;
228 info!("User {} uploaded cover image (existing): {}", auth.id_tag, fid);
229 return Ok((
230 StatusCode::OK,
231 Json(json!({
232 "fileId": fid,
233 "type": "cover"
234 })),
235 ));
236 }
237 };
238
239 let result =
241 image::generate_image_variants(&app, auth.tn_id, f_id, &image_data, &preset).await?;
242
243 app.scheduler
245 .task(TenantImageUpdaterTask::new(auth.tn_id, f_id, TenantImageType::CoverPic))
246 .depend_on(vec![result.file_id_task])
247 .schedule()
248 .await?;
249
250 let pending_file_id = format!("@{}", f_id);
252
253 info!("User {} uploaded cover image: {}", auth.id_tag, pending_file_id);
254
255 Ok((
256 StatusCode::OK,
257 Json(json!({
258 "fileId": pending_file_id,
259 "type": "cover"
260 })),
261 ))
262}
263
264