use crate::{
events::TenantEventPublisher,
files::{
create_file_key,
upload_file::{UploadFile, UploadFileError, UploadedFileData, upload_file},
},
};
use docbox_database::{
DbErr, DbPool,
models::{
document_box::DocumentBoxScopeRaw,
file::FileId,
folder::Folder,
presigned_upload_task::{
CreatePresignedUploadTask, PresignedTaskStatus, PresignedUploadTask,
PresignedUploadTaskId,
},
user::UserId,
},
};
use docbox_processing::{ProcessingConfig, ProcessingError, ProcessingLayer};
use docbox_search::TenantSearchIndex;
use docbox_storage::{StorageLayer, StorageLayerError};
use mime::Mime;
use serde::Serialize;
use std::{collections::HashMap, str::FromStr};
use thiserror::Error;
use uuid::Uuid;
#[derive(Serialize)]
pub struct PresignedUploadOutcome {
pub task_id: PresignedUploadTaskId,
pub method: String,
pub uri: String,
pub headers: HashMap<String, String>,
}
#[derive(Debug, Error)]
pub enum PresignedUploadError {
#[error(transparent)]
UploadFile(#[from] UploadFileError),
#[error("failed to load file from storage")]
LoadFile(StorageLayerError),
#[error("file had an invalid mime type")]
InvalidMimeType(mime::FromStrError),
#[error("failed to create file")]
CreateFile(DbErr),
#[error("failed to process file: {0}")]
Processing(#[from] ProcessingError),
#[error("failed to update task status")]
UpdateTaskStatus(DbErr),
}
pub struct CreatePresigned {
pub name: String,
pub document_box: DocumentBoxScopeRaw,
pub folder: Folder,
pub size: i32,
pub mime: Mime,
pub created_by: Option<UserId>,
pub parent_id: Option<FileId>,
pub processing_config: Option<ProcessingConfig>,
}
#[derive(Debug, Error)]
pub enum CreatePresignedUploadError {
#[error("failed to create presigned url")]
CreatePresigned,
#[error("failed to store upload configuration")]
SerializeConfig,
#[error("failed to store presigned upload task")]
StoreTask,
}
pub async fn create_presigned_upload(
db: &DbPool,
storage: &StorageLayer,
create: CreatePresigned,
) -> Result<PresignedUploadOutcome, CreatePresignedUploadError> {
let file_key = create_file_key(
&create.folder.document_box,
&create.name,
&create.mime,
Uuid::new_v4(),
);
let (signed_request, expires_at) = storage
.create_presigned(&file_key, create.size as i64)
.await
.map_err(|error| {
tracing::error!(?error, "failed to create presigned upload");
CreatePresignedUploadError::CreatePresigned
})?;
let processing_config = match &create.processing_config {
Some(config) => {
let value = serde_json::to_value(config).map_err(|error| {
tracing::error!(?error, "failed to serialize processing config");
CreatePresignedUploadError::SerializeConfig
})?;
Some(value)
}
None => None,
};
let task = PresignedUploadTask::create(
db,
CreatePresignedUploadTask {
name: create.name,
mime: create.mime.to_string(),
document_box: create.document_box,
folder_id: create.folder.id,
size: create.size,
file_key,
created_by: create.created_by,
expires_at,
parent_id: create.parent_id,
processing_config,
},
)
.await
.map_err(|error| {
tracing::error!(?error, "failed to store presigned upload task");
CreatePresignedUploadError::StoreTask
})?;
Ok(PresignedUploadOutcome {
task_id: task.id,
method: signed_request.method().to_string(),
uri: signed_request.uri().to_string(),
headers: signed_request
.headers()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect(),
})
}
pub struct CompletePresigned {
pub task: PresignedUploadTask,
pub folder: Folder,
}
pub async fn safe_complete_presigned(
db_pool: DbPool,
search: TenantSearchIndex,
storage: StorageLayer,
events: TenantEventPublisher,
processing: ProcessingLayer,
mut complete: CompletePresigned,
) -> Result<(), PresignedUploadError> {
match complete_presigned(
&db_pool,
&search,
&storage,
&processing,
&events,
&mut complete,
)
.await
{
Ok(output) => {
let status = PresignedTaskStatus::Completed {
file_id: output.file.id,
};
if let Err(error) = complete.task.set_status(&db_pool, status).await {
tracing::error!(?error, "failed to set presigned task status");
return Err(PresignedUploadError::UpdateTaskStatus(error));
}
Ok(())
}
Err(error) => {
tracing::error!(?error, "failed to complete presigned upload");
let status = PresignedTaskStatus::Failed {
error: error.to_string(),
};
if let Err(error) = complete.task.set_status(&db_pool, status).await {
tracing::error!(?error, "failed to set presigned task status");
return Err(PresignedUploadError::UpdateTaskStatus(error));
}
Err(error)
}
}
}
pub async fn complete_presigned(
db: &DbPool,
search: &TenantSearchIndex,
storage: &StorageLayer,
processing: &ProcessingLayer,
events: &TenantEventPublisher,
complete: &mut CompletePresigned,
) -> Result<UploadedFileData, PresignedUploadError> {
let task = &mut complete.task;
let file_bytes = storage
.get_file(&task.file_key)
.await
.map_err(PresignedUploadError::LoadFile)?
.collect_bytes()
.await
.map_err(PresignedUploadError::LoadFile)?;
let mime = mime::Mime::from_str(&task.mime).map_err(PresignedUploadError::InvalidMimeType)?;
let processing_config: Option<ProcessingConfig> = match &task.processing_config {
Some(value) => match serde_json::from_value(value.0.clone()) {
Ok(value) => value,
Err(error) => {
tracing::error!(?error, "failed to deserialize processing config");
None
}
},
None => None,
};
let upload = UploadFile {
fixed_id: None,
parent_id: task.parent_id,
folder_id: complete.folder.id,
document_box: complete.folder.document_box.clone(),
name: task.name.clone(),
mime,
file_bytes,
created_by: task.created_by.clone(),
file_key: Some(task.file_key.clone()),
processing_config,
};
let output = upload_file(db, search, storage, processing, events, upload).await?;
Ok(output)
}