docbox_core/folders/
update_folder.rs

1use docbox_database::{
2    DbErr, DbPool, DbResult, DbTransaction,
3    models::{
4        document_box::DocumentBoxScopeRaw,
5        edit_history::{
6            CreateEditHistory, CreateEditHistoryType, EditHistory, EditHistoryMetadata,
7        },
8        folder::{Folder, FolderId},
9        user::UserId,
10    },
11};
12use docbox_search::{TenantSearchIndex, models::UpdateSearchIndexData};
13use std::ops::DerefMut;
14use thiserror::Error;
15
16#[derive(Debug, Error)]
17pub enum UpdateFolderError {
18    /// Database related error
19    #[error(transparent)]
20    Database(#[from] DbErr),
21
22    /// Target folder could not be found
23    #[error("unknown target folder")]
24    UnknownTargetFolder,
25
26    /// Modification of the root folder is not allowed
27    #[error("cannot modify root")]
28    CannotModifyRoot,
29
30    /// Attempted to move a folder into itself
31    #[error("cannot move into self")]
32    CannotMoveIntoSelf,
33
34    /// Failed to update the search index
35    #[error(transparent)]
36    SearchIndex(anyhow::Error),
37}
38
39pub struct UpdateFolder {
40    /// Move the folder to another folder
41    pub folder_id: Option<FolderId>,
42
43    /// Update the folder name
44    pub name: Option<String>,
45}
46
47pub async fn update_folder(
48    db: &DbPool,
49    search: &TenantSearchIndex,
50    scope: &DocumentBoxScopeRaw,
51    folder: Folder,
52    user_id: Option<String>,
53    update: UpdateFolder,
54) -> Result<(), UpdateFolderError> {
55    let mut folder = folder;
56
57    let mut folder_id = folder
58        .folder_id
59        // Cannot modify the root folder, this is not allowed
60        .ok_or(UpdateFolderError::CannotModifyRoot)?;
61
62    let mut db = db
63        .begin()
64        .await
65        .inspect_err(|cause| tracing::error!(?cause, "failed to begin transaction"))?;
66
67    if let Some(target_id) = update.folder_id {
68        // Cannot move folder into itself
69        if target_id == folder.id {
70            return Err(UpdateFolderError::CannotMoveIntoSelf);
71        }
72
73        // Ensure the target folder exists, also ensures the target folder is in the same scope
74        // (We may allow across scopes in the future, but would need additional checks for access control of target scope)
75        let target_folder = Folder::find_by_id(db.deref_mut(), scope, target_id)
76            .await
77            .inspect_err(|cause| tracing::error!(?cause, "failed to query target folder"))?
78            .ok_or(UpdateFolderError::UnknownTargetFolder)?;
79
80        folder_id = target_folder.id;
81
82        folder = move_folder(&mut db, user_id.clone(), folder, folder_id, target_folder)
83            .await
84            .inspect_err(|cause| tracing::error!(?cause, "failed to move folder"))?;
85    };
86
87    if let Some(new_name) = update.name {
88        folder = update_folder_name(&mut db, user_id, folder, new_name)
89            .await
90            .inspect_err(|cause| tracing::error!(?cause, "failed to update folder name"))?;
91    }
92
93    // Update search index data for the new name and value
94    search
95        .update_data(
96            folder.id,
97            UpdateSearchIndexData {
98                folder_id,
99                name: folder.name.clone(),
100                content: None,
101                pages: None,
102            },
103        )
104        .await
105        .map_err(|cause| {
106            tracing::error!(?cause, "failed to update search index");
107            UpdateFolderError::SearchIndex(cause)
108        })?;
109
110    db.commit().await.inspect_err(|cause| {
111        tracing::error!(?cause, "failed to commit transaction");
112    })?;
113
114    Ok(())
115}
116
117#[tracing::instrument(skip_all, fields(?user_id, folder_id = %folder.id, target_folder_id = %target_folder.id))]
118async fn move_folder(
119    db: &mut DbTransaction<'_>,
120    user_id: Option<UserId>,
121    folder: Folder,
122    folder_id: FolderId,
123    target_folder: Folder,
124) -> DbResult<Folder> {
125    // Track the edit history
126    EditHistory::create(
127        db.deref_mut(),
128        CreateEditHistory {
129            ty: CreateEditHistoryType::Folder(folder.id),
130            user_id: user_id.clone(),
131            metadata: EditHistoryMetadata::MoveToFolder {
132                original_id: folder_id,
133                target_id: target_folder.id,
134            },
135        },
136    )
137    .await
138    .inspect_err(|error| tracing::error!(?error, "failed to store folder move edit history"))?;
139
140    // Perform the move
141    folder
142        .move_to_folder(db.deref_mut(), target_folder.id)
143        .await
144        .inspect_err(|error| tracing::error!(?error, "failed to move folder"))
145}
146
147#[tracing::instrument(skip_all, fields(?user_id, folder_id = %folder.id, %new_name))]
148async fn update_folder_name(
149    db: &mut DbTransaction<'_>,
150    user_id: Option<UserId>,
151    folder: Folder,
152    new_name: String,
153) -> DbResult<Folder> {
154    // Track the edit history
155    EditHistory::create(
156        db.deref_mut(),
157        CreateEditHistory {
158            ty: CreateEditHistoryType::Folder(folder.id),
159            user_id: user_id.clone(),
160            metadata: EditHistoryMetadata::Rename {
161                original_name: folder.name.clone(),
162                new_name: new_name.clone(),
163            },
164        },
165    )
166    .await
167    .inspect_err(|error| tracing::error!(?error, "failed to store folder rename edit history"))?;
168
169    // Perform the rename
170    folder
171        .rename(db.deref_mut(), new_name)
172        .await
173        .inspect_err(|error| tracing::error!(?error, "failed to rename folder in database"))
174}