docbox_core/files/
generated.rs

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