1use docbox_database::{
2 DbErr, DbPool, DbResult, DbTransaction,
3 models::{
4 document_box::DocumentBoxScopeRaw,
5 edit_history::{
6 CreateEditHistory, CreateEditHistoryType, EditHistory, EditHistoryMetadata,
7 },
8 file::{File, FileId},
9 folder::{Folder, FolderId},
10 user::UserId,
11 },
12};
13use docbox_search::{TenantSearchIndex, models::UpdateSearchIndexData};
14use std::ops::DerefMut;
15use thiserror::Error;
16
17#[derive(Debug, Error)]
18pub enum UpdateFileError {
19 #[error(transparent)]
21 Database(#[from] DbErr),
22
23 #[error("unknown target folder")]
25 UnknownTargetFolder,
26
27 #[error(transparent)]
29 SearchIndex(anyhow::Error),
30}
31
32pub struct UpdateFile {
33 pub folder_id: Option<FolderId>,
35
36 pub name: Option<String>,
38
39 pub pinned: Option<bool>,
41}
42
43pub async fn update_file(
44 db: &DbPool,
45 search: &TenantSearchIndex,
46 scope: &DocumentBoxScopeRaw,
47 file: File,
48 user_id: Option<String>,
49 update: UpdateFile,
50) -> Result<(), UpdateFileError> {
51 let mut file = file;
52
53 let mut db = db
54 .begin()
55 .await
56 .inspect_err(|cause| tracing::error!(?cause, "failed to begin transaction"))?;
57
58 if let Some(target_id) = update.folder_id {
59 let target_folder = Folder::find_by_id(db.deref_mut(), scope, target_id)
62 .await
63 .inspect_err(|cause| tracing::error!(?cause, "failed to query target folder"))?
64 .ok_or(UpdateFileError::UnknownTargetFolder)?;
65
66 file = move_file(&mut db, user_id.clone(), file, target_folder)
67 .await
68 .inspect_err(|cause| tracing::error!(?cause, "failed to move file"))?;
69 };
70
71 if let Some(new_name) = update.name {
72 file = update_file_name(&mut db, user_id.clone(), file, new_name)
73 .await
74 .inspect_err(|cause| tracing::error!(?cause, "failed to update file name"))?;
75 }
76
77 if let Some(new_value) = update.pinned {
78 file = update_file_pinned(&mut db, user_id, file, new_value)
79 .await
80 .inspect_err(|cause| tracing::error!(?cause, "failed to update file pinned state"))?;
81 }
82
83 search
85 .update_data(
86 file.id,
87 UpdateSearchIndexData {
88 folder_id: file.folder_id,
89 name: file.name.clone(),
90 content: None,
92 pages: None,
93 },
94 )
95 .await
96 .map_err(|cause| {
97 tracing::error!(?cause, "failed to update search index");
98 UpdateFileError::SearchIndex(cause)
99 })?;
100
101 db.commit().await.inspect_err(|cause| {
102 tracing::error!(?cause, "failed to commit transaction");
103 })?;
104
105 Ok(())
106}
107
108#[tracing::instrument(skip_all, fields(?user_id, %file_id, ?metadata))]
110async fn add_edit_history(
111 db: &mut DbTransaction<'_>,
112 user_id: Option<UserId>,
113 file_id: FileId,
114 metadata: EditHistoryMetadata,
115) -> DbResult<()> {
116 EditHistory::create(
117 db.deref_mut(),
118 CreateEditHistory {
119 ty: CreateEditHistoryType::File(file_id),
120 user_id,
121 metadata,
122 },
123 )
124 .await
125 .inspect_err(|error| tracing::error!(?error, "failed to store file edit history entry"))?;
126
127 Ok(())
128}
129
130#[tracing::instrument(skip_all, fields(?user_id, file_id = %file.id, %new_value))]
131async fn update_file_pinned(
132 db: &mut DbTransaction<'_>,
133 user_id: Option<UserId>,
134 file: File,
135 new_value: bool,
136) -> DbResult<File> {
137 add_edit_history(
139 db,
140 user_id,
141 file.id,
142 EditHistoryMetadata::ChangePinned {
143 previous_value: file.pinned,
144 new_value,
145 },
146 )
147 .await?;
148
149 file.set_pinned(db.deref_mut(), new_value)
150 .await
151 .inspect_err(|error| tracing::error!(?error, "failed to update file pinned state"))
152}
153
154#[tracing::instrument(skip_all, fields(?user_id, file_id = %file.id, %new_name))]
155async fn update_file_name(
156 db: &mut DbTransaction<'_>,
157 user_id: Option<UserId>,
158 file: File,
159 new_name: String,
160) -> DbResult<File> {
161 add_edit_history(
163 db,
164 user_id,
165 file.id,
166 EditHistoryMetadata::Rename {
167 original_name: file.name.clone(),
168 new_name: new_name.clone(),
169 },
170 )
171 .await?;
172
173 file.rename(db.deref_mut(), new_name.clone())
174 .await
175 .inspect_err(|error| tracing::error!(?error, "failed to rename file in database"))
176}
177
178#[tracing::instrument(skip_all, fields(user_id = ?user_id, file_id = %file.id, target_folder_id = %target_folder.id))]
179async fn move_file(
180 db: &mut DbTransaction<'_>,
181 user_id: Option<UserId>,
182 file: File,
183 target_folder: Folder,
184) -> DbResult<File> {
185 add_edit_history(
187 db,
188 user_id,
189 file.id,
190 EditHistoryMetadata::MoveToFolder {
191 original_id: file.folder_id,
192 target_id: target_folder.id,
193 },
194 )
195 .await?;
196
197 file.move_to_folder(db.deref_mut(), target_folder.id)
198 .await
199 .inspect_err(|error| tracing::error!(?error, "failed to move file in database"))
200}