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    /// Update the pinned state
47    pub pinned: Option<bool>,
48}
49
50pub async fn update_folder(
51    db: &DbPool,
52    search: &TenantSearchIndex,
53    scope: &DocumentBoxScopeRaw,
54    folder: Folder,
55    user_id: Option<String>,
56    update: UpdateFolder,
57) -> Result<(), UpdateFolderError> {
58    let mut folder = folder;
59
60    let mut folder_id = folder
61        .folder_id
62        // Cannot modify the root folder, this is not allowed
63        .ok_or(UpdateFolderError::CannotModifyRoot)?;
64
65    let mut db = db
66        .begin()
67        .await
68        .inspect_err(|cause| tracing::error!(?cause, "failed to begin transaction"))?;
69
70    if let Some(target_id) = update.folder_id {
71        // Cannot move folder into itself
72        if target_id == folder.id {
73            return Err(UpdateFolderError::CannotMoveIntoSelf);
74        }
75
76        // Ensure the target folder exists, also ensures the target folder is in the same scope
77        // (We may allow across scopes in the future, but would need additional checks for access control of target scope)
78        let target_folder = Folder::find_by_id(db.deref_mut(), scope, target_id)
79            .await
80            .inspect_err(|cause| tracing::error!(?cause, "failed to query target folder"))?
81            .ok_or(UpdateFolderError::UnknownTargetFolder)?;
82
83        folder_id = target_folder.id;
84
85        folder = move_folder(&mut db, user_id.clone(), folder, folder_id, target_folder)
86            .await
87            .inspect_err(|cause| tracing::error!(?cause, "failed to move folder"))?;
88    };
89
90    if let Some(new_name) = update.name {
91        folder = update_folder_name(&mut db, user_id.clone(), folder, new_name)
92            .await
93            .inspect_err(|cause| tracing::error!(?cause, "failed to update folder name"))?;
94    }
95
96    if let Some(new_value) = update.pinned {
97        folder = update_folder_pinned(&mut db, user_id, folder, new_value)
98            .await
99            .inspect_err(|cause| tracing::error!(?cause, "failed to update folder pinned state"))?;
100    }
101
102    // Update search index data for the new name and value
103    search
104        .update_data(
105            folder.id,
106            UpdateSearchIndexData {
107                folder_id,
108                name: folder.name.clone(),
109                content: None,
110                pages: None,
111            },
112        )
113        .await
114        .map_err(|cause| {
115            tracing::error!(?cause, "failed to update search index");
116            UpdateFolderError::SearchIndex(cause)
117        })?;
118
119    db.commit().await.inspect_err(|cause| {
120        tracing::error!(?cause, "failed to commit transaction");
121    })?;
122
123    Ok(())
124}
125
126/// Add a new edit history item for a folder
127#[tracing::instrument(skip_all, fields(?user_id, %folder_id, ?metadata))]
128async fn add_edit_history(
129    db: &mut DbTransaction<'_>,
130    user_id: Option<UserId>,
131    folder_id: FolderId,
132    metadata: EditHistoryMetadata,
133) -> DbResult<()> {
134    // Track the edit history
135    EditHistory::create(
136        db.deref_mut(),
137        CreateEditHistory {
138            ty: CreateEditHistoryType::Folder(folder_id),
139            user_id,
140            metadata,
141        },
142    )
143    .await
144    .inspect_err(|error| tracing::error!(?error, "failed to store folder edit history entry"))?;
145
146    Ok(())
147}
148
149#[tracing::instrument(skip_all, fields(?user_id, folder_id = %folder.id, target_folder_id = %target_folder.id))]
150async fn move_folder(
151    db: &mut DbTransaction<'_>,
152    user_id: Option<UserId>,
153    folder: Folder,
154    folder_id: FolderId,
155    target_folder: Folder,
156) -> DbResult<Folder> {
157    // Track the edit history
158    add_edit_history(
159        db,
160        user_id,
161        folder.id,
162        EditHistoryMetadata::MoveToFolder {
163            original_id: folder_id,
164            target_id: target_folder.id,
165        },
166    )
167    .await?;
168
169    // Perform the move
170    folder
171        .move_to_folder(db.deref_mut(), target_folder.id)
172        .await
173        .inspect_err(|error| tracing::error!(?error, "failed to move folder"))
174}
175
176#[tracing::instrument(skip_all, fields(?user_id, folder_id = %folder.id, %new_value))]
177async fn update_folder_pinned(
178    db: &mut DbTransaction<'_>,
179    user_id: Option<UserId>,
180    folder: Folder,
181    new_value: bool,
182) -> DbResult<Folder> {
183    // Track the edit history
184    add_edit_history(
185        db,
186        user_id,
187        folder.id,
188        EditHistoryMetadata::ChangePinned {
189            previous_value: folder.pinned,
190            new_value,
191        },
192    )
193    .await?;
194
195    folder
196        .set_pinned(db.deref_mut(), new_value)
197        .await
198        .inspect_err(|error| tracing::error!(?error, "failed to update folder pinned state"))
199}
200
201#[tracing::instrument(skip_all, fields(?user_id, folder_id = %folder.id, %new_name))]
202async fn update_folder_name(
203    db: &mut DbTransaction<'_>,
204    user_id: Option<UserId>,
205    folder: Folder,
206    new_name: String,
207) -> DbResult<Folder> {
208    // Track the edit history
209    add_edit_history(
210        db,
211        user_id,
212        folder.id,
213        EditHistoryMetadata::Rename {
214            original_name: folder.name.clone(),
215            new_name: new_name.clone(),
216        },
217    )
218    .await?;
219
220    // Perform the rename
221    folder
222        .rename(db.deref_mut(), new_name)
223        .await
224        .inspect_err(|error| tracing::error!(?error, "failed to rename folder in database"))
225}