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::{
10 document_box::DocumentBoxScope,
11 file::UploadTaskResponse,
12 folder::{
13 CreateFolderRequest, FolderResponse, HttpFolderError, UpdateFolderRequest,
14 ZipFolderRequest,
15 },
16 },
17};
18use axum::{Json, extract::Path, http::StatusCode};
19use axum_valid::Garde;
20use docbox_core::{
21 database::models::{
22 edit_history::EditHistory,
23 folder::{Folder, FolderId, FolderWithExtra, ResolvedFolderWithExtra},
24 shared::WithFullPath,
25 tasks::TaskStatus,
26 },
27 folders::{
28 create_folder::{CreateFolderData, safe_create_folder},
29 create_folder_zip::{CreateFolderZipOptions, create_folder_zip},
30 delete_folder::delete_folder,
31 update_folder::{UpdateFolder, UpdateFolderError},
32 },
33 tasks::background_task::background_task,
34};
35use tracing::Instrument;
36
37pub const FOLDER_TAG: &str = "Folder";
38
39#[utoipa::path(
43 post,
44 operation_id = "folder_create",
45 tag = FOLDER_TAG,
46 path = "/box/{scope}/folder",
47 responses(
48 (status = 201, description = "Folder created successfully", body = FolderResponse),
49 (status = 404, description = "Destination folder not found", body = HttpErrorResponse),
50 (status = 500, description = "Internal server error", body = HttpErrorResponse)
51 ),
52 params(
53 ("scope" = DocumentBoxScope, Path, description = "Scope to create the folder within"),
54 TenantParams,
55 UserParams
56 )
57)]
58#[tracing::instrument(skip_all, fields(%scope, ?req))]
59pub async fn create(
60 action_user: ActionUser,
61 TenantDb(db): TenantDb,
62 TenantSearch(search): TenantSearch,
63 TenantEvents(events): TenantEvents,
64 Path(DocumentBoxScope(scope)): Path<DocumentBoxScope>,
65 Garde(Json(req)): Garde<Json<CreateFolderRequest>>,
66) -> Result<(StatusCode, Json<FolderResponse>), DynHttpError> {
67 let folder_id = req.folder_id;
68 let parent_folder = Folder::find_by_id(&db, &scope, folder_id)
69 .await
70 .map_err(|error| {
72 tracing::error!(
73 %scope,
74 %folder_id,
75 ?error,
76 "failed to query link destination folder"
77 );
78 HttpCommonError::ServerError
79 })?
80 .ok_or(HttpFolderError::UnknownFolder)?;
82
83 let created_by = action_user.store_user(&db).await?;
85
86 let create = CreateFolderData {
88 folder: parent_folder,
89 name: req.name,
90 created_by: created_by.as_ref().map(|value| value.id.to_string()),
91 };
92
93 let folder = safe_create_folder(&db, search, &events, create)
95 .await
96 .map_err(|error| {
97 tracing::error!(?error, "failed to create link");
98 HttpFolderError::CreateError(error)
99 })?;
100
101 Ok((
102 StatusCode::CREATED,
103 Json(FolderResponse {
104 folder: FolderWithExtra {
105 folder,
106 created_by,
107 last_modified_at: None,
108 last_modified_by: None,
109 },
110 children: ResolvedFolderWithExtra::default(),
111 }),
112 ))
113}
114
115#[utoipa::path(
120 get,
121 operation_id = "folder_get",
122 tag = FOLDER_TAG,
123 path = "/box/{scope}/folder/{folder_id}",
124 responses(
125 (status = 200, description = "Folder obtained successfully", body = FolderResponse),
126 (status = 404, description = "Folder not found", body = HttpErrorResponse),
127 (status = 500, description = "Internal server error", body = HttpErrorResponse)
128 ),
129 params(
130 ("scope" = DocumentBoxScope, Path, description = "Scope the folder resides within"),
131 ("folder_id" = Uuid, Path, description = "ID of the folder to request"),
132 TenantParams
133 )
134)]
135#[tracing::instrument(skip_all, fields(%scope, %folder_id))]
136pub async fn get(
137 TenantDb(db): TenantDb,
138 Path((scope, folder_id)): Path<(DocumentBoxScope, FolderId)>,
139) -> HttpResult<FolderResponse> {
140 let DocumentBoxScope(scope) = scope;
141
142 let WithFullPath {
143 data: folder,
144 full_path,
145 } = Folder::find_by_id_with_extra(&db, &scope, folder_id)
146 .await
147 .map_err(|error| {
149 tracing::error!(?error, "failed to query folder");
150 HttpCommonError::ServerError
151 })?
152 .ok_or(HttpFolderError::UnknownFolder)?;
154
155 let children = ResolvedFolderWithExtra::resolve(&db, folder.folder.id, full_path)
156 .await
157 .map_err(|error| {
158 tracing::error!(?error, "failed to resolve folder children");
159 HttpCommonError::ServerError
160 })?;
161
162 Ok(Json(FolderResponse { folder, children }))
163}
164
165#[utoipa::path(
169 get,
170 operation_id = "folder_edit_history",
171 tag = FOLDER_TAG,
172 path = "/box/{scope}/folder/{folder_id}/edit-history",
173 responses(
174 (status = 200, description = "Obtained edit history", body = [EditHistory]),
175 (status = 404, description = "Folder not found", body = HttpErrorResponse),
176 (status = 500, description = "Internal server error", body = HttpErrorResponse)
177 ),
178 params(
179 ("scope" = DocumentBoxScope, Path, description = "Scope the folder resides within"),
180 ("folder_id" = Uuid, Path, description = "ID of the folder to request"),
181 TenantParams
182 )
183)]
184#[tracing::instrument(skip_all, fields(%scope, %folder_id))]
185pub async fn get_edit_history(
186 TenantDb(db): TenantDb,
187 Path((scope, folder_id)): Path<(DocumentBoxScope, FolderId)>,
188) -> HttpResult<Vec<EditHistory>> {
189 let DocumentBoxScope(scope) = scope;
190
191 _ = Folder::find_by_id(&db, &scope, folder_id)
192 .await
193 .map_err(|error| {
195 tracing::error!(?error, "failed to query folder");
196 HttpCommonError::ServerError
197 })?
198 .ok_or(HttpFolderError::UnknownFolder)?;
200
201 let edit_history = EditHistory::all_by_folder(&db, folder_id)
202 .await
203 .map_err(|error| {
204 tracing::error!(?error, "failed to query folder edit history");
205 HttpCommonError::ServerError
206 })?;
207
208 Ok(Json(edit_history))
209}
210
211#[utoipa::path(
215 put,
216 operation_id = "folder_update",
217 tag = FOLDER_TAG,
218 path = "/box/{scope}/folder/{folder_id}",
219 responses(
220 (status = 200, description = "Updated folder successfully"),
221 (status = 400, description = "Attempted to move a root folder or a folder into itself", body = HttpErrorResponse),
222 (status = 404, description = "Folder not found", body = HttpErrorResponse),
223 (status = 500, description = "Internal server error", body = HttpErrorResponse)
224 ),
225 params(
226 ("scope" = DocumentBoxScope, Path, description = "Scope the folder resides within"),
227 ("folder_id" = Uuid, Path, description = "ID of the folder to request"),
228 TenantParams,
229 UserParams
230 )
231)]
232#[tracing::instrument(skip_all, fields(%scope, %folder_id, ?req))]
233pub async fn update(
234 action_user: ActionUser,
235 TenantDb(db): TenantDb,
236 TenantSearch(search): TenantSearch,
237 Path((scope, folder_id)): Path<(DocumentBoxScope, FolderId)>,
238 Garde(Json(req)): Garde<Json<UpdateFolderRequest>>,
239) -> HttpStatusResult {
240 let DocumentBoxScope(scope) = scope;
241
242 let folder = Folder::find_by_id(&db, &scope, folder_id)
243 .await
244 .map_err(|error| {
246 tracing::error!(%scope, %folder_id, ?error, "failed to query folder");
247 HttpCommonError::ServerError
248 })?
249 .ok_or(HttpFolderError::UnknownFolder)?;
251
252 let user = action_user.store_user(&db).await?;
254 let user_id = user.as_ref().map(|value| value.id.to_string());
255
256 let update = UpdateFolder {
257 folder_id: req.folder_id,
258 name: req.name,
259 pinned: req.pinned,
260 };
261
262 docbox_core::folders::update_folder::update_folder(
263 &db, &search, &scope, folder, user_id, update,
264 )
265 .await
266 .map_err(|error| match error {
267 UpdateFolderError::UnknownTargetFolder => HttpFolderError::UnknownTargetFolder.into(),
268 UpdateFolderError::CannotModifyRoot => HttpFolderError::CannotModifyRoot.into(),
269 UpdateFolderError::CannotMoveIntoSelf => HttpFolderError::CannotMoveIntoSelf.into(),
270 _ => DynHttpError::from(HttpCommonError::ServerError),
271 })?;
272
273 Ok(StatusCode::OK)
274}
275
276#[utoipa::path(
282 delete,
283 operation_id = "folder_delete",
284 tag = FOLDER_TAG,
285 path = "/box/{scope}/folder/{folder_id}",
286 responses(
287 (status = 204, description = "Deleted folder successfully"),
288 (status = 404, description = "Folder not found", body = HttpErrorResponse),
289 (status = 500, description = "Internal server error", body = HttpErrorResponse)
290 ),
291 params(
292 ("scope" = DocumentBoxScope, Path, description = "Scope the folder resides within"),
293 ("folder_id" = Uuid, Path, description = "ID of the folder to delete"),
294 TenantParams
295 )
296)]
297#[tracing::instrument(skip_all, fields(%scope, %folder_id))]
298pub async fn delete(
299 TenantDb(db): TenantDb,
300 TenantStorage(storage): TenantStorage,
301 TenantEvents(events): TenantEvents,
302 TenantSearch(search): TenantSearch,
303 Path((scope, folder_id)): Path<(DocumentBoxScope, FolderId)>,
304) -> HttpStatusResult {
305 let DocumentBoxScope(scope) = scope;
306
307 let folder = Folder::find_by_id(&db, &scope, folder_id)
308 .await
309 .map_err(|error| {
311 tracing::error!(?error, "failed to query folder");
312 HttpCommonError::ServerError
313 })?
314 .ok_or(HttpFolderError::UnknownFolder)?;
316
317 if folder.folder_id.is_none() {
319 return Err(HttpFolderError::CannotDeleteRoot.into());
320 }
321
322 delete_folder(&db, &storage, &search, &events, folder)
323 .await
324 .map_err(|error| {
325 tracing::error!(?error, "failed to delete folder");
326 HttpCommonError::ServerError
327 })?;
328
329 Ok(StatusCode::NO_CONTENT)
330}
331
332#[utoipa::path(
336 post,
337 operation_id = "folder_create_zip",
338 tag = FOLDER_TAG,
339 path = "/box/{scope}/folder/{folder_id}/zip",
340 responses(
341 (status = 204, description = "Deleted folder successfully"),
342 (status = 404, description = "Folder not found", body = HttpErrorResponse),
343 (status = 500, description = "Internal server error", body = HttpErrorResponse)
344 ),
345 params(
346 ("scope" = DocumentBoxScope, Path, description = "Scope the folder resides within"),
347 ("folder_id" = Uuid, Path, description = "ID of the folder to delete"),
348 TenantParams
349 )
350)]
351#[tracing::instrument(skip_all, fields(%scope, %folder_id, ?req))]
352pub async fn create_zip(
353 TenantDb(db): TenantDb,
354 TenantStorage(storage): TenantStorage,
355 Path((scope, folder_id)): Path<(DocumentBoxScope, FolderId)>,
356 Garde(Json(req)): Garde<Json<ZipFolderRequest>>,
357) -> HttpResult<UploadTaskResponse> {
358 let DocumentBoxScope(scope) = scope;
359
360 let folder = Folder::find_by_id(&db, &scope, folder_id)
361 .await
362 .map_err(|error| {
364 tracing::error!(?error, "failed to query folder");
365 HttpCommonError::ServerError
366 })?
367 .ok_or(HttpFolderError::UnknownFolder)?;
369
370 let options = CreateFolderZipOptions {
371 include: req.include,
372 exclude: req.exclude,
373 };
374
375 let span = tracing::Span::current();
376
377 let (task_id, created_at) = background_task(
379 db.clone(),
380 scope.clone(),
381 async move {
382 let result = create_folder_zip(&db, &storage, &folder, options)
383 .await
384 .map_err(|error| {
385 tracing::error!(?error, "failed to upload file");
386 DynHttpError::from(HttpFolderError::CreateZipFile)
387 })
388 .and_then(|value| {
390 serde_json::to_value(&value).map_err(|error| {
391 tracing::error!(?error, "failed to serialize upload task outcome");
392 DynHttpError::from(HttpCommonError::ServerError)
393 })
394 });
395
396 match result {
397 Ok(value) => (TaskStatus::Completed, value),
398 Err(error) => (
399 TaskStatus::Failed,
400 serde_json::json!({ "error": error.to_string() }),
401 ),
402 }
403 }
404 .instrument(span),
407 )
408 .await
409 .map_err(|error| {
410 tracing::error!(?error, "failed to create background task");
411 HttpCommonError::ServerError
412 })?;
413
414 Ok(Json(UploadTaskResponse {
415 task_id,
416 created_at,
417 }))
418}