docbox_core/files/
generated.rs

1//! Business logic for working with generated files
2
3use crate::files::create_generated_file_key;
4use anyhow::Context;
5use bytes::Bytes;
6use docbox_storage::TenantStorageLayer;
7use futures::{
8    StreamExt,
9    stream::{FuturesOrdered, FuturesUnordered},
10};
11use mime::Mime;
12use tracing::{Instrument, debug, error};
13
14use docbox_database::models::{
15    file::FileId,
16    generated_file::{CreateGeneratedFile, GeneratedFile, GeneratedFileId, GeneratedFileType},
17};
18
19#[derive(Debug)]
20pub struct QueuedUpload {
21    pub mime: Mime,
22    pub ty: GeneratedFileType,
23    pub bytes: Bytes,
24}
25
26impl QueuedUpload {
27    pub fn new(mime: Mime, ty: GeneratedFileType, bytes: Bytes) -> Self {
28        Self { mime, ty, bytes }
29    }
30}
31
32pub enum GeneratedFileDeleteResult {
33    /// Successful upload of all files
34    Ok,
35    /// Error path contains any files that were upload
36    /// along with the error that occurred
37    Err(Vec<GeneratedFileId>, anyhow::Error),
38}
39
40pub async fn delete_generated_files(
41    storage: &TenantStorageLayer,
42    files: &[GeneratedFile],
43) -> GeneratedFileDeleteResult {
44    let files_count = files.len();
45
46    let mut futures = files
47        .iter()
48        .map(|file| {
49            async {
50                let id = file.id;
51                let file_id = file.file_id;
52                let file_key = file.file_key.to_string();
53
54                debug!(%id, %file_id, %file_key, "uploading file to s3",);
55
56                // Delete file from S3
57                if let Err(cause) = storage.delete_file(&file_key).await {
58                    error!(%id, %file_id, %file_key, ?cause, "failed to delete generated file");
59                }
60
61                debug!("deleted file from s3");
62
63                anyhow::Ok(id)
64            }
65        })
66        .collect::<FuturesUnordered<_>>();
67
68    let mut deleted: Vec<GeneratedFileId> = Vec::with_capacity(files_count);
69
70    while let Some(result) = futures.next().await {
71        match result {
72            Ok(id) => deleted.push(id),
73            Err(err) => return GeneratedFileDeleteResult::Err(deleted, err),
74        }
75    }
76
77    GeneratedFileDeleteResult::Ok
78}
79
80/// Triggers the file uploads returning a list of the [CreateGeneratedFile] structures
81/// to persist to the database
82pub async fn upload_generated_files(
83    storage: &TenantStorageLayer,
84    base_file_key: &str,
85    file_id: &FileId,
86    file_hash: &str,
87    queued_uploads: Vec<QueuedUpload>,
88) -> Vec<anyhow::Result<CreateGeneratedFile>> {
89    queued_uploads
90        .into_iter()
91        .map(|queued_upload| {
92            // Task needs its own copies of state
93            let file_id = *file_id;
94            let file_hash = file_hash.to_string();
95            let file_mime = queued_upload.mime.to_string();
96            let file_key = create_generated_file_key(base_file_key, &queued_upload.mime);
97            let span = tracing::info_span!("upload_generated_files", %file_id, %file_hash, %file_key, %file_mime);
98
99            async move {
100                // Upload the file to S3
101                storage
102                    .upload_file(&file_key, file_mime, queued_upload.bytes)
103                    .await
104                    .context("failed to upload generated file")?;
105
106                anyhow::Ok(CreateGeneratedFile {
107                    file_id,
108                    hash: file_hash,
109                    mime: queued_upload.mime.to_string(),
110                    ty: queued_upload.ty,
111                    file_key,
112                })
113            }
114            .instrument(span)
115        })
116        .collect::<FuturesOrdered<_>>()
117        .collect()
118        .await
119}