Skip to main content

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