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#[serde_as]
27#[derive(Debug, Deserialize, Validate, ToSchema)]
28pub struct CreatePresignedRequest {
29 #[garde(length(min = 1, max = 255))]
31 #[schema(min_length = 1, max_length = 255)]
32 pub name: String,
33
34 #[garde(skip)]
36 #[schema(value_type = Uuid)]
37 pub folder_id: FolderId,
38
39 #[garde(range(min = 1))]
42 #[schema(minimum = 1)]
43 pub size: i32,
44
45 #[garde(skip)]
47 #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
48 #[schema(value_type = Option<String>)]
49 pub mime: Option<Mime>,
50
51 #[garde(skip)]
54 #[schema(value_type = Option<Uuid>)]
55 pub parent_id: Option<FileId>,
56
57 #[garde(skip)]
59 pub processing_config: Option<ProcessingConfig>,
60
61 #[garde(skip)]
65 pub disable_mime_sniffing: Option<bool>,
66}
67
68#[derive(Serialize, ToSchema)]
71pub struct PresignedUploadResponse {
72 #[schema(value_type = Uuid)]
74 pub task_id: PresignedUploadTaskId,
75 pub method: String,
77 pub uri: String,
79 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 Pending,
89 Complete {
91 file: FileWithExtra,
93 generated: Vec<GeneratedFile>,
95 },
96 Failed {
98 error: String,
100 },
101}
102
103#[derive(TryFromMultipart, Validate, ToSchema)]
104pub struct UploadFileRequest {
105 #[garde(length(min = 1, max = 255))]
107 #[schema(min_length = 1, max_length = 255)]
108 pub name: String,
109
110 #[garde(skip)]
112 #[schema(value_type = Uuid)]
113 pub folder_id: FolderId,
114
115 #[garde(skip)]
118 #[form_data(limit = "unlimited")]
119 #[schema(format = Binary,value_type= Vec<u8>)]
120 pub file: FieldData<Bytes>,
121
122 #[garde(skip)]
125 pub mime: Option<String>,
126
127 #[garde(skip)]
130 pub asynchronous: Option<bool>,
131
132 #[garde(skip)]
136 pub disable_mime_sniffing: Option<bool>,
137
138 #[garde(skip)]
143 #[schema(value_type = Option<Uuid>)]
144 pub fixed_id: Option<FileId>,
145
146 #[garde(skip)]
149 #[schema(value_type = Option<Uuid>)]
150 pub parent_id: Option<FileId>,
151
152 #[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 pub file: FileWithExtra,
168 pub generated: Vec<GeneratedFile>,
170 #[schema(no_recursion)]
172 pub additional_files: Vec<UploadedFile>,
173}
174
175#[derive(Debug, Validate, Deserialize, ToSchema)]
177pub struct UpdateFileRequest {
178 #[garde(inner(length(min = 1, max = 255)))]
180 #[schema(min_length = 1, max_length = 255)]
181 pub name: Option<String>,
182
183 #[garde(skip)]
185 #[schema(value_type = Option<Uuid>)]
186 pub folder_id: Option<FolderId>,
187
188 #[garde(skip)]
190 #[schema(value_type = Option<bool>)]
191 pub pinned: Option<bool>,
192}
193
194#[derive(Debug, Serialize, ToSchema)]
196pub struct FileResponse {
197 pub file: FileWithExtra,
199 pub generated: Vec<GeneratedFile>,
201}
202
203#[derive(Default, Debug, Deserialize)]
204#[serde(default)]
205pub struct RawFileQuery {
206 pub download: bool,
207}
208
209#[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#[derive(Debug, Validate, Deserialize, ToSchema)]
219pub struct GetPresignedRequest {
220 #[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#[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 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}