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(transparent)]
42 UploadFile(#[from] UploadFileError),
43
44 #[error("failed to load file from storage")]
46 LoadFile(StorageLayerError),
47
48 #[error("file had an invalid mime type")]
50 InvalidMimeType(mime::FromStrError),
51
52 #[error("failed to create file")]
54 CreateFile(DbErr),
55
56 #[error("failed to process file: {0}")]
58 Processing(#[from] ProcessingError),
59
60 #[error("failed to update task status")]
62 UpdateTaskStatus(DbErr),
63}
64
65pub struct CreatePresigned {
66 pub name: String,
68
69 pub document_box: DocumentBoxScopeRaw,
71
72 pub folder: Folder,
74
75 pub size: i32,
77
78 pub mime: Mime,
80
81 pub created_by: Option<UserId>,
83
84 pub parent_id: Option<FileId>,
86
87 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
103pub 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 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
173pub 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
221pub 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 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 let mime = mime::Mime::from_str(&task.mime).map_err(PresignedUploadError::InvalidMimeType)?;
243
244 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 let output = upload_file(db, search, storage, processing, events, upload).await?;
271 Ok(output)
272}