docbox_core/folders/
delete_folder.rs

1use crate::events::{TenantEventMessage, TenantEventPublisher};
2use crate::files::delete_file::{DeleteFileError, delete_file};
3use crate::links::delete_link::{DeleteLinkError, delete_link};
4use docbox_database::{
5    DbPool,
6    models::{
7        document_box::WithScope,
8        file::File,
9        folder::{Folder, ResolvedFolder},
10        link::Link,
11    },
12};
13use docbox_search::{SearchError, TenantSearchIndex};
14use docbox_storage::TenantStorageLayer;
15use std::collections::VecDeque;
16use thiserror::Error;
17
18/// Item to be removed
19pub enum RemoveStackItem {
20    /// Folder that needs to have its children removed
21    Folder(Folder),
22    /// Folder that has already been processed, all children
23    /// should have been removed by previous stack passes
24    EmptyFolder(Folder),
25    /// File to remove
26    File(File),
27    /// Link to remove
28    Link(Link),
29}
30
31#[derive(Debug, Error)]
32pub enum DeleteFolderError {
33    #[error("failed to resolve folder for deletion")]
34    ResolveFolder,
35    #[error(transparent)]
36    Folder(#[from] InternalDeleteFolderError),
37    #[error(transparent)]
38    File(#[from] DeleteFileError),
39    #[error(transparent)]
40    Link(#[from] DeleteLinkError),
41}
42
43#[derive(Debug, Error)]
44pub enum InternalDeleteFolderError {
45    #[error(transparent)]
46    Search(#[from] SearchError),
47
48    #[error("failed to delete folder metadata")]
49    Database,
50}
51
52pub async fn delete_folder(
53    db: &DbPool,
54    storage: &TenantStorageLayer,
55    search: &TenantSearchIndex,
56    events: &TenantEventPublisher,
57    folder: Folder,
58) -> Result<(), DeleteFolderError> {
59    // Stack to store the next item to delete
60    let mut stack = VecDeque::new();
61
62    let document_box = folder.document_box.clone();
63
64    // Push the first folder item
65    stack.push_back(RemoveStackItem::Folder(folder));
66
67    while let Some(item) = stack.pop_front() {
68        match item {
69            RemoveStackItem::Folder(folder) => {
70                // Resolve the folder children
71                let resolved = ResolvedFolder::resolve(db, &folder)
72                    .await
73                    .map_err(|error| {
74                        tracing::error!(?error, "failed to resolve folder for deletion");
75                        DeleteFolderError::ResolveFolder
76                    })?;
77
78                // Push the empty folder first (Will be taken out last)
79                stack.push_front(RemoveStackItem::EmptyFolder(folder));
80
81                // Populate the stack with the resolved files, links, and folders
82                for item in resolved.folders {
83                    stack.push_front(RemoveStackItem::Folder(item));
84                }
85
86                for item in resolved.files {
87                    stack.push_front(RemoveStackItem::File(item));
88                }
89
90                for item in resolved.links {
91                    stack.push_front(RemoveStackItem::Link(item));
92                }
93            }
94            RemoveStackItem::EmptyFolder(folder) => {
95                internal_delete_folder(db, search, events, folder).await?;
96            }
97            RemoveStackItem::File(file) => {
98                delete_file(db, storage, search, events, file, document_box.clone()).await?;
99            }
100            RemoveStackItem::Link(link) => {
101                delete_link(db, search, events, link, document_box.clone()).await?;
102            }
103        }
104    }
105
106    Ok(())
107}
108
109/// Deletes the folder itself and associated metadata, use [delete_folder]
110/// to properly delete the folder and all of its recursive contents
111async fn internal_delete_folder(
112    db: &DbPool,
113    search: &TenantSearchIndex,
114    events: &TenantEventPublisher,
115    folder: Folder,
116) -> Result<(), InternalDeleteFolderError> {
117    // Delete the indexed file contents
118    search.delete_data(folder.id).await?;
119
120    let result = folder.delete(db).await.map_err(|error| {
121        tracing::error!(?error, "failed to delete folder");
122        InternalDeleteFolderError::Database
123    })?;
124
125    let document_box = folder.document_box.clone();
126
127    // Check we actually removed something before emitting an event
128    if result.rows_affected() < 1 {
129        return Ok(());
130    }
131
132    // Publish an event
133    events.publish_event(TenantEventMessage::FolderDeleted(WithScope::new(
134        folder,
135        document_box,
136    )));
137
138    Ok(())
139}