docbox_core/files/
delete_file.rs

1use crate::{
2    events::{TenantEventMessage, TenantEventPublisher},
3    files::generated::{GeneratedFileDeleteResult, delete_generated_files},
4};
5use docbox_database::{
6    DbErr, DbPool,
7    models::{
8        document_box::{DocumentBoxScopeRaw, WithScope},
9        file::File,
10        generated_file::GeneratedFile,
11    },
12};
13use docbox_search::TenantSearchIndex;
14use docbox_storage::TenantStorageLayer;
15use futures::{StreamExt, stream::FuturesUnordered};
16use thiserror::Error;
17use tracing::error;
18
19#[derive(Debug, Error)]
20pub enum DeleteFileError {
21    /// Failed to delete the search index
22    #[error("failed to delete tenant search index: {0}")]
23    DeleteIndex(anyhow::Error),
24
25    /// Database error
26    #[error(transparent)]
27    Database(#[from] DbErr),
28
29    /// Failed to remove file from storage
30    #[error("failed to remove file from storage: {0}")]
31    DeleteFileStorage(anyhow::Error),
32
33    /// Failed to remove generated file from storage
34    #[error("failed to remove generated file from storage: {0}")]
35    DeleteGeneratedFileStorage(anyhow::Error),
36}
37
38/// Deletes a file and all associated generated files.
39///
40/// Deletes files from S3 before deleting the database metadata to
41/// prevent dangling files in the bucket. Same goes for the search
42/// index
43///
44/// This process cannot be rolled back since the changes to S3 are
45/// permanent. If a failure occurs after generated files are deleted
46/// they will stay deleted.
47///
48/// We may choose to revise this to load the generated files into memory
49/// in order to restore them on failure.
50pub async fn delete_file(
51    db: &DbPool,
52    storage: &TenantStorageLayer,
53    search: &TenantSearchIndex,
54    events: &TenantEventPublisher,
55    file: File,
56    scope: DocumentBoxScopeRaw,
57) -> Result<(), DeleteFileError> {
58    let generated = GeneratedFile::find_all(db, file.id)
59        .await
60        .inspect_err(|error| tracing::error!(?error, "failed to query generated files"))?;
61
62    match delete_generated_files(storage, &generated).await {
63        GeneratedFileDeleteResult::Ok => {}
64        GeneratedFileDeleteResult::Err(deleted, err) => {
65            // Attempt to delete generated files from db that were deleted from S3
66            let mut delete_files_future = generated
67                .into_iter()
68                .filter(|file| deleted.contains(&file.id))
69                .map(|file| file.delete(db))
70                .collect::<FuturesUnordered<_>>();
71
72            // Ignore errors from this point, they are not recoverable
73            while let Some(result) = delete_files_future.next().await {
74                if let Err(cause) = result {
75                    tracing::error!(?cause, "failed to delete generated file from db");
76                }
77            }
78
79            return Err(DeleteFileError::DeleteGeneratedFileStorage(err));
80        }
81    }
82
83    let mut delete_files_future = generated
84        .into_iter()
85        .map(|file| file.delete(db))
86        .collect::<FuturesUnordered<_>>();
87
88    // Delete the generated files from the database
89    while let Some(result) = delete_files_future.next().await {
90        if let Err(cause) = result {
91            tracing::error!(?cause, "failed to delete generated file");
92            return Err(DeleteFileError::Database(cause));
93        }
94    }
95
96    // Delete the file from S3
97    storage
98        .delete_file(&file.file_key)
99        .await
100        .map_err(DeleteFileError::DeleteFileStorage)?;
101
102    // Delete the indexed file contents
103    search
104        .delete_data(file.id)
105        .await
106        .map_err(DeleteFileError::DeleteIndex)?;
107
108    // Delete the file itself
109    file.delete(db)
110        .await
111        .inspect_err(|error| tracing::error!(?error, "failed to delete file from database"))?;
112
113    // Publish an event
114    events.publish_event(TenantEventMessage::FileDeleted(WithScope::new(file, scope)));
115
116    Ok(())
117}