1use crate::{
4 error::{DynHttpError, HttpCommonError, HttpErrorResponse, HttpResult, HttpStatusResult},
5 extensions::max_file_size::MaxFileSizeBytes,
6 middleware::{
7 action_user::{ActionUser, UserParams},
8 tenant::{TenantDb, TenantEvents, TenantParams, TenantSearch, TenantStorage},
9 },
10 models::{
11 document_box::DocumentBoxScope,
12 file::{
13 BinaryResponse, CreatePresignedRequest, FileResponse, FileUploadResponse,
14 GetPresignedRequest, HttpFileError, PresignedDownloadResponse, PresignedStatusResponse,
15 PresignedUploadResponse, RawFileQuery, UpdateFileRequest, UploadFileRequest,
16 UploadTaskResponse, UploadedFile,
17 },
18 folder::HttpFolderError,
19 },
20};
21use axum::{
22 Extension, Json,
23 body::Body,
24 extract::{Path, Query},
25 http::{HeaderValue, Response, StatusCode, header},
26};
27use axum_typed_multipart::TypedMultipart;
28use axum_valid::Garde;
29use docbox_core::{
30 database::models::{
31 edit_history::EditHistory,
32 file::{File, FileId, FileWithExtra},
33 folder::Folder,
34 generated_file::{GeneratedFile, GeneratedFileType},
35 presigned_upload_task::{PresignedTaskStatus, PresignedUploadTask, PresignedUploadTaskId},
36 tasks::TaskStatus,
37 user::User,
38 },
39 files::{
40 delete_file::delete_file,
41 update_file::{UpdateFile, UpdateFileError},
42 upload_file::{UploadFile, UploadedFileData, upload_file},
43 upload_file_presigned::{CreatePresigned, create_presigned_upload},
44 },
45 processing::{ProcessingConfig, ProcessingLayer},
46 search::models::{FileSearchRequest, FileSearchResultResponse},
47 tasks::background_task::background_task,
48 utils::file::get_file_name_ext,
49};
50use mime::Mime;
51use std::{str::FromStr, time::Duration};
52use tracing::Instrument;
53
54pub const FILE_TAG: &str = "File";
55
56#[utoipa::path(
76 post,
77 operation_id = "file_upload",
78 tag = FILE_TAG,
79 path = "/box/{scope}/file",
80 responses(
81 (status = 200, description = "Upload or task created successfully", body = FileUploadResponse),
82 (status = 400, description = "Malformed or invalid request not meeting validation requirements", body = HttpErrorResponse),
83 (status = 404, description = "Target folder could not be found", body = HttpErrorResponse),
84 (status = 409, description = "Fixed ID is already in use", body = HttpErrorResponse),
85 (status = 500, description = "Internal server error", body = HttpErrorResponse)
86 ),
87 request_body(content = UploadFileRequest, description = "Multipart upload", content_type = "multipart/form-data"),
88 params(
89 ("scope" = DocumentBoxScope, Path, description = "Scope to create the file within"),
90 TenantParams,
91 UserParams
92 )
93)]
94#[tracing::instrument(skip_all, fields(%scope))]
95#[allow(clippy::too_many_arguments)]
96pub async fn upload(
97 action_user: ActionUser,
98 TenantDb(db): TenantDb,
99 TenantSearch(search): TenantSearch,
100 TenantStorage(storage): TenantStorage,
101 TenantEvents(events): TenantEvents,
102 Extension(processing): Extension<ProcessingLayer>,
104 Path(DocumentBoxScope(scope)): Path<DocumentBoxScope>,
106 Garde(TypedMultipart(req)): Garde<TypedMultipart<UploadFileRequest>>,
107) -> HttpResult<FileUploadResponse> {
108 let folder = Folder::find_by_id(&db, &scope, req.folder_id)
109 .await
110 .map_err(|error| {
111 tracing::error!(?error, "failed to query folder");
112 HttpCommonError::ServerError
113 })?
114 .ok_or(HttpFolderError::UnknownTargetFolder)?;
115
116 if let Some(fixed_id) = req.fixed_id
117 && File::find(&db, &scope, fixed_id)
118 .await
119 .map_err(|error| {
120 tracing::error!(?error, "failed to check for duplicate files");
121 HttpCommonError::ServerError
122 })?
123 .is_some()
124 {
125 return Err(DynHttpError::from(HttpFileError::FileIdInUse));
126 }
127
128 let content_type = req.mime.or(req.file.metadata.content_type);
129
130 let mut mime = match content_type {
131 Some(value) => Mime::from_str(&value).map_err(|_| HttpFileError::InvalidMimeType)?,
132 None => mime::APPLICATION_OCTET_STREAM,
134 };
135
136 if mime == mime::APPLICATION_OCTET_STREAM
139 && req.disable_mime_sniffing.is_none_or(|value| !value)
140 {
141 let guessed_mime = get_file_name_ext(&req.name).and_then(|ext| {
142 let guesses = mime_guess::from_ext(&ext);
143 guesses.first()
144 });
145
146 if let Some(guessed_mime) = guessed_mime {
147 mime = guessed_mime
148 }
149 }
150
151 let processing_config: Option<ProcessingConfig> = match &req.processing_config {
153 Some(value) => match serde_json::from_str(value) {
154 Ok(value) => value,
155 Err(error) => {
156 tracing::error!(?error, "failed to deserialize processing config");
157 None
158 }
159 },
160 None => None,
161 };
162
163 let created_by = action_user.store_user(&db).await?;
165
166 let upload = UploadFile {
168 fixed_id: req.fixed_id,
169 parent_id: req.parent_id,
170 folder_id: folder.id,
171 document_box: folder.document_box.clone(),
172 name: req.name,
173 mime,
174 file_bytes: req.file.contents,
175 created_by: created_by.as_ref().map(|value| value.id.to_string()),
176 file_key: None,
177 processing_config,
178 };
179
180 if !req.asynchronous.unwrap_or_default() {
182 let data = upload_file(&db, &search, &storage, &processing, &events, upload)
183 .await
184 .map_err(|error| {
185 tracing::error!(?error, "failed to upload file");
186 HttpFileError::UploadFileError(error)
187 })?;
188 let result = map_uploaded_file(data, &created_by);
189 return Ok(Json(FileUploadResponse::Sync(Box::new(result))));
190 }
191
192 let span = tracing::Span::current();
193
194 let (task_id, created_at) = background_task(
196 db.clone(),
197 scope.clone(),
198 async move {
199 let result = upload_file(&db, &search, &storage, &processing, &events, upload)
200 .await
201 .map_err(|error| {
202 tracing::error!(?error, "failed to upload file");
203 DynHttpError::from(HttpFileError::UploadFileError(error))
204 })
205 .map(|data| map_uploaded_file(data, &created_by))
207 .and_then(|value| {
209 serde_json::to_value(&value).map_err(|error| {
210 tracing::error!(?error, "failed to serialize upload task outcome");
211 DynHttpError::from(HttpCommonError::ServerError)
212 })
213 });
214
215 match result {
216 Ok(value) => (TaskStatus::Completed, value),
217 Err(error) => (
218 TaskStatus::Failed,
219 serde_json::json!({ "error": error.to_string() }),
220 ),
221 }
222 }
223 .instrument(span),
226 )
227 .await
228 .map_err(|error| {
229 tracing::error!(?error, "failed to create background task");
230 HttpCommonError::ServerError
231 })?;
232
233 Ok(Json(FileUploadResponse::Async(UploadTaskResponse {
234 task_id,
235 created_at,
236 })))
237}
238
239fn map_uploaded_file(data: UploadedFileData, created_by: &Option<User>) -> UploadedFile {
242 let UploadedFileData {
243 file,
244 generated,
245 additional_files,
246 } = data;
247
248 UploadedFile {
249 file: FileWithExtra {
250 file,
251 created_by: created_by.clone(),
252 last_modified_at: None,
253 last_modified_by: None,
254 },
255 generated,
256
257 additional_files: additional_files
259 .into_iter()
260 .map(|data| map_uploaded_file(data, created_by))
261 .collect(),
262 }
263}
264
265#[utoipa::path(
273 post,
274 operation_id = "file_create_presigned",
275 tag = FILE_TAG,
276 path = "/box/{scope}/file/presigned",
277 responses(
278 (status = 201, description = "Created presigned upload successfully", body = PresignedUploadResponse),
279 (status = 400, description = "Malformed or invalid request not meeting validation requirements", body = HttpErrorResponse),
280 (status = 404, description = "Target folder could not be found", body = HttpErrorResponse),
281 (status = 500, description = "Internal server error", body = HttpErrorResponse)
282 ),
283 params(
284 ("scope" = DocumentBoxScope, Path, description = "Scope to create the file within"),
285 TenantParams,
286 UserParams
287 )
288)]
289#[tracing::instrument(skip_all, fields(%scope, ?req))]
290pub async fn create_presigned(
291 action_user: ActionUser,
292 Extension(MaxFileSizeBytes(max_file_size)): Extension<MaxFileSizeBytes>,
293 TenantDb(db): TenantDb,
294 TenantStorage(storage): TenantStorage,
295 Path(DocumentBoxScope(scope)): Path<DocumentBoxScope>,
296 Garde(Json(req)): Garde<Json<CreatePresignedRequest>>,
297) -> Result<(StatusCode, Json<PresignedUploadResponse>), DynHttpError> {
298 if req.size > max_file_size {
299 return Err(HttpFileError::FileTooLarge(req.size, max_file_size).into());
300 }
301
302 let folder = Folder::find_by_id(&db, &scope, req.folder_id)
303 .await
304 .map_err(|error| {
305 tracing::error!(?error, "failed to query folder");
306 HttpCommonError::ServerError
307 })?
308 .ok_or(HttpFolderError::UnknownTargetFolder)?;
309
310 let created_by = action_user.store_user(&db).await?;
312
313 let mut mime = req.mime.unwrap_or(mime::APPLICATION_OCTET_STREAM);
314
315 if mime == mime::APPLICATION_OCTET_STREAM
318 && req.disable_mime_sniffing.is_none_or(|value| !value)
319 {
320 let guessed_mime = get_file_name_ext(&req.name).and_then(|ext| {
321 let guesses = mime_guess::from_ext(&ext);
322 guesses.first()
323 });
324
325 if let Some(guessed_mime) = guessed_mime {
326 mime = guessed_mime
327 }
328 }
329
330 let response = create_presigned_upload(
331 &db,
332 &storage,
333 CreatePresigned {
334 name: req.name,
335 document_box: scope,
336 folder,
337 size: req.size,
338 mime,
339 created_by: created_by.map(|user| user.id),
340 parent_id: req.parent_id,
341 processing_config: req.processing_config,
342 },
343 )
344 .await
345 .map_err(|error| {
346 tracing::error!(?error, "failed to create presigned upload");
347 HttpCommonError::ServerError
348 })?;
349
350 Ok((
351 StatusCode::CREATED,
352 Json(PresignedUploadResponse {
353 task_id: response.task_id,
354 method: response.method,
355 uri: response.uri,
356 headers: response.headers,
357 }),
358 ))
359}
360
361#[utoipa::path(
367 get,
368 operation_id = "file_get_presigned",
369 tag = FILE_TAG,
370 path = "/box/{scope}/file/presigned/{task_id}",
371 responses(
372 (status = 200, description = "Obtained presigned upload successfully", body = PresignedStatusResponse),
373 (status = 404, description = "Presigned upload not found", body = HttpErrorResponse),
374 (status = 500, description = "Internal server error", body = HttpErrorResponse)
375 ),
376 params(
377 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
378 ("task_id" = Uuid, Path, description = "ID of the task to query"),
379 TenantParams
380 )
381)]
382#[tracing::instrument(skip_all, fields(%scope, %task_id))]
383pub async fn get_presigned(
384 TenantDb(db): TenantDb,
385 Path((scope, task_id)): Path<(DocumentBoxScope, PresignedUploadTaskId)>,
386) -> HttpResult<PresignedStatusResponse> {
387 let DocumentBoxScope(scope) = scope;
388
389 let task = PresignedUploadTask::find(&db, &scope, task_id)
390 .await
391 .map_err(|error| {
392 tracing::error!(?error, "failed to query presigned upload");
393 HttpCommonError::ServerError
394 })?
395 .ok_or(HttpFileError::UnknownTask)?;
396
397 let file_id = match task.status {
398 PresignedTaskStatus::Pending => return Ok(Json(PresignedStatusResponse::Pending)),
399 PresignedTaskStatus::Completed { file_id } => file_id,
400 PresignedTaskStatus::Failed { error } => {
401 return Ok(Json(PresignedStatusResponse::Failed { error }));
402 }
403 };
404
405 let file = File::find_with_extra(&db, &scope, file_id)
406 .await
407 .map_err(|error| {
408 tracing::error!(?error, "failed to query file");
409 HttpCommonError::ServerError
410 })?
411 .ok_or(HttpFileError::UnknownFile)?;
412
413 let generated = GeneratedFile::find_all(&db, file_id)
414 .await
415 .map_err(|error| {
416 tracing::error!(?error, "failed to query generated files");
417 HttpCommonError::ServerError
418 })?;
419
420 Ok(Json(PresignedStatusResponse::Complete { file, generated }))
421}
422
423#[utoipa::path(
428 get,
429 operation_id = "file_get",
430 tag = FILE_TAG,
431 path = "/box/{scope}/file/{file_id}",
432 responses(
433 (status = 200, description = "Obtained file successfully", body = FileResponse),
434 (status = 404, description = "File not found", body = HttpErrorResponse),
435 (status = 500, description = "Internal server error", body = HttpErrorResponse)
436 ),
437 params(
438 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
439 ("file_id" = Uuid, Path, description = "ID of the file to query"),
440 TenantParams
441 )
442)]
443#[tracing::instrument(skip_all, fields(%scope, %file_id))]
444pub async fn get(
445 TenantDb(db): TenantDb,
446 Path((scope, file_id)): Path<(DocumentBoxScope, FileId)>,
447) -> HttpResult<FileResponse> {
448 let DocumentBoxScope(scope) = scope;
449 let file = File::find_with_extra(&db, &scope, file_id)
450 .await
451 .map_err(|error| {
452 tracing::error!(?error, "failed to query file");
453 HttpCommonError::ServerError
454 })?
455 .ok_or(HttpFileError::UnknownFile)?;
456
457 let generated = GeneratedFile::find_all(&db, file_id)
458 .await
459 .map_err(|error| {
460 tracing::error!(?error, "failed to query generated files");
461 HttpCommonError::ServerError
462 })?;
463
464 Ok(Json(FileResponse { file, generated }))
465}
466
467#[utoipa::path(
472 get,
473 operation_id = "file_get_children",
474 tag = FILE_TAG,
475 path = "/box/{scope}/file/{file_id}/children",
476 responses(
477 (status = 200, description = "Obtained children successfully", body = [FileWithExtra]),
478 (status = 404, description = "File not found", body = HttpErrorResponse),
479 (status = 500, description = "Internal server error", body = HttpErrorResponse)
480 ),
481 params(
482 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
483 ("file_id" = Uuid, Path, description = "ID of the file to query"),
484 TenantParams
485 )
486)]
487#[tracing::instrument(skip_all, fields(%scope, %file_id))]
488pub async fn get_children(
489 TenantDb(db): TenantDb,
490 Path((scope, file_id)): Path<(DocumentBoxScope, FileId)>,
491) -> HttpResult<Vec<FileWithExtra>> {
492 let DocumentBoxScope(scope) = scope;
493
494 _ = File::find_with_extra(&db, &scope, file_id)
496 .await
497 .map_err(|error| {
498 tracing::error!(?error, "failed to query file");
499 HttpCommonError::ServerError
500 })?
501 .ok_or(HttpFileError::UnknownFile)?;
502
503 let files = File::find_by_parent_file_with_extra(&db, file_id)
504 .await
505 .map_err(|error| {
506 tracing::error!(?error, "failed to query file children");
507 HttpCommonError::ServerError
508 })?;
509
510 Ok(Json(files))
511}
512
513#[utoipa::path(
517 get,
518 operation_id = "file_edit_history",
519 tag = FILE_TAG,
520 path = "/box/{scope}/file/{file_id}/edit-history",
521 responses(
522 (status = 200, description = "Obtained edit-history successfully", body = [EditHistory]),
523 (status = 404, description = "File not found", body = HttpErrorResponse),
524 (status = 500, description = "Internal server error", body = HttpErrorResponse)
525 ),
526 params(
527 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
528 ("file_id" = Uuid, Path, description = "ID of the file to query"),
529 TenantParams
530 )
531)]
532#[tracing::instrument(skip_all, fields(%scope, %file_id))]
533pub async fn get_edit_history(
534 TenantDb(db): TenantDb,
535 Path((scope, file_id)): Path<(DocumentBoxScope, FileId)>,
536) -> HttpResult<Vec<EditHistory>> {
537 let DocumentBoxScope(scope) = scope;
538
539 _ = File::find(&db, &scope, file_id)
540 .await
541 .map_err(|error| {
542 tracing::error!(?error, "failed to query file");
543 HttpCommonError::ServerError
544 })?
545 .ok_or(HttpFileError::UnknownFile)?;
546
547 let edit_history = EditHistory::all_by_file(&db, file_id)
548 .await
549 .map_err(|error| {
550 tracing::error!(?error, "failed to query file history");
551 HttpCommonError::ServerError
552 })?;
553
554 Ok(Json(edit_history))
555}
556
557#[utoipa::path(
561 put,
562 operation_id = "file_update",
563 tag = FILE_TAG,
564 path = "/box/{scope}/file/{file_id}",
565 responses(
566 (status = 200, description = "Obtained edit-history successfully", body = [EditHistory]),
567 (status = 404, description = "File not found", body = HttpErrorResponse),
568 (status = 500, description = "Internal server error", body = HttpErrorResponse)
569 ),
570 params(
571 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
572 ("file_id" = Uuid, Path, description = "ID of the file to query"),
573 TenantParams,
574 UserParams
575 )
576)]
577#[tracing::instrument(skip_all, fields(%scope, %file_id, ?req))]
578pub async fn update(
579 action_user: ActionUser,
580 TenantDb(db): TenantDb,
581 TenantSearch(search): TenantSearch,
582 Path((scope, file_id)): Path<(DocumentBoxScope, FileId)>,
583 Garde(Json(req)): Garde<Json<UpdateFileRequest>>,
584) -> HttpStatusResult {
585 let DocumentBoxScope(scope) = scope;
586
587 let file = File::find(&db, &scope, file_id)
588 .await
589 .map_err(|error| {
590 tracing::error!(?error, "failed to query file");
591 HttpCommonError::ServerError
592 })?
593 .ok_or(HttpFileError::UnknownFile)?;
594
595 let user = action_user.store_user(&db).await?;
597 let user_id = user.as_ref().map(|value| value.id.to_string());
598
599 let update = UpdateFile {
600 folder_id: req.folder_id,
601 name: req.name,
602 pinned: req.pinned,
603 };
604
605 docbox_core::files::update_file::update_file(&db, &search, &scope, file, user_id, update)
606 .await
607 .map_err(|error| match error {
608 UpdateFileError::UnknownTargetFolder => {
609 DynHttpError::from(HttpFolderError::UnknownTargetFolder)
610 }
611 _ => DynHttpError::from(HttpCommonError::ServerError),
612 })?;
613
614 Ok(StatusCode::OK)
615}
616
617#[utoipa::path(
622 get,
623 operation_id = "file_get_raw",
624 tag = FILE_TAG,
625 path = "/box/{scope}/file/{file_id}/raw",
626 responses(
627 (status = 200, description = "Obtained raw file successfully", content_type = "application/octet-stream", body = BinaryResponse),
628 (status = 404, description = "File not found", body = HttpErrorResponse),
629 (status = 500, description = "Internal server error", body = HttpErrorResponse)
630 ),
631 params(
632 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
633 ("file_id" = Uuid, Path, description = "ID of the file to query"),
634 TenantParams
635 )
636)]
637#[tracing::instrument(skip_all, fields(%scope, %file_id, ?query))]
638pub async fn get_raw(
639 TenantDb(db): TenantDb,
640 TenantStorage(storage): TenantStorage,
641 Path((scope, file_id)): Path<(DocumentBoxScope, FileId)>,
642 Query(query): Query<RawFileQuery>,
643) -> Result<Response<Body>, DynHttpError> {
644 let DocumentBoxScope(scope) = scope;
645
646 let file = File::find(&db, &scope, file_id)
647 .await
648 .map_err(|error| {
649 tracing::error!(?error, "failed to query file");
650 HttpCommonError::ServerError
651 })?
652 .ok_or(HttpFileError::UnknownFile)?;
653
654 let byte_stream = storage.get_file(&file.file_key).await.map_err(|error| {
655 tracing::error!(?error, "failed to get file from storage");
656 HttpCommonError::ServerError
657 })?;
658
659 let body = axum::body::Body::from_stream(byte_stream);
660
661 let ty = if query.download {
662 "attachment"
663 } else {
664 "inline"
665 };
666
667 let disposition = format!("{};filename=\"{}\"", ty, file.name);
668
669 let csp = match mime::Mime::from_str(&file.mime) {
670 Ok(mime) if mime.type_() == mime::IMAGE => {
672 "default-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"
673 }
674 _ => "script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'",
676 };
677
678 Ok(Response::builder()
679 .header(header::CONTENT_TYPE, file.mime)
680 .header(header::CONTENT_SECURITY_POLICY, csp)
681 .header(
682 header::CONTENT_DISPOSITION,
683 HeaderValue::from_str(&disposition)?,
684 )
685 .body(body)?)
686}
687
688#[utoipa::path(
694 post,
695 operation_id = "file_get_raw_presigned",
696 tag = FILE_TAG,
697 path = "/box/{scope}/file/{file_id}/raw-presigned",
698 responses(
699 (status = 200, description = "Obtained raw file successfully"),
700 (status = 404, description = "File not found", body = HttpErrorResponse),
701 (status = 500, description = "Internal server error", body = HttpErrorResponse)
702 ),
703 params(
704 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
705 ("file_id" = Uuid, Path, description = "ID of the file to download"),
706 TenantParams
707 )
708)]
709#[tracing::instrument(skip_all, fields(%scope, %file_id, ?req))]
710pub async fn get_raw_presigned(
711 TenantDb(db): TenantDb,
712 TenantStorage(storage): TenantStorage,
713 Path((scope, file_id)): Path<(DocumentBoxScope, FileId)>,
714 Json(req): Json<GetPresignedRequest>,
715) -> HttpResult<PresignedDownloadResponse> {
716 let DocumentBoxScope(scope) = scope;
717
718 let file = File::find(&db, &scope, file_id)
719 .await
720 .map_err(|error| {
721 tracing::error!(?error, "failed to query file");
722 HttpCommonError::ServerError
723 })?
724 .ok_or(HttpFileError::UnknownFile)?;
725
726 let expires_at = req.expires_at.unwrap_or(900);
727 let expires_at = Duration::from_secs(expires_at as u64);
728
729 let (signed_request, expires_at) = storage
730 .create_presigned_download(&file.file_key, expires_at)
731 .await
732 .map_err(|error| {
733 tracing::error!(?error, "failed to created file presigned download");
734 HttpCommonError::ServerError
735 })?;
736
737 Ok(Json(PresignedDownloadResponse {
738 method: signed_request.method().to_string(),
739 uri: signed_request.uri().to_string(),
740 headers: signed_request
741 .headers()
742 .map(|(key, value)| (key.to_string(), value.to_string()))
743 .collect(),
744 expires_at,
745 }))
746}
747
748#[utoipa::path(
758 get,
759 operation_id = "file_get_raw_named",
760 tag = FILE_TAG,
761 path = "/box/{scope}/file/{file_id}/raw/{file_name}",
762 responses(
763 (status = 200, description = "Obtained raw file successfully", content_type = "application/octet-stream", body = BinaryResponse),
764 (status = 404, description = "File not found", body = HttpErrorResponse),
765 (status = 500, description = "Internal server error", body = HttpErrorResponse)
766 ),
767 params(
768 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
769 ("file_id" = Uuid, Path, description = "ID of the file to query"),
770 ("file_name" = String, Path, description = "User defined file name for the download", allow_reserved = true),
771 TenantParams
772 )
773)]
774#[tracing::instrument(skip_all, fields(%scope, %file_id, ?query))]
775pub async fn get_raw_named(
776 db: TenantDb,
777 storage: TenantStorage,
778 Path((scope, file_id, _tail)): Path<(DocumentBoxScope, FileId, String)>,
779 query: Query<RawFileQuery>,
780) -> Result<Response<Body>, DynHttpError> {
781 get_raw(db, storage, Path((scope, file_id)), query).await
782}
783
784#[utoipa::path(
788 post,
789 operation_id = "file_search",
790 tag = FILE_TAG,
791 path = "/box/{scope}/file/{file_id}/search",
792 responses(
793 (status = 200, description = "Searched successfully", body = FileSearchResultResponse),
794 (status = 400, description = "Malformed or invalid request not meeting validation requirements", body = HttpErrorResponse),
795 (status = 404, description = "File not found", body = HttpErrorResponse),
796 (status = 500, description = "Internal server error", body = HttpErrorResponse)
797 ),
798 params(
799 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
800 ("file_id" = Uuid, Path, description = "ID of the file to query"),
801 TenantParams
802 )
803)]
804#[tracing::instrument(skip_all, fields(%scope, %file_id, ?req))]
805pub async fn search(
806 TenantDb(db): TenantDb,
807 TenantSearch(search): TenantSearch,
808 Path((scope, file_id)): Path<(DocumentBoxScope, FileId)>,
809 Json(req): Json<FileSearchRequest>,
810) -> HttpResult<FileSearchResultResponse> {
811 let DocumentBoxScope(scope) = scope;
812
813 _ = File::find(&db, &scope, file_id)
815 .await
816 .map_err(|error| {
817 tracing::error!(?error, "failed to query file");
818 HttpCommonError::ServerError
819 })?
820 .ok_or(HttpFileError::UnknownFile)?;
821
822 let result = search
823 .search_index_file(&scope, file_id, req)
824 .await
825 .map_err(|error| {
826 tracing::error!(?error, "failed to search document box");
827 HttpCommonError::ServerError
828 })?;
829
830 Ok(Json(FileSearchResultResponse {
831 total_hits: result.total_hits,
832 results: result.results,
833 }))
834}
835
836#[utoipa::path(
840 delete,
841 operation_id = "file_delete",
842 tag = FILE_TAG,
843 path = "/box/{scope}/file/{file_id}",
844 responses(
845 (status = 204, description = "Deleted file successfully"),
846 (status = 404, description = "File not found", body = HttpErrorResponse),
847 (status = 500, description = "Internal server error", body = HttpErrorResponse)
848 ),
849 params(
850 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
851 ("file_id" = Uuid, Path, description = "ID of the file to delete"),
852 TenantParams
853 )
854)]
855#[tracing::instrument(skip_all, fields(%scope, %file_id))]
856pub async fn delete(
857 TenantDb(db): TenantDb,
858 TenantStorage(storage): TenantStorage,
859 TenantSearch(search): TenantSearch,
860 TenantEvents(events): TenantEvents,
861 Path((scope, file_id)): Path<(DocumentBoxScope, FileId)>,
862) -> HttpStatusResult {
863 let DocumentBoxScope(scope) = scope;
864
865 let file = File::find(&db, &scope, file_id)
866 .await
867 .map_err(|error| {
868 tracing::error!(?error, "failed to query file");
869 HttpCommonError::ServerError
870 })?
871 .ok_or(HttpFileError::UnknownFile)?;
872
873 delete_file(&db, &storage, &search, &events, file, scope)
874 .await
875 .map_err(|error| {
876 tracing::error!(?error, "failed to delete file");
877 HttpCommonError::ServerError
878 })?;
879
880 Ok(StatusCode::NO_CONTENT)
881}
882
883#[utoipa::path(
889 get,
890 operation_id = "file_get_generated",
891 tag = FILE_TAG,
892 path = "/box/{scope}/file/{file_id}/generated/{type}",
893 responses(
894 (status = 200, description = "Obtained generated file successfully", body = GeneratedFile),
895 (status = 404, description = "Generated file not found", body = HttpErrorResponse),
896 (status = 500, description = "Internal server error", body = HttpErrorResponse)
897 ),
898 params(
899 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
900 ("file_id" = Uuid, Path, description = "ID of the file to query"),
901 ("type" = GeneratedFileType, Path, description = "ID of the file to query"),
902 TenantParams
903 )
904)]
905#[tracing::instrument(skip_all, fields(%scope, %file_id, %generated_type))]
906pub async fn get_generated(
907 TenantDb(db): TenantDb,
908 Path((scope, file_id, generated_type)): Path<(DocumentBoxScope, FileId, GeneratedFileType)>,
909) -> HttpResult<GeneratedFile> {
910 let DocumentBoxScope(scope) = scope;
911
912 let file = GeneratedFile::find(&db, &scope, file_id, generated_type)
913 .await
914 .map_err(|error| {
915 tracing::error!(?error, "failed to query generated file");
916 HttpCommonError::ServerError
917 })?
918 .ok_or(HttpFileError::NoMatchingGenerated)?;
919
920 Ok(Json(file))
921}
922
923#[utoipa::path(
928 get,
929 operation_id = "file_get_generated_raw",
930 tag = FILE_TAG,
931 path = "/box/{scope}/file/{file_id}/generated/{type}/raw",
932 responses(
933 (status = 200, description = "Obtained raw file successfully", content_type = "application/octet-stream", body = BinaryResponse),
934 (status = 404, description = "Generated file not found", body = HttpErrorResponse),
935 (status = 500, description = "Internal server error", body = HttpErrorResponse)
936 ),
937 params(
938 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
939 ("file_id" = Uuid, Path, description = "ID of the file to query"),
940 ("type" = GeneratedFileType, Path, description = "ID of the file to query"),
941 TenantParams
942 )
943)]
944#[tracing::instrument(skip_all, fields(%scope, %file_id, %generated_type))]
945pub async fn get_generated_raw(
946 TenantDb(db): TenantDb,
947 TenantStorage(storage): TenantStorage,
948 Path((scope, file_id, generated_type)): Path<(DocumentBoxScope, FileId, GeneratedFileType)>,
949) -> Result<Response<Body>, DynHttpError> {
950 let DocumentBoxScope(scope) = scope;
951
952 let file = GeneratedFile::find(&db, &scope, file_id, generated_type)
953 .await
954 .map_err(|error| {
955 tracing::error!(?error, "failed to query generated file");
956 HttpCommonError::ServerError
957 })?
958 .ok_or(HttpFileError::NoMatchingGenerated)?;
959
960 let byte_stream = storage.get_file(&file.file_key).await.map_err(|error| {
961 tracing::error!(?error, "failed to file from storage");
962 HttpCommonError::ServerError
963 })?;
964
965 let body = axum::body::Body::from_stream(byte_stream);
966
967 let csp = match mime::Mime::from_str(&file.mime) {
968 Ok(mime) if mime.type_() == mime::IMAGE => "default-src 'none'; img-src 'self' data:;",
970 _ => "script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'",
972 };
973
974 Ok(Response::builder()
975 .header(header::CONTENT_TYPE, file.mime)
976 .header(header::CONTENT_SECURITY_POLICY, csp)
977 .header(
978 header::CONTENT_DISPOSITION,
979 HeaderValue::from_str("inline;filename=\"preview.pdf\"")?,
980 )
981 .body(body)?)
982}
983
984#[utoipa::path(
990 post,
991 operation_id = "file_get_generated_raw_presigned",
992 tag = FILE_TAG,
993 path = "/box/{scope}/file/{file_id}/generated/{type}/raw-presigned",
994 responses(
995 (status = 200, description = "Obtained raw file successfully"),
996 (status = 404, description = "Generated file not found", body = HttpErrorResponse),
997 (status = 500, description = "Internal server error", body = HttpErrorResponse)
998 ),
999 params(
1000 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
1001 ("file_id" = Uuid, Path, description = "ID of the file to query"),
1002 ("type" = GeneratedFileType, Path, description = "ID of the file to query"),
1003 TenantParams
1004 )
1005)]
1006#[tracing::instrument(skip_all, fields(%scope, %file_id, %generated_type, ?req))]
1007pub async fn get_generated_raw_presigned(
1008 TenantDb(db): TenantDb,
1009 TenantStorage(storage): TenantStorage,
1010 Path((scope, file_id, generated_type)): Path<(DocumentBoxScope, FileId, GeneratedFileType)>,
1011 Json(req): Json<GetPresignedRequest>,
1012) -> HttpResult<PresignedDownloadResponse> {
1013 let DocumentBoxScope(scope) = scope;
1014
1015 let file = GeneratedFile::find(&db, &scope, file_id, generated_type)
1016 .await
1017 .map_err(|error| {
1018 tracing::error!(?error, "failed to query generated file");
1019 HttpCommonError::ServerError
1020 })?
1021 .ok_or(HttpFileError::NoMatchingGenerated)?;
1022
1023 let expires_at = req.expires_at.unwrap_or(900);
1024 let expires_at = Duration::from_secs(expires_at as u64);
1025
1026 let (signed_request, expires_at) = storage
1027 .create_presigned_download(&file.file_key, expires_at)
1028 .await
1029 .map_err(|_| HttpCommonError::ServerError)?;
1030
1031 Ok(Json(PresignedDownloadResponse {
1032 method: signed_request.method().to_string(),
1033 uri: signed_request.uri().to_string(),
1034 headers: signed_request
1035 .headers()
1036 .map(|(key, value)| (key.to_string(), value.to_string()))
1037 .collect(),
1038 expires_at,
1039 }))
1040}
1041
1042#[utoipa::path(
1049 get,
1050 operation_id = "file_get_generated_raw_named",
1051 tag = FILE_TAG,
1052 path = "/box/{scope}/file/{file_id}/generated/{type}/raw/{file_name}",
1053 responses(
1054 (status = 200, description = "Obtained raw file successfully", content_type = "application/octet-stream", body = BinaryResponse),
1055 (status = 404, description = "Generated file not found", body = HttpErrorResponse),
1056 (status = 500, description = "Internal server error", body = HttpErrorResponse)
1057 ),
1058 params(
1059 ("scope" = DocumentBoxScope, Path, description = "Scope the file resides within"),
1060 ("file_id" = Uuid, Path, description = "ID of the file to query"),
1061 ("type" = GeneratedFileType, Path, description = "ID of the file to query"),
1062 ("file_name" = String, Path, description = "User defined file name for the download", allow_reserved = true),
1063 TenantParams
1064 )
1065)]
1066#[tracing::instrument(skip_all, fields(%scope, %file_id, %generated_type))]
1067pub async fn get_generated_raw_named(
1068 db: TenantDb,
1069 storage: TenantStorage,
1070 Path((scope, file_id, generated_type, _tail)): Path<(
1071 DocumentBoxScope,
1072 FileId,
1073 GeneratedFileType,
1074 String,
1075 )>,
1076) -> Result<Response<Body>, DynHttpError> {
1077 get_generated_raw(db, storage, Path((scope, file_id, generated_type))).await
1078}