docbox_core/files/
generated.rs

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