create_rust_app/storage/
attachment.rs

1use diesel::result::Error;
2use diesel::QueryResult;
3//use md5;
4//use mime_guess;
5use 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    /// in `actix_web` we don't need to support send+sync handlers, so we can use the `&mut Connection` directly.
52    ///
53    /// # Errors
54    /// * Diesel error
55    #[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                // one already exists, we need to delete it
79                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                    // throw the error
85                    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            // attempt to delete the attachment
130            // if it fails, it fails
131            Self::detach(db, storage, attached.id).await?;
132        }
133
134        upload_result
135    }
136
137    /// in poem, we need to pass in the pool itself because the Connection is not Send+Sync which poem handlers require
138    ///
139    /// # Errors
140    /// * Diesel error
141    #[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                // one already exists, we need to delete it
167                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                    // throw the error
173                    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            // attempt to delete the attachment
218            // if it fails, it fails
219            Attachment::detach(pool.clone(), storage, attached.id).await?;
220        }
221
222        upload_result
223    }
224
225    /// in `actix_web` we don't need to support send+sync handlers, so we can use the &mut Connection directly.
226    ///
227    /// # Errors
228    /// * Diesel error
229    #[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            // we continue even if there's an error deleting the actual object
239            // todo: make this more robust by checking why it failed to delete the object
240            //       => is it because it didn't exist?
241            println!("{error}");
242        }
243
244        diesel::connection::Connection::transaction::<(), Error, _>(db, |db| {
245            // delete the attachment first because it references the blobs
246            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    /// in poem, we need to pass in the pool itself because the Connection is not Send+Sync which poem handlers require
257    ///
258    /// # Errors
259    /// * Diesel error
260    ///
261    /// # Panics
262    /// * If the pool is unable to get a connection
263    #[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            // we continue even if there's an error deleting the actual object
281            // todo: make this more robust by checking why it failed to delete the object
282            //       => is it because it didn't exist?
283            println!("{}", error);
284        }
285
286        diesel::connection::Connection::transaction::<(), Error, _>(&mut db, |db| {
287            // delete the attachment first because it references the blobs
288            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    /// # Errors
299    /// * Diesel error
300    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            // we continue even if there's an error deleting the actual object
328            // todo: make this more robust by checking why it failed to delete the objects
329            //       => is it because it didn't exist?
330            println!("{error}");
331        }
332
333        diesel::connection::Connection::transaction::<(), Error, _>(db, |db| {
334            // delete the attachments first because they reference the blobs
335            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    /// Find an attachment for a given record type and record id
358    ///
359    /// # Errors
360    /// * Diesel error
361    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    /// Find all attachments for a given record type and record id
375    ///
376    /// # Errors
377    /// * Diesel error
378    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    /// Find all attachments for a given record type and record ids
392    ///
393    /// # Errors
394    /// * Diesel error
395    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 update(db: &mut Connection, item_id: ID, item: &AttachmentChangeset) -> QueryResult<Self> {
409    //     use super::schema::attachments::dsl::*;
410    //
411    //     diesel::update(attachments.filter(id.eq(item_id)))
412    //         .set(item)
413    //         .get_result(db)
414    // }
415
416    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}