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 #[error(transparent)]
21 Database(#[from] DbErr),
22
23 #[error("unknown target folder")]
25 UnknownTargetFolder,
26
27 #[error("cannot modify root")]
29 CannotModifyRoot,
30
31 #[error("cannot move into self")]
33 CannotMoveIntoSelf,
34
35 #[error("cannot move into child of self")]
37 CannotMoveIntoChildOfSelf,
38
39 #[error(transparent)]
41 SearchIndex(SearchError),
42}
43
44pub struct UpdateFolder {
45 pub folder_id: Option<FolderId>,
47
48 pub name: Option<String>,
50
51 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 .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 if target_id == folder.id {
78 return Err(UpdateFolderError::CannotMoveIntoSelf);
79 }
80
81 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 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 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#[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 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 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 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 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 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 folder
244 .rename(db.deref_mut(), new_name)
245 .await
246 .inspect_err(|error| tracing::error!(?error, "failed to rename folder in database"))
247}