use crate::{
events::{TenantEventMessage, TenantEventPublisher},
files::{
create_file_key,
generated::{make_create_generated_files, upload_generated_files},
index_file::store_file_index,
},
};
use bytes::Bytes;
use chrono::Utc;
use docbox_database::models::{
document_box::DocumentBoxScopeRaw, generated_file::CreateGeneratedFile,
};
use docbox_database::models::{document_box::DocumentBoxScopeRawRef, folder::FolderId};
use docbox_database::{
DbErr, DbPool, DbTransaction,
models::{
document_box::WithScope,
file::{CreateFile, File, FileId},
generated_file::GeneratedFile,
user::UserId,
},
};
use docbox_processing::{
ProcessingConfig, ProcessingError, ProcessingIndexMetadata, ProcessingLayer, QueuedUpload,
process_file,
};
use docbox_search::{SearchError, TenantSearchIndex};
use docbox_storage::{StorageLayer, StorageLayerError, UploadFileOptions};
use mime::Mime;
use std::ops::DerefMut;
use thiserror::Error;
use tracing::Instrument;
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum UploadFileError {
#[error("failed to create file search index: {0}")]
CreateIndex(SearchError),
#[error("failed to create file entry")]
CreateFile(DbErr),
#[error("failed to process file: {0}")]
Processing(#[from] ProcessingError),
#[error("failed to upload generated file to storage layer: {0}")]
UploadGeneratedFile(StorageLayerError),
#[error("failed to create generated file")]
CreateGeneratedFile(DbErr),
#[error("failed to upload file to storage layer: {0}")]
UploadFile(StorageLayerError),
#[error("failed to upload files")]
FailedFileUploads(Vec<UploadFileError>),
#[error("failed to perform operation (start)")]
BeginTransaction(DbErr),
#[error("failed to perform operation (end)")]
CommitTransaction(DbErr),
}
#[derive(Default)]
pub struct UploadFileState {
pub storage_upload_keys: Vec<String>,
pub search_index_files: Vec<Uuid>,
}
pub struct UploadFile {
pub fixed_id: Option<FileId>,
pub parent_id: Option<FileId>,
pub folder_id: FolderId,
pub document_box: DocumentBoxScopeRaw,
pub name: String,
pub mime: Mime,
pub file_bytes: Bytes,
pub created_by: Option<UserId>,
pub file_key: Option<String>,
pub processing_config: Option<ProcessingConfig>,
}
#[derive(Debug)]
pub struct UploadedFileData {
pub file: File,
pub generated: Vec<GeneratedFile>,
pub additional_files: Vec<UploadedFileData>,
}
pub async fn upload_file(
db: &DbPool,
search: &TenantSearchIndex,
storage: &StorageLayer,
processing: &ProcessingLayer,
events: &TenantEventPublisher,
upload: UploadFile,
) -> Result<UploadedFileData, UploadFileError> {
let document_box = upload.document_box.clone();
let mut upload_state = UploadFileState::default();
let data =
match upload_file_inner(search, storage, processing, upload, &mut upload_state, 0).await {
Ok(value) => value,
Err(error) => {
tracing::error!(?error, "failed to complete inner file processing");
background_rollback_upload_file(search.clone(), storage.clone(), upload_state);
return Err(error);
}
};
let mut db = db.begin().await.map_err(|error| {
tracing::error!(?error, "failed to begin transaction");
UploadFileError::BeginTransaction(error)
})?;
let output = match persist_file_upload(&mut db, data).await {
Ok(value) => value,
Err(error) => {
if let Err(error) = db.rollback().await {
tracing::error!(?error, "failed to roll back database transaction");
}
tracing::error!(?error, "failed to complete inner file processing");
background_rollback_upload_file(search.clone(), storage.clone(), upload_state);
return Err(error);
}
};
if let Err(error) = db.commit().await {
tracing::error!(?error, "failed to commit transaction");
background_rollback_upload_file(search.clone(), storage.clone(), upload_state);
return Err(UploadFileError::CommitTransaction(error));
}
publish_file_creation_events(events, &document_box, &output);
Ok(output)
}
pub fn publish_file_creation_events(
events: &TenantEventPublisher,
document_box: DocumentBoxScopeRawRef<'_>,
output: &UploadedFileData,
) {
events.publish_event(TenantEventMessage::FileCreated(WithScope::new(
output.file.clone(),
document_box.to_string(),
)));
for additional_file in &output.additional_files {
publish_file_creation_events(events, document_box, additional_file);
}
}
#[derive(Debug)]
pub struct PreparedUploadData {
file: CreateFile,
generated_files: Option<Vec<CreateGeneratedFile>>,
additional_files: Vec<PreparedUploadData>,
}
async fn upload_file_inner(
search: &TenantSearchIndex,
storage: &StorageLayer,
processing: &ProcessingLayer,
upload: UploadFile,
upload_state: &mut UploadFileState,
iteration: usize,
) -> Result<PreparedUploadData, UploadFileError> {
let server_max_iterations = processing.config.max_unpack_iterations.unwrap_or(1);
let max_iterations = upload
.processing_config
.as_ref()
.and_then(|value| value.max_unpack_iterations)
.unwrap_or(1)
.min(server_max_iterations);
let (s3_upload, file_key) = match upload.file_key.as_ref() {
Some(file_key) => (false, file_key.clone()),
None => (
true,
create_file_key(
&upload.document_box,
&upload.name,
&upload.mime,
Uuid::new_v4(),
),
),
};
let processing_output = process_file(
&upload.processing_config,
processing,
upload.file_bytes.clone(),
&upload.mime,
)
.await?;
let encrypted = processing_output
.as_ref()
.map(|output| output.encrypted)
.unwrap_or_default();
let file_record = make_file_record(&upload, &file_key, &upload.file_bytes, encrypted);
let mut index_metadata: Option<ProcessingIndexMetadata> = None;
let mut generated_files: Option<Vec<CreateGeneratedFile>> = None;
let mut additional_files: Vec<PreparedUploadData> = Vec::new();
if let Some(processing_output) = processing_output {
index_metadata = processing_output.index_metadata;
tracing::debug!("uploading generated files");
let prepared_files = store_generated_files(
storage,
&file_record,
&mut upload_state.storage_upload_keys,
processing_output.upload_queue,
)
.await?;
generated_files = Some(prepared_files);
let next_iteration = iteration + 1;
if next_iteration > max_iterations {
tracing::debug!(
?iteration,
"additional files were present but exceeded the max unpacking iteration for this request"
);
} else {
for additional_file in processing_output.additional_files {
let upload = UploadFile {
parent_id: Some(file_record.id),
fixed_id: additional_file.fixed_id,
folder_id: upload.folder_id,
document_box: upload.document_box.clone(),
name: additional_file.name,
mime: additional_file.mime,
file_bytes: additional_file.bytes,
created_by: upload.created_by.clone(),
file_key: None,
processing_config: upload.processing_config.clone(),
};
let output = Box::pin(upload_file_inner(
search,
storage,
processing,
upload,
upload_state,
next_iteration,
))
.await?;
additional_files.push(output);
}
}
}
tracing::debug!("indexing file contents");
store_file_index(search, &file_record, &upload.document_box, index_metadata).await?;
upload_state.search_index_files.push(file_record.id);
if s3_upload {
tracing::debug!("uploading main file");
storage
.upload_file(
&file_key,
upload.file_bytes,
UploadFileOptions {
content_type: file_record.mime.clone(),
..Default::default()
},
)
.await
.map_err(UploadFileError::UploadFile)?;
upload_state.storage_upload_keys.push(file_key.clone());
}
Ok(PreparedUploadData {
file: file_record,
generated_files,
additional_files,
})
}
async fn persist_file_upload(
db: &mut DbTransaction<'_>,
data: PreparedUploadData,
) -> Result<UploadedFileData, UploadFileError> {
let file = File::create(db.deref_mut(), data.file)
.await
.map_err(UploadFileError::CreateFile)?;
let mut generated_files = Vec::new();
if let Some(creates) = data.generated_files {
for create in creates {
let generated_file = GeneratedFile::create(db.deref_mut(), create)
.await
.map_err(UploadFileError::CreateGeneratedFile)?;
generated_files.push(generated_file);
}
}
let mut additional_files: Vec<UploadedFileData> = Vec::new();
for additional_file in data.additional_files {
let inner = Box::pin(persist_file_upload(db, additional_file)).await?;
additional_files.push(inner);
}
Ok(UploadedFileData {
file,
generated: generated_files,
additional_files,
})
}
fn make_file_record(
upload: &UploadFile,
file_key: &str,
file_bytes: &Bytes,
encrypted: bool,
) -> CreateFile {
let id = upload.fixed_id.unwrap_or_else(Uuid::new_v4);
let hash = sha256::digest(file_bytes.as_ref() as &[u8]);
let size = file_bytes.len().min(i32::MAX as usize) as i32;
let created_at = Utc::now();
CreateFile {
id,
parent_id: upload.parent_id,
name: upload.name.clone(),
mime: upload.mime.to_string(),
file_key: file_key.to_owned(),
folder_id: upload.folder_id,
hash,
size,
created_by: upload.created_by.clone(),
created_at,
encrypted,
}
}
pub async fn store_generated_files(
storage: &StorageLayer,
file: &CreateFile,
storage_upload_keys: &mut Vec<String>,
queued_uploads: Vec<QueuedUpload>,
) -> Result<Vec<CreateGeneratedFile>, UploadFileError> {
let prepared_uploads =
make_create_generated_files(&file.file_key, &file.id, &file.hash, queued_uploads);
let upload_results = upload_generated_files(storage, prepared_uploads).await;
let mut generated_files = Vec::new();
let mut upload_errors = Vec::new();
for result in upload_results {
match result {
Ok(create) => {
storage_upload_keys.push(file.file_key.clone());
generated_files.push(create);
}
Err(error) => {
tracing::error!(?error, "failed to upload generated file");
upload_errors.push(UploadFileError::UploadGeneratedFile(error));
}
}
}
if !upload_errors.is_empty() {
tracing::warn!(
?upload_errors,
"error while uploading generated files, operation failed"
);
return Err(UploadFileError::FailedFileUploads(upload_errors));
}
Ok(generated_files)
}
fn background_rollback_upload_file(
search: TenantSearchIndex,
storage: StorageLayer,
upload_state: UploadFileState,
) {
let span = tracing::Span::current();
tokio::spawn(
async move {
rollback_upload_file(&search, &storage, upload_state).await;
}
.instrument(span),
);
}
async fn rollback_upload_file(
search: &TenantSearchIndex,
storage: &StorageLayer,
upload_state: UploadFileState,
) {
for key in upload_state.storage_upload_keys {
if let Err(err) = storage.delete_file(&key).await {
tracing::error!(?err, "failed to rollback created tenant s3 file");
}
}
for index in upload_state.search_index_files {
if let Err(err) = search.delete_data(index).await {
tracing::error!(?index, ?err, "failed to rollback created file search index",);
}
}
}