Skip to main content

docbox_http/models/
file.rs

1use crate::error::HttpError;
2use axum::http::StatusCode;
3use axum_typed_multipart::{FieldData, TryFromMultipart};
4use bytes::Bytes;
5use chrono::{DateTime, Utc};
6use docbox_core::processing::{ProcessingConfig, ProcessingError};
7use docbox_core::{
8    database::models::{
9        file::{FileId, FileWithExtra},
10        folder::FolderId,
11        generated_file::GeneratedFile,
12        presigned_upload_task::PresignedUploadTaskId,
13        tasks::TaskId,
14    },
15    files::upload_file::UploadFileError,
16};
17use garde::Validate;
18use mime::Mime;
19use serde::{Deserialize, Serialize};
20use serde_with::serde_as;
21use std::{collections::HashMap, marker::PhantomData};
22use thiserror::Error;
23use utoipa::ToSchema;
24
25/// Request to create a new presigned file upload
26#[serde_as]
27#[derive(Debug, Deserialize, Validate, ToSchema)]
28pub struct CreatePresignedRequest {
29    /// Name of the file being uploaded
30    #[garde(length(min = 1, max = 255))]
31    #[schema(min_length = 1, max_length = 255)]
32    pub name: String,
33
34    /// ID of the folder to store the file in
35    #[garde(skip)]
36    #[schema(value_type = Uuid)]
37    pub folder_id: FolderId,
38
39    /// Size of the file being uploaded in bytes. Must match the size of the
40    /// file being uploaded
41    #[garde(range(min = 1))]
42    #[schema(minimum = 1)]
43    pub size: i32,
44
45    /// Mime type of the file
46    #[garde(skip)]
47    #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
48    #[schema(value_type = Option<String>)]
49    pub mime: Option<Mime>,
50
51    /// Optional ID of the parent file if this file is associated as a child
52    /// of another file. Mainly used to associating attachments to email files
53    #[garde(skip)]
54    #[schema(value_type = Option<Uuid>)]
55    pub parent_id: Option<FileId>,
56
57    /// Optional processing config
58    #[garde(skip)]
59    pub processing_config: Option<ProcessingConfig>,
60
61    /// Whether to disable mime sniffing for the file. When false/not specified
62    /// if a application/octet-stream mime type is provided the file name
63    /// will be used to attempt to determine the real mime type
64    #[garde(skip)]
65    pub disable_mime_sniffing: Option<bool>,
66}
67
68/// Response describing how to upload the presigned file and the ID
69/// for polling the progress
70#[derive(Serialize, ToSchema)]
71pub struct PresignedUploadResponse {
72    /// ID of the file upload task to poll
73    #[schema(value_type = Uuid)]
74    pub task_id: PresignedUploadTaskId,
75    /// HTTP method to use when uploading the file
76    pub method: String,
77    /// URL to upload the file to
78    pub uri: String,
79    /// Headers to include on the file upload request
80    pub headers: HashMap<String, String>,
81}
82
83#[derive(Serialize, ToSchema)]
84#[serde(tag = "status")]
85#[allow(clippy::large_enum_variant)]
86pub enum PresignedStatusResponse {
87    /// Presigned upload is currently pending
88    Pending,
89    /// Presigned upload is completed
90    Complete {
91        /// The uploaded file
92        file: FileWithExtra,
93        /// The generated file
94        generated: Vec<GeneratedFile>,
95    },
96    /// Presigned upload failed
97    Failed {
98        /// The error that occurred
99        error: String,
100    },
101}
102
103#[derive(TryFromMultipart, Validate, ToSchema)]
104pub struct UploadFileRequest {
105    /// Name of the file being uploaded
106    #[garde(length(min = 1, max = 255))]
107    #[schema(min_length = 1, max_length = 255)]
108    pub name: String,
109
110    /// ID of the folder to store the file in
111    #[garde(skip)]
112    #[schema(value_type = Uuid)]
113    pub folder_id: FolderId,
114
115    /// The actual file you are uploading, ensure the mime type for the file
116    /// is set correctly
117    #[garde(skip)]
118    #[form_data(limit = "unlimited")]
119    #[schema(format = Binary,value_type= Vec<u8>)]
120    pub file: FieldData<Bytes>,
121
122    /// Optional mime type override, when not present the mime type will
123    /// be extracted from [UploadFileRequest::file]
124    #[garde(skip)]
125    pub mime: Option<String>,
126
127    /// Whether to process the file asynchronously returning a task
128    /// response instead of waiting for the upload
129    #[garde(skip)]
130    pub asynchronous: Option<bool>,
131
132    /// Whether to disable mime sniffing for the file. When false/not specified
133    /// if a application/octet-stream mime type is provided the file name
134    /// will be used to attempt to determine the real mime type
135    #[garde(skip)]
136    pub disable_mime_sniffing: Option<bool>,
137
138    /// Fixed file ID the file must use. Should only be used for
139    /// migrating existing files and maintaining the same UUID.
140    ///
141    /// Should not be provided for general use
142    #[garde(skip)]
143    #[schema(value_type = Option<Uuid>)]
144    pub fixed_id: Option<FileId>,
145
146    /// Optional ID of the parent file if this file is associated as a child
147    /// of another file. Mainly used to associating attachments to email files
148    #[garde(skip)]
149    #[schema(value_type = Option<Uuid>)]
150    pub parent_id: Option<FileId>,
151
152    /// Optional JSON encoded processing config
153    #[garde(skip)]
154    pub processing_config: Option<String>,
155}
156
157#[derive(Debug, Serialize, ToSchema)]
158#[serde(untagged)]
159pub enum FileUploadResponse {
160    Sync(Box<UploadedFile>),
161    Async(UploadTaskResponse),
162}
163
164#[derive(Debug, Serialize, ToSchema)]
165pub struct UploadedFile {
166    /// The uploaded file itself
167    pub file: FileWithExtra,
168    /// Generated data alongside the file
169    pub generated: Vec<GeneratedFile>,
170    /// Additional files created and uploaded from processing the file
171    #[schema(no_recursion)]
172    pub additional_files: Vec<UploadedFile>,
173}
174
175/// Request to rename and or move a file
176#[derive(Debug, Validate, Deserialize, ToSchema)]
177pub struct UpdateFileRequest {
178    /// Name for the folder
179    #[garde(inner(length(min = 1, max = 255)))]
180    #[schema(min_length = 1, max_length = 255)]
181    pub name: Option<String>,
182
183    /// New parent folder for the folder
184    #[garde(skip)]
185    #[schema(value_type = Option<Uuid>)]
186    pub folder_id: Option<FolderId>,
187
188    /// Whether to pin the file
189    #[garde(skip)]
190    #[schema(value_type = Option<bool>)]
191    pub pinned: Option<bool>,
192}
193
194/// Response for requesting a document box
195#[derive(Debug, Serialize, ToSchema)]
196pub struct FileResponse {
197    /// The file itself
198    pub file: FileWithExtra,
199    /// Files generated from the file (thumbnails, pdf, etc)
200    pub generated: Vec<GeneratedFile>,
201}
202
203#[derive(Default, Debug, Deserialize)]
204#[serde(default)]
205pub struct RawFileQuery {
206    pub download: bool,
207}
208
209/// Response from creating an upload
210#[derive(Debug, Serialize, ToSchema)]
211pub struct UploadTaskResponse {
212    #[schema(value_type = Uuid)]
213    pub task_id: TaskId,
214    pub created_at: DateTime<Utc>,
215}
216
217/// Request to rename and or move a file
218#[derive(Debug, Validate, Deserialize, ToSchema)]
219pub struct GetPresignedRequest {
220    /// Expiry time in seconds for the presigned URL
221    #[garde(skip)]
222    #[schema(default = 900)]
223    pub expires_at: Option<i64>,
224}
225
226#[derive(Serialize, ToSchema)]
227pub struct PresignedDownloadResponse {
228    pub method: String,
229    pub uri: String,
230    pub headers: HashMap<String, String>,
231    pub expires_at: DateTime<Utc>,
232}
233
234/// Type hint type for Utoipa to indicate a binary response type
235#[derive(ToSchema)]
236#[schema(value_type = String, format = Binary)]
237pub struct BinaryResponse(PhantomData<Vec<u8>>);
238
239#[derive(Debug, Error)]
240pub enum HttpFileError {
241    #[error("unknown file")]
242    UnknownFile,
243
244    #[error("unknown task")]
245    UnknownTask,
246
247    #[error("file size is larger than the maximum allowed size (requested: {0}, maximum: {1})")]
248    FileTooLarge(i32, i32),
249
250    #[error("fixed file id already in use")]
251    FileIdInUse,
252
253    #[error("request file mime content type is invalid")]
254    InvalidMimeType,
255
256    #[error("no matching generated file")]
257    NoMatchingGenerated,
258
259    #[allow(unused)]
260    #[error("unsupported file type")]
261    UnsupportedFileType,
262
263    #[error(transparent)]
264    UploadFileError(UploadFileError),
265}
266
267impl HttpError for HttpFileError {
268    fn status(&self) -> axum::http::StatusCode {
269        match self {
270            HttpFileError::FileTooLarge(_, _) => StatusCode::BAD_REQUEST,
271            HttpFileError::FileIdInUse => StatusCode::CONFLICT,
272            HttpFileError::UnknownFile
273            | HttpFileError::NoMatchingGenerated
274            | HttpFileError::UnknownTask => StatusCode::NOT_FOUND,
275            HttpFileError::UnsupportedFileType | HttpFileError::InvalidMimeType => {
276                StatusCode::BAD_REQUEST
277            }
278            HttpFileError::UploadFileError(error) => match error {
279                // Some processing errors can be assumed as the files fault
280                UploadFileError::Processing(
281                    ProcessingError::MalformedFile(_)
282                    | ProcessingError::ReadPdfInfo(_)
283                    | ProcessingError::ExtractFileText(_)
284                    | ProcessingError::DecodeImage(_)
285                    | ProcessingError::GenerateThumbnail(_)
286                    | ProcessingError::Email(_),
287                ) => StatusCode::UNPROCESSABLE_ENTITY,
288
289                _ => StatusCode::INTERNAL_SERVER_ERROR,
290            },
291        }
292    }
293}