docbox_core/files/
update_file.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        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    /// 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    /// Failed to update the search index
28    #[error(transparent)]
29    SearchIndex(anyhow::Error),
30}
31
32pub struct UpdateFile {
33    /// Move the file to another folder
34    pub folder_id: Option<FolderId>,
35
36    /// Update the file name
37    pub name: Option<String>,
38
39    /// Update the file pinned state
40    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        // Ensure the target folder exists, also ensures the target folder is in the same scope
60        // (We may allow across scopes in the future, but would need additional checks for access control of target scope)
61        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    // Update search index data for the new name and value
84    search
85        .update_data(
86            file.id,
87            UpdateSearchIndexData {
88                folder_id: file.folder_id,
89                name: file.name.clone(),
90                // Don't update unchanged
91                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/// Add a new edit history item for a file
109#[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    // Track the edit history
138    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    // Track the edit history
162    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    // Track the edit history
186    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}