docbox_core/files/
upload_file_presigned.rs

1use crate::{
2    events::TenantEventPublisher,
3    files::{
4        create_file_key,
5        upload_file::{UploadFile, UploadFileError, UploadedFileData, upload_file},
6    },
7};
8use docbox_database::{
9    DbErr, DbPool,
10    models::{
11        document_box::DocumentBoxScopeRaw,
12        file::FileId,
13        folder::Folder,
14        presigned_upload_task::{
15            CreatePresignedUploadTask, PresignedTaskStatus, PresignedUploadTask,
16            PresignedUploadTaskId,
17        },
18        user::UserId,
19    },
20};
21use docbox_processing::{ProcessingConfig, ProcessingError, ProcessingLayer};
22use docbox_search::TenantSearchIndex;
23use docbox_storage::{StorageLayerError, TenantStorageLayer};
24use mime::Mime;
25use serde::Serialize;
26use std::{collections::HashMap, str::FromStr};
27use thiserror::Error;
28use uuid::Uuid;
29
30#[derive(Serialize)]
31pub struct PresignedUploadOutcome {
32    pub task_id: PresignedUploadTaskId,
33    pub method: String,
34    pub uri: String,
35    pub headers: HashMap<String, String>,
36}
37
38#[derive(Debug, Error)]
39pub enum PresignedUploadError {
40    /// Error when uploading files
41    #[error(transparent)]
42    UploadFile(#[from] UploadFileError),
43
44    /// Error loading the file the storage layer
45    #[error("failed to load file from storage")]
46    LoadFile(StorageLayerError),
47
48    /// Stored file metadata mime type was invalid
49    #[error("file had an invalid mime type")]
50    InvalidMimeType(mime::FromStrError),
51
52    /// Failed to create the file database row
53    #[error("failed to create file")]
54    CreateFile(DbErr),
55
56    /// Failed to process the file
57    #[error("failed to process file: {0}")]
58    Processing(#[from] ProcessingError),
59
60    /// Failed to update the task status
61    #[error("failed to update task status")]
62    UpdateTaskStatus(DbErr),
63}
64
65pub struct CreatePresigned {
66    /// Name of the file being uploaded
67    pub name: String,
68
69    /// The document box scope to store within
70    pub document_box: DocumentBoxScopeRaw,
71
72    /// Folder to store the file in
73    pub folder: Folder,
74
75    /// Size of the file being uploaded
76    pub size: i32,
77
78    /// Mime type of the file
79    pub mime: Mime,
80
81    /// User uploading the file
82    pub created_by: Option<UserId>,
83
84    /// Optional parent file ID
85    pub parent_id: Option<FileId>,
86
87    /// Config for processing step
88    pub processing_config: Option<ProcessingConfig>,
89}
90
91#[derive(Debug, Error)]
92pub enum CreatePresignedUploadError {
93    #[error("failed to create presigned url")]
94    CreatePresigned,
95
96    #[error("failed to store upload configuration")]
97    SerializeConfig,
98
99    #[error("failed to store presigned upload task")]
100    StoreTask,
101}
102
103/// Create a new presigned file upload request
104pub async fn create_presigned_upload(
105    db: &DbPool,
106    storage: &TenantStorageLayer,
107    create: CreatePresigned,
108) -> Result<PresignedUploadOutcome, CreatePresignedUploadError> {
109    let file_key = create_file_key(
110        &create.folder.document_box,
111        &create.name,
112        &create.mime,
113        Uuid::new_v4(),
114    );
115    let (signed_request, expires_at) = storage
116        .create_presigned(&file_key, create.size as i64)
117        .await
118        .map_err(|error| {
119            tracing::error!(?error, "failed to create presigned upload");
120            CreatePresignedUploadError::CreatePresigned
121        })?;
122
123    // Encode the processing config for the database
124    let processing_config = match &create.processing_config {
125        Some(config) => {
126            let value = serde_json::to_value(config).map_err(|error| {
127                tracing::error!(?error, "failed to serialize processing config");
128                CreatePresignedUploadError::SerializeConfig
129            })?;
130
131            Some(value)
132        }
133        None => None,
134    };
135
136    let task = PresignedUploadTask::create(
137        db,
138        CreatePresignedUploadTask {
139            name: create.name,
140            mime: create.mime.to_string(),
141            document_box: create.document_box,
142            folder_id: create.folder.id,
143            size: create.size,
144            file_key,
145            created_by: create.created_by,
146            expires_at,
147            parent_id: create.parent_id,
148            processing_config,
149        },
150    )
151    .await
152    .map_err(|error| {
153        tracing::error!(?error, "failed to store presigned upload task");
154        CreatePresignedUploadError::StoreTask
155    })?;
156
157    Ok(PresignedUploadOutcome {
158        task_id: task.id,
159        method: signed_request.method().to_string(),
160        uri: signed_request.uri().to_string(),
161        headers: signed_request
162            .headers()
163            .map(|(key, value)| (key.to_string(), value.to_string()))
164            .collect(),
165    })
166}
167
168pub struct CompletePresigned {
169    pub task: PresignedUploadTask,
170    pub folder: Folder,
171}
172
173/// Safely performs [upload_file] ensuring that on failure all resources are
174/// properly cleaned up
175pub async fn safe_complete_presigned(
176    db_pool: DbPool,
177    search: TenantSearchIndex,
178    storage: TenantStorageLayer,
179    events: TenantEventPublisher,
180    processing: ProcessingLayer,
181    mut complete: CompletePresigned,
182) -> Result<(), PresignedUploadError> {
183    match complete_presigned(
184        &db_pool,
185        &search,
186        &storage,
187        &processing,
188        &events,
189        &mut complete,
190    )
191    .await
192    {
193        Ok(output) => {
194            let status = PresignedTaskStatus::Completed {
195                file_id: output.file.id,
196            };
197
198            if let Err(cause) = complete.task.set_status(&db_pool, status).await {
199                tracing::error!(?cause, "failed to set presigned task status");
200                return Err(PresignedUploadError::UpdateTaskStatus(cause));
201            }
202
203            Ok(())
204        }
205        Err(error) => {
206            tracing::error!(?error, "failed to complete presigned upload");
207            let status = PresignedTaskStatus::Failed {
208                error: error.to_string(),
209            };
210
211            if let Err(cause) = complete.task.set_status(&db_pool, status).await {
212                tracing::error!(?cause, "failed to set presigned task status");
213                return Err(PresignedUploadError::UpdateTaskStatus(cause));
214            }
215
216            Err(error)
217        }
218    }
219}
220
221/// Completes a presigned file upload
222pub async fn complete_presigned(
223    db: &DbPool,
224    search: &TenantSearchIndex,
225    storage: &TenantStorageLayer,
226    processing: &ProcessingLayer,
227    events: &TenantEventPublisher,
228    complete: &mut CompletePresigned,
229) -> Result<UploadedFileData, PresignedUploadError> {
230    let task = &mut complete.task;
231
232    // Load the file from storage
233    let file_bytes = storage
234        .get_file(&task.file_key)
235        .await
236        .map_err(PresignedUploadError::LoadFile)?
237        .collect_bytes()
238        .await
239        .map_err(PresignedUploadError::LoadFile)?;
240
241    // Get the mime type from the task
242    let mime = mime::Mime::from_str(&task.mime).map_err(PresignedUploadError::InvalidMimeType)?;
243
244    // Parse task processing config
245    let processing_config: Option<ProcessingConfig> = match &task.processing_config {
246        Some(value) => match serde_json::from_value(value.0.clone()) {
247            Ok(value) => value,
248            Err(cause) => {
249                tracing::error!(?cause, "failed to deserialize processing config");
250                None
251            }
252        },
253        None => None,
254    };
255
256    let upload = UploadFile {
257        fixed_id: None,
258        parent_id: task.parent_id,
259        folder_id: complete.folder.id,
260        document_box: complete.folder.document_box.clone(),
261        name: task.name.clone(),
262        mime,
263        file_bytes,
264        created_by: task.created_by.clone(),
265        file_key: Some(task.file_key.clone()),
266        processing_config,
267    };
268
269    // Perform the upload
270    let output = upload_file(db, search, storage, processing, events, upload).await?;
271    Ok(output)
272}