Skip to main content

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        shared::WithFullPath,
10        user::UserId,
11    },
12};
13use docbox_search::{SearchError, TenantSearchIndex, models::UpdateSearchIndexData};
14use std::ops::DerefMut;
15use thiserror::Error;
16
17#[derive(Debug, Error)]
18pub enum UpdateFolderError {
19    /// Database related error
20    #[error(transparent)]
21    Database(#[from] DbErr),
22
23    /// Target folder could not be found
24    #[error("unknown target folder")]
25    UnknownTargetFolder,
26
27    /// Modification of the root folder is not allowed
28    #[error("cannot modify root")]
29    CannotModifyRoot,
30
31    /// Attempted to move a folder into itself
32    #[error("cannot move into self")]
33    CannotMoveIntoSelf,
34
35    /// Attempted to move a folder into itself
36    #[error("cannot move into child of self")]
37    CannotMoveIntoChildOfSelf,
38
39    /// Failed to update the search index
40    #[error(transparent)]
41    SearchIndex(SearchError),
42}
43
44pub struct UpdateFolder {
45    /// Move the folder to another folder
46    pub folder_id: Option<FolderId>,
47
48    /// Update the folder name
49    pub name: Option<String>,
50
51    /// Update the pinned state
52    pub pinned: Option<bool>,
53}
54
55pub async fn update_folder(
56    db: &DbPool,
57    search: &TenantSearchIndex,
58    scope: &DocumentBoxScopeRaw,
59    folder: Folder,
60    user_id: Option<String>,
61    update: UpdateFolder,
62) -> Result<(), UpdateFolderError> {
63    let mut folder = folder;
64
65    let mut folder_id = folder
66        .folder_id
67        // Cannot modify the root folder, this is not allowed
68        .ok_or(UpdateFolderError::CannotModifyRoot)?;
69
70    let mut db = db
71        .begin()
72        .await
73        .inspect_err(|error| tracing::error!(?error, "failed to begin transaction"))?;
74
75    if let Some(target_id) = update.folder_id {
76        // Cannot move folder into itself
77        if target_id == folder.id {
78            return Err(UpdateFolderError::CannotMoveIntoSelf);
79        }
80
81        // Ensure the target folder exists, also ensures the target folder is in the same scope
82        // (We may allow across scopes in the future, but would need additional checks for access control of target scope)
83        let WithFullPath {
84            data: target_folder,
85            full_path: target_folder_path,
86        } = Folder::find_by_id_with_extra(db.deref_mut(), scope, target_id)
87            .await
88            .inspect_err(|error| tracing::error!(?error, "failed to query target folder"))?
89            .ok_or(UpdateFolderError::UnknownTargetFolder)?;
90
91        // Ensure that we aren't moving the folder into a child of itself
92        if target_folder_path
93            .iter()
94            .any(|segment| segment.id == folder.id)
95        {
96            return Err(UpdateFolderError::CannotMoveIntoChildOfSelf);
97        }
98
99        folder_id = target_folder.folder.id;
100
101        folder = move_folder(
102            &mut db,
103            user_id.clone(),
104            folder,
105            folder_id,
106            target_folder.folder,
107        )
108        .await
109        .inspect_err(|error| tracing::error!(?error, "failed to move folder"))?;
110    };
111
112    if let Some(new_name) = update.name {
113        folder = update_folder_name(&mut db, user_id.clone(), folder, new_name)
114            .await
115            .inspect_err(|error| tracing::error!(?error, "failed to update folder name"))?;
116    }
117
118    if let Some(new_value) = update.pinned {
119        folder = update_folder_pinned(&mut db, user_id, folder, new_value)
120            .await
121            .inspect_err(|error| tracing::error!(?error, "failed to update folder pinned state"))?;
122    }
123
124    // Update search index data for the new name and value
125    search
126        .update_data(
127            folder.id,
128            UpdateSearchIndexData {
129                folder_id,
130                name: folder.name.clone(),
131                content: None,
132                pages: None,
133            },
134        )
135        .await
136        .map_err(|error| {
137            tracing::error!(?error, "failed to update search index");
138            UpdateFolderError::SearchIndex(error)
139        })?;
140
141    db.commit().await.inspect_err(|error| {
142        tracing::error!(?error, "failed to commit transaction");
143    })?;
144
145    Ok(())
146}
147
148/// Add a new edit history item for a folder
149#[tracing::instrument(skip_all, fields(?user_id, %folder_id, ?metadata))]
150async fn add_edit_history(
151    db: &mut DbTransaction<'_>,
152    user_id: Option<UserId>,
153    folder_id: FolderId,
154    metadata: EditHistoryMetadata,
155) -> DbResult<()> {
156    // Track the edit history
157    EditHistory::create(
158        db.deref_mut(),
159        CreateEditHistory {
160            ty: CreateEditHistoryType::Folder(folder_id),
161            user_id,
162            metadata,
163        },
164    )
165    .await
166    .inspect_err(|error| tracing::error!(?error, "failed to store folder edit history entry"))?;
167
168    Ok(())
169}
170
171#[tracing::instrument(skip_all, fields(?user_id, folder_id = %folder.id, target_folder_id = %target_folder.id))]
172async fn move_folder(
173    db: &mut DbTransaction<'_>,
174    user_id: Option<UserId>,
175    folder: Folder,
176    folder_id: FolderId,
177    target_folder: Folder,
178) -> DbResult<Folder> {
179    // Track the edit history
180    add_edit_history(
181        db,
182        user_id,
183        folder.id,
184        EditHistoryMetadata::MoveToFolder {
185            original_id: folder_id,
186            target_id: target_folder.id,
187        },
188    )
189    .await?;
190
191    // Perform the move
192    folder
193        .move_to_folder(db.deref_mut(), target_folder.id)
194        .await
195        .inspect_err(|error| tracing::error!(?error, "failed to move folder"))
196}
197
198#[tracing::instrument(skip_all, fields(?user_id, folder_id = %folder.id, %new_value))]
199async fn update_folder_pinned(
200    db: &mut DbTransaction<'_>,
201    user_id: Option<UserId>,
202    folder: Folder,
203    new_value: bool,
204) -> DbResult<Folder> {
205    // Track the edit history
206    add_edit_history(
207        db,
208        user_id,
209        folder.id,
210        EditHistoryMetadata::ChangePinned {
211            previous_value: folder.pinned,
212            new_value,
213        },
214    )
215    .await?;
216
217    folder
218        .set_pinned(db.deref_mut(), new_value)
219        .await
220        .inspect_err(|error| tracing::error!(?error, "failed to update folder pinned state"))
221}
222
223#[tracing::instrument(skip_all, fields(?user_id, folder_id = %folder.id, %new_name))]
224async fn update_folder_name(
225    db: &mut DbTransaction<'_>,
226    user_id: Option<UserId>,
227    folder: Folder,
228    new_name: String,
229) -> DbResult<Folder> {
230    // Track the edit history
231    add_edit_history(
232        db,
233        user_id,
234        folder.id,
235        EditHistoryMetadata::Rename {
236            original_name: folder.name.clone(),
237            new_name: new_name.clone(),
238        },
239    )
240    .await?;
241
242    // Perform the rename
243    folder
244        .rename(db.deref_mut(), new_name)
245        .await
246        .inspect_err(|error| tracing::error!(?error, "failed to rename folder in database"))
247}