1use diesel::result::Error;
2use diesel::QueryResult;
3use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8use crate::diesel::{
9 insert_into, AsChangeset, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable,
10 RunQueryDsl,
11};
12use crate::storage::attachment_blob::AttachmentBlobChangeset;
13use crate::storage::{schema, AttachmentBlob, Utc, ID};
14use crate::Connection;
15
16use super::{schema::attachments, Storage};
17
18#[allow(clippy::module_name_repetitions)]
19#[derive(
20 Debug, Serialize, Deserialize, Clone, Queryable, Insertable, Identifiable, AsChangeset,
21)]
22#[diesel(table_name=attachments)]
23pub struct Attachment {
24 pub id: ID,
25
26 pub name: String,
27 pub record_type: String,
28 pub record_id: ID,
29 pub blob_id: ID,
30
31 pub created_at: Utc,
32}
33
34#[allow(clippy::module_name_repetitions)]
35#[derive(Debug, Serialize, Deserialize, Clone, Insertable, AsChangeset)]
36#[diesel(table_name=attachments)]
37pub struct AttachmentChangeset {
38 pub name: String,
39 pub record_type: String,
40 pub record_id: ID,
41 pub blob_id: ID,
42}
43
44#[allow(clippy::module_name_repetitions)]
45pub struct AttachmentData {
46 pub data: Vec<u8>,
47 pub file_name: Option<String>,
48}
49
50impl Attachment {
51 #[allow(clippy::too_many_arguments)]
56 #[cfg(feature = "backend_actix-web")]
57 pub async fn attach(
58 db: &mut Connection,
59 storage: &Storage,
60 name: String,
61 record_type: String,
62 record_id: ID,
63 data: AttachmentData,
64 allow_multiple: bool,
65 overwrite_existing: bool,
66 ) -> Result<String, String> {
67 let checksum = format!("{:x}", md5::compute(&data.data));
68 let file_name = data.file_name.clone();
69 let content_type = file_name
70 .and_then(|f| mime_guess::from_path(f).first_raw())
71 .map(std::string::ToString::to_string);
72 let key = Uuid::new_v4().to_string();
73
74 if !allow_multiple {
75 if let Ok(existing) =
76 Self::find_for_record(db, name.clone(), record_type.clone(), record_id)
77 {
78 if overwrite_existing {
80 Self::detach(db, storage, existing.id).await.map_err(|_| {
81 format!("Could not detach the existing attachment for '{name}' attachment on '{record_type}'", name=name.clone(), record_type=record_type.clone())
82 })?;
83 } else {
84 return Err(format!("Only 1 attachment is allowed for '{name}' type attachments on '{record_type}'", name=name.clone(), record_type=record_type.clone()));
86 }
87 }
88 }
89
90 let attached = diesel::connection::Connection::transaction::<Self, Error, _>(db, |db| {
91 let blob = AttachmentBlob::create(
92 db,
93 #[allow(clippy::cast_possible_wrap)]
94 &AttachmentBlobChangeset {
95 byte_size: data.data.len() as i64,
96 service_name: "s3".to_string(),
97 key: key.clone(),
98 checksum: checksum.clone(),
99 content_type: content_type.clone(),
100 file_name: data.file_name.clone().unwrap_or_default(),
101 },
102 )?;
103
104 let attached = Self::create(
105 db,
106 &AttachmentChangeset {
107 blob_id: blob.id,
108 record_id,
109 record_type,
110 name,
111 },
112 )?;
113
114 Ok(attached)
115 })
116 .map_err(|err| err.to_string())?;
117
118 let upload_result = storage
119 .upload(
120 key.clone(),
121 data.data,
122 content_type.clone().unwrap_or_default(),
123 checksum.clone(),
124 )
125 .await
126 .map(|()| key);
127
128 if upload_result.is_err() {
129 Self::detach(db, storage, attached.id).await?;
132 }
133
134 upload_result
135 }
136
137 #[allow(clippy::too_many_arguments)]
142 #[cfg(feature = "backend_poem")]
143 pub async fn attach(
144 pool: std::sync::Arc<&crate::database::Pool>,
145 storage: &Storage,
146 name: String,
147 record_type: String,
148 record_id: ID,
149 data: AttachmentData,
150 allow_multiple: bool,
151 overwrite_existing: bool,
152 ) -> Result<String, String> {
153 let mut db = pool.clone().get().unwrap();
154
155 let checksum = format!("{:x}", md5::compute(&data.data));
156 let file_name = data.file_name.clone();
157 let content_type = file_name
158 .and_then(|f| mime_guess::from_path(f).first_raw())
159 .map(std::string::ToString::to_string);
160 let key = Uuid::new_v4().to_string();
161
162 if !allow_multiple {
163 if let Ok(existing) =
164 Self::find_for_record(&mut db, name.clone(), record_type.clone(), record_id)
165 {
166 if overwrite_existing {
168 Self::detach(pool.clone(), storage, existing.id).await.map_err(|_| {
169 format!("Could not detach the existing attachment for '{name}' attachment on '{record_type}'", name=name.clone(), record_type=record_type.clone())
170 })?;
171 } else {
172 return Err(format!("Only 1 attachment is allowed for '{name}' type attachments on '{record_type}'", name=name.clone(), record_type=record_type.clone()));
174 }
175 }
176 }
177
178 let attached =
179 diesel::connection::Connection::transaction::<Self, Error, _>(&mut db, |db| {
180 let blob = AttachmentBlob::create(
181 db,
182 &AttachmentBlobChangeset {
183 byte_size: data.data.len() as i64,
184 service_name: "s3".to_string(),
185 key: key.clone(),
186 checksum: checksum.clone(),
187 content_type: content_type.clone(),
188 file_name: data.file_name.clone().unwrap_or(String::new()),
189 },
190 )?;
191
192 let attached = Attachment::create(
193 db,
194 &AttachmentChangeset {
195 blob_id: blob.id,
196 record_id,
197 record_type,
198 name,
199 },
200 )?;
201
202 Ok(attached)
203 })
204 .map_err(|err| err.to_string())?;
205
206 let upload_result = storage
207 .upload(
208 key.clone(),
209 data.data,
210 content_type.clone().unwrap_or("".to_string()),
211 checksum.clone(),
212 )
213 .await
214 .map(|_| key);
215
216 if upload_result.is_err() {
217 Attachment::detach(pool.clone(), storage, attached.id).await?;
220 }
221
222 upload_result
223 }
224
225 #[cfg(feature = "backend_actix-web")]
230 pub async fn detach(db: &mut Connection, storage: &Storage, item_id: ID) -> Result<(), String> {
231 let attached = Self::find_by_id(db, item_id).map_err(|_| "Could not load attachment")?;
232 let blob = AttachmentBlob::find_by_id(db, attached.blob_id)
233 .map_err(|_| "Could not load attachment blob")?;
234
235 let delete_result = storage.delete(blob.key.clone()).await;
236
237 if let Err(error) = delete_result {
238 println!("{error}");
242 }
243
244 diesel::connection::Connection::transaction::<(), Error, _>(db, |db| {
245 Self::delete(db, attached.id)?;
247 AttachmentBlob::delete(db, blob.id)?;
248
249 Ok(())
250 })
251 .map_err(|err| err.to_string())?;
252
253 Ok(())
254 }
255
256 #[allow(clippy::too_many_arguments)]
264 #[cfg(feature = "backend_poem")]
265 pub async fn detach(
266 pool: std::sync::Arc<&crate::database::Pool>,
267 storage: &Storage,
268 item_id: ID,
269 ) -> Result<(), String> {
270 let mut db = pool.get().unwrap();
271
272 let attached =
273 Self::find_by_id(&mut db, item_id).map_err(|_| "Could not load attachment")?;
274 let blob = AttachmentBlob::find_by_id(&mut db, attached.blob_id)
275 .map_err(|_| "Could not load attachment blob")?;
276
277 let delete_result = storage.delete(blob.key.clone()).await;
278
279 if let Err(error) = delete_result {
280 println!("{}", error);
284 }
285
286 diesel::connection::Connection::transaction::<(), Error, _>(&mut db, |db| {
287 Self::delete(db, attached.id)?;
289 AttachmentBlob::delete(db, blob.id)?;
290
291 Ok(())
292 })
293 .map_err(|err| err.to_string())?;
294
295 Ok(())
296 }
297
298 pub async fn detach_all(
301 db: &mut Connection,
302 storage: &Storage,
303 name: String,
304 record_type: String,
305 record_id: ID,
306 ) -> Result<(), String> {
307 let attached = Self::find_all_for_record(db, name, record_type, record_id)
308 .map_err(|_| "Could not load attachments")?;
309 let attached_ids = attached
310 .iter()
311 .map(|attached| attached.id)
312 .collect::<Vec<_>>();
313 let blob_ids = attached
314 .iter()
315 .map(|attached| attached.blob_id)
316 .collect::<Vec<_>>();
317 let blobs = AttachmentBlob::find_all_by_id(db, blob_ids.clone())
318 .map_err(|_| "Could not load attachment blobs")?;
319 let keys = blobs
320 .iter()
321 .map(|blob| blob.key.to_string())
322 .collect::<Vec<_>>();
323
324 let delete_result = storage.delete_many(keys).await;
325
326 if let Err(error) = delete_result {
327 println!("{error}");
331 }
332
333 diesel::connection::Connection::transaction::<(), Error, _>(db, |db| {
334 Self::delete_all(db, attached_ids)?;
336 AttachmentBlob::delete_all(db, blob_ids)?;
337
338 Ok(())
339 })
340 .map_err(|err| err.to_string())?;
341
342 Ok(())
343 }
344
345 fn create(db: &mut Connection, item: &AttachmentChangeset) -> QueryResult<Self> {
346 use super::schema::attachments::dsl::attachments;
347
348 insert_into(attachments).values(item).get_result::<Self>(db)
349 }
350
351 fn find_by_id(db: &mut Connection, item_id: ID) -> QueryResult<Self> {
352 schema::attachments::table
353 .filter(schema::attachments::id.eq(item_id))
354 .first(db)
355 }
356
357 pub fn find_for_record(
362 db: &mut Connection,
363 item_name: String,
364 item_record_type: String,
365 item_record_id: ID,
366 ) -> QueryResult<Self> {
367 schema::attachments::table
368 .filter(schema::attachments::name.eq(item_name))
369 .filter(schema::attachments::record_type.eq(item_record_type))
370 .filter(schema::attachments::record_id.eq(item_record_id))
371 .first::<Self>(db)
372 }
373
374 pub fn find_all_for_record(
379 db: &mut Connection,
380 item_name: String,
381 item_record_type: String,
382 item_record_id: ID,
383 ) -> QueryResult<Vec<Self>> {
384 schema::attachments::table
385 .filter(schema::attachments::name.eq(item_name))
386 .filter(schema::attachments::record_type.eq(item_record_type))
387 .filter(schema::attachments::record_id.eq(item_record_id))
388 .get_results::<Self>(db)
389 }
390
391 pub fn find_all_for_records(
396 db: &mut Connection,
397 item_name: String,
398 item_record_type: String,
399 item_record_ids: Vec<ID>,
400 ) -> QueryResult<Vec<Self>> {
401 schema::attachments::table
402 .filter(schema::attachments::name.eq(item_name))
403 .filter(schema::attachments::record_type.eq(item_record_type))
404 .filter(schema::attachments::record_id.eq_any(item_record_ids))
405 .get_results::<Self>(db)
406 }
407
408 fn delete(db: &mut Connection, item_id: ID) -> QueryResult<usize> {
417 use super::schema::attachments::dsl::attachments;
418
419 diesel::delete(attachments.filter(schema::attachments::id.eq(item_id))).execute(db)
420 }
421
422 fn delete_all(db: &mut Connection, item_ids: Vec<ID>) -> QueryResult<usize> {
423 use super::schema::attachments::dsl::attachments;
424
425 diesel::delete(attachments.filter(schema::attachments::id.eq_any(item_ids))).execute(db)
426 }
427}