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::{SearchError, TenantSearchIndex, models::UpdateSearchIndexData};
13use std::ops::DerefMut;
14use thiserror::Error;
15
16#[derive(Debug, Error)]
17pub enum UpdateFolderError {
18 #[error(transparent)]
20 Database(#[from] DbErr),
21
22 #[error("unknown target folder")]
24 UnknownTargetFolder,
25
26 #[error("cannot modify root")]
28 CannotModifyRoot,
29
30 #[error("cannot move into self")]
32 CannotMoveIntoSelf,
33
34 #[error(transparent)]
36 SearchIndex(SearchError),
37}
38
39pub struct UpdateFolder {
40 pub folder_id: Option<FolderId>,
42
43 pub name: Option<String>,
45
46 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 .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 if target_id == folder.id {
73 return Err(UpdateFolderError::CannotMoveIntoSelf);
74 }
75
76 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 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#[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 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 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 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 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 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 folder
222 .rename(db.deref_mut(), new_name)
223 .await
224 .inspect_err(|error| tracing::error!(?error, "failed to rename folder in database"))
225}