use crate::error::HttpError;
use axum::http::StatusCode;
use axum_typed_multipart::{FieldData, TryFromMultipart};
use bytes::Bytes;
use chrono::{DateTime, Utc};
use docbox_core::processing::{ProcessingConfig, ProcessingError};
use docbox_core::{
database::models::{
file::{FileId, FileWithExtra},
folder::FolderId,
generated_file::GeneratedFile,
presigned_upload_task::PresignedUploadTaskId,
tasks::TaskId,
},
files::upload_file::UploadFileError,
};
use garde::Validate;
use mime::Mime;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use std::{collections::HashMap, marker::PhantomData};
use thiserror::Error;
use utoipa::ToSchema;
#[serde_as]
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct CreatePresignedRequest {
#[garde(length(min = 1, max = 255))]
#[schema(min_length = 1, max_length = 255)]
pub name: String,
#[garde(skip)]
#[schema(value_type = Uuid)]
pub folder_id: FolderId,
#[garde(range(min = 1))]
#[schema(minimum = 1)]
pub size: i32,
#[garde(skip)]
#[serde_as(as = "Option<serde_with::DisplayFromStr>")]
#[schema(value_type = Option<String>)]
pub mime: Option<Mime>,
#[garde(skip)]
#[schema(value_type = Option<Uuid>)]
pub parent_id: Option<FileId>,
#[garde(skip)]
pub processing_config: Option<ProcessingConfig>,
#[garde(skip)]
pub disable_mime_sniffing: Option<bool>,
}
#[derive(Serialize, ToSchema)]
pub struct PresignedUploadResponse {
#[schema(value_type = Uuid)]
pub task_id: PresignedUploadTaskId,
pub method: String,
pub uri: String,
pub headers: HashMap<String, String>,
}
#[derive(Serialize, ToSchema)]
#[serde(tag = "status")]
#[allow(clippy::large_enum_variant)]
pub enum PresignedStatusResponse {
Pending,
Complete {
file: FileWithExtra,
generated: Vec<GeneratedFile>,
},
Failed {
error: String,
},
}
#[derive(TryFromMultipart, Validate, ToSchema)]
pub struct UploadFileRequest {
#[garde(length(min = 1, max = 255))]
#[schema(min_length = 1, max_length = 255)]
pub name: String,
#[garde(skip)]
#[schema(value_type = Uuid)]
pub folder_id: FolderId,
#[garde(skip)]
#[form_data(limit = "unlimited")]
#[schema(format = Binary,value_type= Vec<u8>)]
pub file: FieldData<Bytes>,
#[garde(skip)]
pub mime: Option<String>,
#[garde(skip)]
pub asynchronous: Option<bool>,
#[garde(skip)]
pub disable_mime_sniffing: Option<bool>,
#[garde(skip)]
#[schema(value_type = Option<Uuid>)]
pub fixed_id: Option<FileId>,
#[garde(skip)]
#[schema(value_type = Option<Uuid>)]
pub parent_id: Option<FileId>,
#[garde(skip)]
pub processing_config: Option<String>,
}
#[derive(Debug, Serialize, ToSchema)]
#[serde(untagged)]
pub enum FileUploadResponse {
Sync(Box<UploadedFile>),
Async(UploadTaskResponse),
}
#[derive(Debug, Serialize, ToSchema)]
pub struct UploadedFile {
pub file: FileWithExtra,
pub generated: Vec<GeneratedFile>,
#[schema(no_recursion)]
pub additional_files: Vec<UploadedFile>,
}
#[derive(Debug, Validate, Deserialize, ToSchema)]
pub struct UpdateFileRequest {
#[garde(inner(length(min = 1, max = 255)))]
#[schema(min_length = 1, max_length = 255)]
pub name: Option<String>,
#[garde(skip)]
#[schema(value_type = Option<Uuid>)]
pub folder_id: Option<FolderId>,
#[garde(skip)]
#[schema(value_type = Option<bool>)]
pub pinned: Option<bool>,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct FileResponse {
pub file: FileWithExtra,
pub generated: Vec<GeneratedFile>,
}
#[derive(Default, Debug, Deserialize)]
#[serde(default)]
pub struct RawFileQuery {
pub download: bool,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct UploadTaskResponse {
#[schema(value_type = Uuid)]
pub task_id: TaskId,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Validate, Deserialize, ToSchema)]
pub struct GetPresignedRequest {
#[garde(skip)]
#[schema(default = 900)]
pub expires_at: Option<i64>,
}
#[derive(Serialize, ToSchema)]
pub struct PresignedDownloadResponse {
pub method: String,
pub uri: String,
pub headers: HashMap<String, String>,
pub expires_at: DateTime<Utc>,
}
#[derive(ToSchema)]
#[schema(value_type = String, format = Binary)]
pub struct BinaryResponse(PhantomData<Vec<u8>>);
#[derive(Debug, Error)]
pub enum HttpFileError {
#[error("unknown file")]
UnknownFile,
#[error("unknown task")]
UnknownTask,
#[error("file size is larger than the maximum allowed size (requested: {0}, maximum: {1})")]
FileTooLarge(i32, i32),
#[error("fixed file id already in use")]
FileIdInUse,
#[error("request file mime content type is invalid")]
InvalidMimeType,
#[error("no matching generated file")]
NoMatchingGenerated,
#[allow(unused)]
#[error("unsupported file type")]
UnsupportedFileType,
#[error(transparent)]
UploadFileError(UploadFileError),
}
impl HttpError for HttpFileError {
fn status(&self) -> axum::http::StatusCode {
match self {
HttpFileError::FileTooLarge(_, _) => StatusCode::BAD_REQUEST,
HttpFileError::FileIdInUse => StatusCode::CONFLICT,
HttpFileError::UnknownFile
| HttpFileError::NoMatchingGenerated
| HttpFileError::UnknownTask => StatusCode::NOT_FOUND,
HttpFileError::UnsupportedFileType | HttpFileError::InvalidMimeType => {
StatusCode::BAD_REQUEST
}
HttpFileError::UploadFileError(error) => match error {
UploadFileError::Processing(
ProcessingError::MalformedFile(_)
| ProcessingError::ReadPdfInfo(_)
| ProcessingError::ExtractFileText(_)
| ProcessingError::DecodeImage(_)
| ProcessingError::GenerateThumbnail(_)
| ProcessingError::Email(_),
) => StatusCode::UNPROCESSABLE_ENTITY,
_ => StatusCode::INTERNAL_SERVER_ERROR,
},
}
}
}