1use crate::{
4 error::{DynHttpError, HttpCommonError, HttpErrorResponse, HttpResult, HttpStatusResult},
5 middleware::{
6 action_user::{ActionUser, UserParams},
7 tenant::{TenantDb, TenantEvents, TenantParams, TenantSearch, TenantStorage},
8 },
9 models::document_box::{
10 CreateDocumentBoxRequest, DocumentBoxResponse, DocumentBoxScope, DocumentBoxStats,
11 HttpDocumentBoxError,
12 },
13};
14use axum::{Json, extract::Path, http::StatusCode};
15use axum_valid::Garde;
16use docbox_core::{
17 database::models::{
18 document_box::DocumentBox,
19 file::File,
20 folder::{Folder, FolderWithExtra, ResolvedFolderWithExtra},
21 shared::WithFullPath,
22 },
23 document_box::{
24 create_document_box::{CreateDocumentBox, CreateDocumentBoxError, create_document_box},
25 delete_document_box::{DeleteDocumentBoxError, delete_document_box},
26 search_document_box::{ResolvedSearchResult, search_document_box},
27 },
28 search::models::{SearchRequest, SearchResultItem, SearchResultResponse},
29};
30use tokio::join;
31
32pub const DOCUMENT_BOX_TAG: &str = "Document Box";
33
34#[utoipa::path(
38 post,
39 operation_id = "document_box_create",
40 tag = DOCUMENT_BOX_TAG,
41 path = "/box",
42 responses(
43 (status = 201, description = "Document box created successfully", body = DocumentBoxResponse),
44 (status = 409, description = "Scope already exists", body = HttpErrorResponse),
45 (status = 500, description = "Internal server error", body = HttpErrorResponse)
46 ),
47 params(
48 TenantParams,
49 UserParams
50 )
51)]
52#[tracing::instrument(skip_all, fields(?req))]
53pub async fn create(
54 action_user: ActionUser,
55 TenantDb(db): TenantDb,
56 TenantEvents(events): TenantEvents,
57 Garde(Json(req)): Garde<Json<CreateDocumentBoxRequest>>,
58) -> Result<(StatusCode, Json<DocumentBoxResponse>), DynHttpError> {
59 let created_by = action_user.store_user(&db).await?;
61
62 let create = CreateDocumentBox {
63 scope: req.scope,
64 created_by: created_by.as_ref().map(|value| value.id.to_string()),
65 };
66
67 let (document_box, root) =
68 create_document_box(&db, &events, create)
69 .await
70 .map_err(|error| match error {
71 CreateDocumentBoxError::ScopeAlreadyExists => {
72 DynHttpError::from(HttpDocumentBoxError::ScopeAlreadyExists)
73 }
74 error => {
75 tracing::error!(?error, "failed to create document box");
76 DynHttpError::from(HttpCommonError::ServerError)
77 }
78 })?;
79
80 Ok((
81 StatusCode::CREATED,
82 Json(DocumentBoxResponse {
83 document_box,
84 root: FolderWithExtra {
85 folder: root,
86 created_by,
87 last_modified_at: None,
88 last_modified_by: None,
89 },
90 children: Default::default(),
91 }),
92 ))
93}
94
95#[utoipa::path(
100 get,
101 operation_id = "document_box_get",
102 tag = DOCUMENT_BOX_TAG,
103 path = "/box/{scope}",
104 responses(
105 (status = 200, description = "Document box obtained successfully", body = DocumentBoxResponse),
106 (status = 404, description = "Document box not found", body = HttpErrorResponse),
107 (status = 500, description = "Internal server error", body = HttpErrorResponse)
108 ),
109 params(
110 ("scope" = DocumentBoxScope, Path, description = "Scope of the document box"),
111 TenantParams
112 )
113)]
114#[tracing::instrument(skip_all, fields(%scope))]
115pub async fn get(
116 TenantDb(db): TenantDb,
117 Path(DocumentBoxScope(scope)): Path<DocumentBoxScope>,
118) -> HttpResult<DocumentBoxResponse> {
119 let document_box = DocumentBox::find_by_scope(&db, &scope)
120 .await
121 .map_err(|error| {
122 tracing::error!(?error, "failed to query document box");
123 HttpCommonError::ServerError
124 })?
125 .ok_or(HttpDocumentBoxError::UnknownDocumentBox)?;
126
127 let WithFullPath {
128 data: root,
129 full_path,
130 } = Folder::find_root_with_extra(&db, &scope)
131 .await
132 .map_err(|error| {
133 tracing::error!(?error, "failed to query folder");
134 HttpCommonError::ServerError
135 })?
136 .ok_or_else(|| {
137 tracing::error!("document box missing root");
138 HttpCommonError::ServerError
139 })?;
140
141 let children = ResolvedFolderWithExtra::resolve(&db, root.folder.id, full_path)
142 .await
143 .map_err(|error| {
144 tracing::error!(?error, "failed to query document box root folder");
145 HttpCommonError::ServerError
146 })?;
147
148 Ok(Json(DocumentBoxResponse {
149 document_box,
150 root,
151 children,
152 }))
153}
154
155#[utoipa::path(
163 get,
164 operation_id = "document_box_stats",
165 tag = DOCUMENT_BOX_TAG,
166 path = "/box/{scope}/stats",
167 responses(
168 (status = 200, description = "Document box stats obtained successfully", body = DocumentBoxStats),
169 (status = 404, description = "Document box not found", body = HttpErrorResponse),
170 (status = 500, description = "Internal server error", body = HttpErrorResponse)
171 ),
172 params(
173 ("scope" = DocumentBoxScope, Path, description = "Scope of the document box"),
174 TenantParams
175 )
176)]
177#[tracing::instrument(skip_all, fields(%scope))]
178pub async fn stats(
179 TenantDb(db): TenantDb,
180 Path(DocumentBoxScope(scope)): Path<DocumentBoxScope>,
181) -> HttpResult<DocumentBoxStats> {
182 let _document_box = DocumentBox::find_by_scope(&db, &scope)
184 .await
185 .map_err(|error| {
186 tracing::error!(?error, "failed to query document box");
187 HttpCommonError::ServerError
188 })?
189 .ok_or(HttpDocumentBoxError::UnknownDocumentBox)?;
190
191 let root = Folder::find_root(&db, &scope)
192 .await
193 .map_err(|error| {
194 tracing::error!(?error, "failed to query folder");
195 HttpCommonError::ServerError
196 })?
197 .ok_or_else(|| {
198 tracing::error!("document box missing root");
199 HttpCommonError::ServerError
200 })?;
201
202 let children_future = Folder::count_children(&db, root.id);
203 let file_size_future = File::total_size_within_scope(&db, &scope);
204
205 let (children, file_size) = join!(children_future, file_size_future);
207
208 let children = children.map_err(|error| {
209 tracing::error!(?error, "failed to query document box children count");
210 HttpCommonError::ServerError
211 })?;
212
213 let file_size = file_size.map_err(|error| {
214 tracing::error!(?error, "failed to query document box file size");
215 HttpCommonError::ServerError
216 })?;
217
218 Ok(Json(DocumentBoxStats {
219 total_files: children.file_count,
220 total_links: children.link_count,
221 total_folders: children.folder_count,
222 file_size,
223 }))
224}
225
226#[utoipa::path(
234 delete,
235 operation_id = "document_box_delete",
236 tag = DOCUMENT_BOX_TAG,
237 path = "/box/{scope}",
238 responses(
239 (status = 204, description = "Document box deleted successfully"),
240 (status = 404, description = "Document box not found", body = HttpErrorResponse),
241 (status = 500, description = "Internal server error", body = HttpErrorResponse)
242 ),
243 params(
244 ("scope" = DocumentBoxScope, Path, description = "Scope of the document box"),
245 TenantParams
246 )
247)]
248#[tracing::instrument(skip_all, fields(%scope))]
249pub async fn delete(
250 TenantDb(db): TenantDb,
251 TenantSearch(search): TenantSearch,
252 TenantStorage(storage): TenantStorage,
253 TenantEvents(events): TenantEvents,
254 Path(DocumentBoxScope(scope)): Path<DocumentBoxScope>,
255) -> HttpStatusResult {
256 delete_document_box(&db, &search, &storage, &events, &scope)
257 .await
258 .map_err(|error| match error {
259 DeleteDocumentBoxError::UnknownScope => HttpDocumentBoxError::UnknownDocumentBox.into(),
260 error => {
261 tracing::error!(?error, "failed to delete document box");
262 DynHttpError::from(HttpCommonError::ServerError)
263 }
264 })?;
265
266 Ok(StatusCode::NO_CONTENT)
267}
268
269#[utoipa::path(
273 post,
274 operation_id = "document_box_search",
275 tag = DOCUMENT_BOX_TAG,
276 path = "/box/{scope}/search",
277 responses(
278 (status = 200, description = "Searched successfully", body = SearchResultResponse),
279 (status = 400, description = "Malformed or invalid request not meeting validation requirements", body = HttpErrorResponse),
280 (status = 404, description = "Target folder not found", body = HttpErrorResponse),
281 (status = 500, description = "Internal server error", body = HttpErrorResponse)
282 ),
283 params(
284 ("scope" = DocumentBoxScope, Path, description = "Scope of the document box"),
285 TenantParams
286 )
287)]
288#[tracing::instrument(skip_all, fields(%scope, ?req))]
289pub async fn search(
290 TenantDb(db): TenantDb,
291 TenantSearch(search): TenantSearch,
292 Path(DocumentBoxScope(scope)): Path<DocumentBoxScope>,
293 Garde(Json(req)): Garde<Json<SearchRequest>>,
294) -> HttpResult<SearchResultResponse> {
295 let resolved = search_document_box(&db, &search, scope, req)
296 .await
297 .map_err(|error| {
298 tracing::error!(?error, "failed to search document box");
299 HttpCommonError::ServerError
300 })?;
301
302 let out: Vec<SearchResultItem> = resolved
303 .results
304 .into_iter()
305 .map(
306 |ResolvedSearchResult { result, data, path }| SearchResultItem {
307 path,
308 score: result.score,
309 data,
310 page_matches: result.page_matches,
311 total_hits: result.total_hits,
312 name_match: result.name_match,
313 content_match: result.content_match,
314 },
315 )
316 .collect();
317
318 Ok(Json(SearchResultResponse {
319 total_hits: resolved.total_hits,
320 results: out,
321 }))
322}