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,
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
40pub async fn update_file(
41    db: &DbPool,
42    search: &TenantSearchIndex,
43    scope: &DocumentBoxScopeRaw,
44    file: File,
45    user_id: Option<String>,
46    update: UpdateFile,
47) -> Result<(), UpdateFileError> {
48    let mut file = file;
49
50    let mut db = db
51        .begin()
52        .await
53        .inspect_err(|cause| tracing::error!(?cause, "failed to begin transaction"))?;
54
55    if let Some(target_id) = update.folder_id {
56        // Ensure the target folder exists, also ensures the target folder is in the same scope
57        // (We may allow across scopes in the future, but would need additional checks for access control of target scope)
58        let target_folder = Folder::find_by_id(db.deref_mut(), scope, target_id)
59            .await
60            .inspect_err(|cause| tracing::error!(?cause, "failed to query target folder"))?
61            .ok_or(UpdateFileError::UnknownTargetFolder)?;
62
63        file = move_file(&mut db, user_id.clone(), file, target_folder)
64            .await
65            .inspect_err(|cause| tracing::error!(?cause, "failed to move file"))?;
66    };
67
68    if let Some(new_name) = update.name {
69        file = update_file_name(&mut db, user_id, file, new_name)
70            .await
71            .inspect_err(|cause| tracing::error!(?cause, "failed to update file name"))?;
72    }
73
74    // Update search index data for the new name and value
75    search
76        .update_data(
77            file.id,
78            UpdateSearchIndexData {
79                folder_id: file.folder_id,
80                name: file.name.clone(),
81                // Don't update unchanged
82                content: None,
83                pages: None,
84            },
85        )
86        .await
87        .map_err(|cause| {
88            tracing::error!(?cause, "failed to update search index");
89            UpdateFileError::SearchIndex(cause)
90        })?;
91
92    db.commit().await.inspect_err(|cause| {
93        tracing::error!(?cause, "failed to commit transaction");
94    })?;
95
96    Ok(())
97}
98
99#[tracing::instrument(skip_all, fields(user_id = ?user_id, file_id = %file.id, new_name = %new_name))]
100async fn update_file_name(
101    db: &mut DbTransaction<'_>,
102    user_id: Option<UserId>,
103    file: File,
104    new_name: String,
105) -> DbResult<File> {
106    // Track the edit history
107    EditHistory::create(
108        db.deref_mut(),
109        CreateEditHistory {
110            ty: CreateEditHistoryType::File(file.id),
111            user_id: user_id.clone(),
112            metadata: EditHistoryMetadata::Rename {
113                original_name: file.name.clone(),
114                new_name: new_name.clone(),
115            },
116        },
117    )
118    .await
119    .inspect_err(|error| tracing::error!(?error, "failed to store file rename edit history"))?;
120
121    file.rename(db.deref_mut(), new_name.clone())
122        .await
123        .inspect_err(|error| tracing::error!(?error, "failed to rename file in database"))
124}
125
126#[tracing::instrument(skip_all, fields(user_id = ?user_id, file_id = %file.id, target_folder_id = %target_folder.id))]
127async fn move_file(
128    db: &mut DbTransaction<'_>,
129    user_id: Option<UserId>,
130    file: File,
131    target_folder: Folder,
132) -> DbResult<File> {
133    // Track the edit history
134    EditHistory::create(
135        db.deref_mut(),
136        CreateEditHistory {
137            ty: CreateEditHistoryType::File(file.id),
138            user_id: user_id.clone(),
139            metadata: EditHistoryMetadata::MoveToFolder {
140                original_id: file.folder_id,
141                target_id: target_folder.id,
142            },
143        },
144    )
145    .await
146    .inspect_err(|error| tracing::error!(?error, "failed to store file move edit history"))?;
147
148    file.move_to_folder(db.deref_mut(), target_folder.id)
149        .await
150        .inspect_err(|error| tracing::error!(?error, "failed to move file in database"))
151}