Skip to main content

docbox_http/models/
document_box.rs

1use std::{fmt::Display, str::FromStr};
2
3use crate::error::HttpError;
4use axum::http::StatusCode;
5use docbox_core::database::models::{
6    document_box::DocumentBox,
7    folder::{FolderWithExtra, ResolvedFolderWithExtra},
8};
9use garde::Validate;
10use serde::{Deserialize, Deserializer, Serialize};
11use thiserror::Error;
12use utoipa::ToSchema;
13
14/// Valid document box scope string, must be: A-Z, a-z, 0-9, ':', '-', '_', '.'
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, ToSchema, Serialize)]
16#[serde(transparent)]
17#[schema(examples( "user:1:files"), value_type = String)]
18pub struct DocumentBoxScope(pub String);
19
20impl Display for DocumentBoxScope {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        self.0.fmt(f)
23    }
24}
25
26const ALLOWED_CHARS: [char; 4] = [':', '-', '_', '.'];
27
28impl DocumentBoxScope {
29    pub fn validate_scope(value: &str) -> bool {
30        if value.trim().is_empty() {
31            return false;
32        }
33
34        value
35            .chars()
36            .all(|char| char.is_ascii_alphanumeric() || ALLOWED_CHARS.contains(&char))
37    }
38}
39
40impl<'de> Deserialize<'de> for DocumentBoxScope {
41    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
42    where
43        D: Deserializer<'de>,
44    {
45        let value = String::deserialize(deserializer)?;
46
47        if !DocumentBoxScope::validate_scope(&value) {
48            return Err(serde::de::Error::custom(InvalidDocumentBoxScope));
49        }
50
51        Ok(Self(value))
52    }
53}
54
55impl Validate for DocumentBoxScope {
56    type Context = ();
57
58    fn validate_into(
59        &self,
60        _ctx: &Self::Context,
61        parent: &mut dyn FnMut() -> garde::Path,
62        report: &mut garde::Report,
63    ) {
64        if !DocumentBoxScope::validate_scope(&self.0) {
65            report.append(parent(), garde::Error::new("document box scope is invalid"))
66        }
67    }
68}
69
70#[derive(Debug, Error)]
71#[error("invalid document box scope")]
72pub struct InvalidDocumentBoxScope;
73
74impl FromStr for DocumentBoxScope {
75    type Err = InvalidDocumentBoxScope;
76
77    fn from_str(s: &str) -> Result<Self, Self::Err> {
78        if !DocumentBoxScope::validate_scope(s) {
79            return Err(InvalidDocumentBoxScope);
80        }
81
82        Ok(Self(s.to_string()))
83    }
84}
85
86/// Request to create a document box
87#[derive(Debug, Validate, Deserialize, ToSchema)]
88pub struct CreateDocumentBoxRequest {
89    /// Scope for the document box to use
90    #[garde(length(min = 1))]
91    #[schema(min_length = 1)]
92    pub scope: String,
93}
94
95/// Response to an options request
96#[derive(Debug, Serialize, ToSchema)]
97pub struct DocumentBoxOptions {
98    /// Max allowed upload file size in bytes
99    pub max_file_size: i32,
100}
101
102/// Response for requesting a document box
103#[derive(Debug, Serialize, ToSchema)]
104pub struct DocumentBoxResponse {
105    /// The created document box
106    pub document_box: DocumentBox,
107    /// Root folder of the document box
108    pub root: FolderWithExtra,
109    /// Resolved contents of the root folder
110    pub children: ResolvedFolderWithExtra,
111}
112
113#[derive(Debug, Serialize, ToSchema)]
114pub struct DocumentBoxStats {
115    /// Total number of files within the document box
116    pub total_files: i64,
117    /// Total number of links within the document box
118    pub total_links: i64,
119    /// Total number of folders within the document box
120    pub total_folders: i64,
121    /// Total size of the files contained within the document box
122    pub file_size: i64,
123}
124
125#[derive(Debug, Error)]
126pub enum HttpDocumentBoxError {
127    #[error("document box with matching scope already exists")]
128    ScopeAlreadyExists,
129
130    #[error("unknown document box")]
131    UnknownDocumentBox,
132}
133
134impl HttpError for HttpDocumentBoxError {
135    fn status(&self) -> axum::http::StatusCode {
136        match self {
137            HttpDocumentBoxError::ScopeAlreadyExists => StatusCode::CONFLICT,
138            HttpDocumentBoxError::UnknownDocumentBox => StatusCode::NOT_FOUND,
139        }
140    }
141}