use crate::{
error::{DynHttpError, HttpCommonError, HttpErrorResponse, HttpResult, HttpStatusResult},
middleware::{
action_user::{ActionUser, UserParams},
tenant::{TenantDb, TenantEvents, TenantParams, TenantSearch, TenantStorage},
},
models::{
document_box::DocumentBoxScope,
file::UploadTaskResponse,
folder::{
CreateFolderRequest, FolderResponse, HttpFolderError, UpdateFolderRequest,
ZipFolderRequest,
},
},
};
use axum::{Json, extract::Path, http::StatusCode};
use axum_valid::Garde;
use docbox_core::{
database::models::{
edit_history::EditHistory,
folder::{Folder, FolderId, FolderWithExtra, ResolvedFolderWithExtra},
shared::WithFullPath,
tasks::TaskStatus,
},
folders::{
create_folder::{CreateFolderData, safe_create_folder},
create_folder_zip::{CreateFolderZipOptions, create_folder_zip},
delete_folder::delete_folder,
update_folder::{UpdateFolder, UpdateFolderError},
},
tasks::background_task::background_task,
};
use tracing::Instrument;
pub const FOLDER_TAG: &str = "Folder";
#[utoipa::path(
post,
operation_id = "folder_create",
tag = FOLDER_TAG,
path = "/box/{scope}/folder",
responses(
(status = 201, description = "Folder created successfully", body = FolderResponse),
(status = 404, description = "Destination folder not found", body = HttpErrorResponse),
(status = 500, description = "Internal server error", body = HttpErrorResponse)
),
params(
("scope" = DocumentBoxScope, Path, description = "Scope to create the folder within"),
TenantParams,
UserParams
)
)]
#[tracing::instrument(skip_all, fields(%scope, ?req))]
pub async fn create(
action_user: ActionUser,
TenantDb(db): TenantDb,
TenantSearch(search): TenantSearch,
TenantEvents(events): TenantEvents,
Path(DocumentBoxScope(scope)): Path<DocumentBoxScope>,
Garde(Json(req)): Garde<Json<CreateFolderRequest>>,
) -> Result<(StatusCode, Json<FolderResponse>), DynHttpError> {
let folder_id = req.folder_id;
let parent_folder = Folder::find_by_id(&db, &scope, folder_id)
.await
.map_err(|error| {
tracing::error!(
%scope,
%folder_id,
?error,
"failed to query link destination folder"
);
HttpCommonError::ServerError
})?
.ok_or(HttpFolderError::UnknownFolder)?;
let created_by = action_user.store_user(&db).await?;
let create = CreateFolderData {
folder: parent_folder,
name: req.name,
created_by: created_by.as_ref().map(|value| value.id.to_string()),
};
let folder = safe_create_folder(&db, search, &events, create)
.await
.map_err(|error| {
tracing::error!(?error, "failed to create link");
HttpFolderError::CreateError(error)
})?;
Ok((
StatusCode::CREATED,
Json(FolderResponse {
folder: FolderWithExtra {
folder,
created_by,
last_modified_at: None,
last_modified_by: None,
},
children: ResolvedFolderWithExtra::default(),
}),
))
}
#[utoipa::path(
get,
operation_id = "folder_get",
tag = FOLDER_TAG,
path = "/box/{scope}/folder/{folder_id}",
responses(
(status = 200, description = "Folder obtained successfully", body = FolderResponse),
(status = 404, description = "Folder not found", body = HttpErrorResponse),
(status = 500, description = "Internal server error", body = HttpErrorResponse)
),
params(
("scope" = DocumentBoxScope, Path, description = "Scope the folder resides within"),
("folder_id" = Uuid, Path, description = "ID of the folder to request"),
TenantParams
)
)]
#[tracing::instrument(skip_all, fields(%scope, %folder_id))]
pub async fn get(
TenantDb(db): TenantDb,
Path((scope, folder_id)): Path<(DocumentBoxScope, FolderId)>,
) -> HttpResult<FolderResponse> {
let DocumentBoxScope(scope) = scope;
let WithFullPath {
data: folder,
full_path,
} = Folder::find_by_id_with_extra(&db, &scope, folder_id)
.await
.map_err(|error| {
tracing::error!(?error, "failed to query folder");
HttpCommonError::ServerError
})?
.ok_or(HttpFolderError::UnknownFolder)?;
let children = ResolvedFolderWithExtra::resolve(&db, folder.folder.id, full_path)
.await
.map_err(|error| {
tracing::error!(?error, "failed to resolve folder children");
HttpCommonError::ServerError
})?;
Ok(Json(FolderResponse { folder, children }))
}
#[utoipa::path(
get,
operation_id = "folder_edit_history",
tag = FOLDER_TAG,
path = "/box/{scope}/folder/{folder_id}/edit-history",
responses(
(status = 200, description = "Obtained edit history", body = [EditHistory]),
(status = 404, description = "Folder not found", body = HttpErrorResponse),
(status = 500, description = "Internal server error", body = HttpErrorResponse)
),
params(
("scope" = DocumentBoxScope, Path, description = "Scope the folder resides within"),
("folder_id" = Uuid, Path, description = "ID of the folder to request"),
TenantParams
)
)]
#[tracing::instrument(skip_all, fields(%scope, %folder_id))]
pub async fn get_edit_history(
TenantDb(db): TenantDb,
Path((scope, folder_id)): Path<(DocumentBoxScope, FolderId)>,
) -> HttpResult<Vec<EditHistory>> {
let DocumentBoxScope(scope) = scope;
_ = Folder::find_by_id(&db, &scope, folder_id)
.await
.map_err(|error| {
tracing::error!(?error, "failed to query folder");
HttpCommonError::ServerError
})?
.ok_or(HttpFolderError::UnknownFolder)?;
let edit_history = EditHistory::all_by_folder(&db, folder_id)
.await
.map_err(|error| {
tracing::error!(?error, "failed to query folder edit history");
HttpCommonError::ServerError
})?;
Ok(Json(edit_history))
}
#[utoipa::path(
put,
operation_id = "folder_update",
tag = FOLDER_TAG,
path = "/box/{scope}/folder/{folder_id}",
responses(
(status = 200, description = "Updated folder successfully"),
(status = 400, description = "Attempted to move a root folder or a folder into itself", body = HttpErrorResponse),
(status = 404, description = "Folder not found", body = HttpErrorResponse),
(status = 500, description = "Internal server error", body = HttpErrorResponse)
),
params(
("scope" = DocumentBoxScope, Path, description = "Scope the folder resides within"),
("folder_id" = Uuid, Path, description = "ID of the folder to request"),
TenantParams,
UserParams
)
)]
#[tracing::instrument(skip_all, fields(%scope, %folder_id, ?req))]
pub async fn update(
action_user: ActionUser,
TenantDb(db): TenantDb,
TenantSearch(search): TenantSearch,
Path((scope, folder_id)): Path<(DocumentBoxScope, FolderId)>,
Garde(Json(req)): Garde<Json<UpdateFolderRequest>>,
) -> HttpStatusResult {
let DocumentBoxScope(scope) = scope;
let folder = Folder::find_by_id(&db, &scope, folder_id)
.await
.map_err(|error| {
tracing::error!(%scope, %folder_id, ?error, "failed to query folder");
HttpCommonError::ServerError
})?
.ok_or(HttpFolderError::UnknownFolder)?;
let user = action_user.store_user(&db).await?;
let user_id = user.as_ref().map(|value| value.id.to_string());
let update = UpdateFolder {
folder_id: req.folder_id,
name: req.name,
pinned: req.pinned,
};
docbox_core::folders::update_folder::update_folder(
&db, &search, &scope, folder, user_id, update,
)
.await
.map_err(|error| match error {
UpdateFolderError::UnknownTargetFolder => HttpFolderError::UnknownTargetFolder.into(),
UpdateFolderError::CannotModifyRoot => HttpFolderError::CannotModifyRoot.into(),
UpdateFolderError::CannotMoveIntoSelf => HttpFolderError::CannotMoveIntoSelf.into(),
_ => DynHttpError::from(HttpCommonError::ServerError),
})?;
Ok(StatusCode::OK)
}
#[utoipa::path(
delete,
operation_id = "folder_delete",
tag = FOLDER_TAG,
path = "/box/{scope}/folder/{folder_id}",
responses(
(status = 204, description = "Deleted folder successfully"),
(status = 404, description = "Folder not found", body = HttpErrorResponse),
(status = 500, description = "Internal server error", body = HttpErrorResponse)
),
params(
("scope" = DocumentBoxScope, Path, description = "Scope the folder resides within"),
("folder_id" = Uuid, Path, description = "ID of the folder to delete"),
TenantParams
)
)]
#[tracing::instrument(skip_all, fields(%scope, %folder_id))]
pub async fn delete(
TenantDb(db): TenantDb,
TenantStorage(storage): TenantStorage,
TenantEvents(events): TenantEvents,
TenantSearch(search): TenantSearch,
Path((scope, folder_id)): Path<(DocumentBoxScope, FolderId)>,
) -> HttpStatusResult {
let DocumentBoxScope(scope) = scope;
let folder = Folder::find_by_id(&db, &scope, folder_id)
.await
.map_err(|error| {
tracing::error!(?error, "failed to query folder");
HttpCommonError::ServerError
})?
.ok_or(HttpFolderError::UnknownFolder)?;
if folder.folder_id.is_none() {
return Err(HttpFolderError::CannotDeleteRoot.into());
}
delete_folder(&db, &storage, &search, &events, folder)
.await
.map_err(|error| {
tracing::error!(?error, "failed to delete folder");
HttpCommonError::ServerError
})?;
Ok(StatusCode::NO_CONTENT)
}
#[utoipa::path(
post,
operation_id = "folder_create_zip",
tag = FOLDER_TAG,
path = "/box/{scope}/folder/{folder_id}/zip",
responses(
(status = 204, description = "Deleted folder successfully"),
(status = 404, description = "Folder not found", body = HttpErrorResponse),
(status = 500, description = "Internal server error", body = HttpErrorResponse)
),
params(
("scope" = DocumentBoxScope, Path, description = "Scope the folder resides within"),
("folder_id" = Uuid, Path, description = "ID of the folder to delete"),
TenantParams
)
)]
#[tracing::instrument(skip_all, fields(%scope, %folder_id, ?req))]
pub async fn create_zip(
TenantDb(db): TenantDb,
TenantStorage(storage): TenantStorage,
Path((scope, folder_id)): Path<(DocumentBoxScope, FolderId)>,
Garde(Json(req)): Garde<Json<ZipFolderRequest>>,
) -> HttpResult<UploadTaskResponse> {
let DocumentBoxScope(scope) = scope;
let folder = Folder::find_by_id(&db, &scope, folder_id)
.await
.map_err(|error| {
tracing::error!(?error, "failed to query folder");
HttpCommonError::ServerError
})?
.ok_or(HttpFolderError::UnknownFolder)?;
let options = CreateFolderZipOptions {
include: req.include,
exclude: req.exclude,
};
let span = tracing::Span::current();
let (task_id, created_at) = background_task(
db.clone(),
scope.clone(),
async move {
let result = create_folder_zip(&db, &storage, &folder, options)
.await
.map_err(|error| {
tracing::error!(?error, "failed to upload file");
DynHttpError::from(HttpFolderError::CreateZipFile)
})
.and_then(|value| {
serde_json::to_value(&value).map_err(|error| {
tracing::error!(?error, "failed to serialize upload task outcome");
DynHttpError::from(HttpCommonError::ServerError)
})
});
match result {
Ok(value) => (TaskStatus::Completed, value),
Err(error) => (
TaskStatus::Failed,
serde_json::json!({ "error": error.to_string() }),
),
}
}
.instrument(span),
)
.await
.map_err(|error| {
tracing::error!(?error, "failed to create background task");
HttpCommonError::ServerError
})?;
Ok(Json(UploadTaskResponse {
task_id,
created_at,
}))
}