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 orig_variant_id: None, file_id: None,
111 parent_id: None,
112 owner_tag: None,
113 creator_tag: Some(auth.id_tag.as_ref().into()),
114 content_type: content_type.into(),
115 file_name: format!("{}-profile-pic.jpg", auth.id_tag).into(),
116 file_tp: Some("BLOB".into()),
117 created_at: None,
118 tags: Some(vec!["profile".into()]),
119 x: Some(json!({ "dim": dim })),
120 visibility: Some('P'), status: None,
122 },
123 )
124 .await?;
125
126 let f_id = match f_id {
128 meta_adapter::FileId::FId(fid) => fid,
129 meta_adapter::FileId::FileId(fid) => {
130 app.meta_adapter
132 .update_tenant(
133 auth.tn_id,
134 &meta_adapter::UpdateTenantData {
135 profile_pic: Patch::Value(fid.to_string()),
136 ..Default::default()
137 },
138 )
139 .await?;
140 info!("User {} uploaded profile image (existing): {}", auth.id_tag, fid);
141 return Ok((
142 StatusCode::OK,
143 Json(json!({
144 "fileId": fid,
145 "type": "profile-pic"
146 })),
147 ));
148 }
149 };
150
151 let result =
153 image::generate_image_variants(&app, auth.tn_id, f_id, &image_data, &preset).await?;
154
155 app.scheduler
157 .task(TenantImageUpdaterTask::new(auth.tn_id, f_id, TenantImageType::ProfilePic))
158 .depend_on(vec![result.file_id_task])
159 .schedule()
160 .await?;
161
162 let pending_file_id = format!("@{}", f_id);
164
165 info!("User {} uploaded profile image: {}", auth.id_tag, pending_file_id);
166
167 Ok((
168 StatusCode::OK,
169 Json(json!({
170 "fileId": pending_file_id,
171 "type": "profile-pic"
172 })),
173 ))
174}
175
176pub async fn put_cover_image(
178 State(app): State<App>,
179 Auth(auth): Auth,
180 body: Bytes,
181) -> ClResult<(StatusCode, Json<serde_json::Value>)> {
182 let image_data = body.to_vec();
184
185 if image_data.is_empty() {
186 return Err(Error::ValidationError("No image data provided".into()));
187 }
188
189 let content_type = image::detect_image_type(&image_data)
191 .ok_or_else(|| Error::ValidationError("Invalid or unsupported image format".into()))?;
192
193 let dim = image::get_image_dimensions(&image_data).await?;
195 info!("Cover image dimensions: {}x{}", dim.0, dim.1);
196
197 let preset = preset::presets::cover();
199
200 let f_id = app
202 .meta_adapter
203 .create_file(
204 auth.tn_id,
205 meta_adapter::CreateFile {
206 preset: Some("cover".into()),
207 orig_variant_id: None, file_id: None,
209 parent_id: None,
210 owner_tag: None,
211 creator_tag: Some(auth.id_tag.as_ref().into()),
212 content_type: content_type.into(),
213 file_name: format!("{}-cover.jpg", auth.id_tag).into(),
214 file_tp: Some("BLOB".into()),
215 created_at: None,
216 tags: Some(vec!["cover".into()]),
217 x: Some(json!({ "dim": dim })),
218 visibility: Some('P'), status: None,
220 },
221 )
222 .await?;
223
224 let f_id = match f_id {
226 meta_adapter::FileId::FId(fid) => fid,
227 meta_adapter::FileId::FileId(fid) => {
228 app.meta_adapter
230 .update_tenant(
231 auth.tn_id,
232 &meta_adapter::UpdateTenantData {
233 cover_pic: Patch::Value(fid.to_string()),
234 ..Default::default()
235 },
236 )
237 .await?;
238 info!("User {} uploaded cover image (existing): {}", auth.id_tag, fid);
239 return Ok((
240 StatusCode::OK,
241 Json(json!({
242 "fileId": fid,
243 "type": "cover"
244 })),
245 ));
246 }
247 };
248
249 let result =
251 image::generate_image_variants(&app, auth.tn_id, f_id, &image_data, &preset).await?;
252
253 app.scheduler
255 .task(TenantImageUpdaterTask::new(auth.tn_id, f_id, TenantImageType::CoverPic))
256 .depend_on(vec![result.file_id_task])
257 .schedule()
258 .await?;
259
260 let pending_file_id = format!("@{}", f_id);
262
263 info!("User {} uploaded cover image: {}", auth.id_tag, pending_file_id);
264
265 Ok((
266 StatusCode::OK,
267 Json(json!({
268 "fileId": pending_file_id,
269 "type": "cover"
270 })),
271 ))
272}
273
274