Skip to main content

fraiseql_server/files/
config.rs

1//! File configuration structures
2
3use std::collections::HashMap;
4
5use serde::Deserialize;
6
7#[derive(Debug, Clone, Deserialize)]
8pub struct FileConfig {
9    /// Upload endpoint path (default: /files/{name})
10    pub path: Option<String>,
11
12    /// Allowed MIME types
13    #[serde(default = "default_allowed_types")]
14    pub allowed_types: Vec<String>,
15
16    /// Maximum file size (e.g., "10MB")
17    #[serde(default = "default_max_size")]
18    pub max_size: String,
19
20    /// Validate magic bytes match declared MIME type
21    #[serde(default = "default_validate_magic")]
22    pub validate_magic_bytes: bool,
23
24    /// Storage backend name (references storage config)
25    #[serde(default = "default_storage")]
26    pub storage: String,
27
28    /// Bucket/container name (env var)
29    pub bucket_env: Option<String>,
30
31    /// Whether files are public
32    #[serde(default = "default_public")]
33    pub public: bool,
34
35    /// Cache duration (for public files)
36    pub cache: Option<String>,
37
38    /// URL expiry for private files
39    pub url_expiry: Option<String>,
40
41    /// Scan for malware
42    #[serde(default)]
43    pub scan_malware: bool,
44
45    /// Image processing configuration
46    pub processing: Option<ProcessingConfig>,
47
48    /// Callback after upload
49    pub on_upload: Option<UploadCallbackConfig>,
50}
51
52fn default_allowed_types() -> Vec<String> {
53    vec![
54        "image/jpeg".to_string(),
55        "image/png".to_string(),
56        "image/webp".to_string(),
57        "image/gif".to_string(),
58        "application/pdf".to_string(),
59    ]
60}
61
62fn default_max_size() -> String {
63    "10MB".to_string()
64}
65fn default_validate_magic() -> bool {
66    true
67}
68fn default_storage() -> String {
69    "default".to_string()
70}
71fn default_public() -> bool {
72    true
73}
74
75impl Default for FileConfig {
76    fn default() -> Self {
77        Self {
78            path:                 None,
79            allowed_types:        default_allowed_types(),
80            max_size:             default_max_size(),
81            validate_magic_bytes: default_validate_magic(),
82            storage:              default_storage(),
83            bucket_env:           None,
84            public:               default_public(),
85            cache:                None,
86            url_expiry:           None,
87            scan_malware:         false,
88            processing:           None,
89            on_upload:            None,
90        }
91    }
92}
93
94#[derive(Debug, Clone, Deserialize)]
95pub struct ProcessingConfig {
96    /// Strip EXIF metadata from images
97    #[serde(default)]
98    pub strip_exif: bool,
99
100    /// Output format (webp, jpeg, png)
101    pub output_format: Option<String>,
102
103    /// Quality (1-100)
104    pub quality: Option<u8>,
105
106    /// Image variants to generate
107    #[serde(default)]
108    pub variants: Vec<VariantConfig>,
109}
110
111#[derive(Debug, Clone, Deserialize)]
112pub struct VariantConfig {
113    pub name:   String,
114    pub width:  u32,
115    pub height: u32,
116
117    /// Resize mode: fit, fill, crop
118    #[serde(default = "default_mode")]
119    pub mode: String,
120}
121
122fn default_mode() -> String {
123    "fit".to_string()
124}
125
126#[derive(Debug, Clone, Deserialize)]
127pub struct UploadCallbackConfig {
128    /// Database function to call
129    pub function: String,
130
131    /// Parameter mapping
132    #[serde(default)]
133    pub mapping: HashMap<String, String>,
134}
135
136#[derive(Debug, Clone, Deserialize)]
137pub struct StorageConfig {
138    /// Backend type: s3, r2, gcs, azure, local
139    pub backend: String,
140
141    /// Region (S3)
142    pub region: Option<String>,
143
144    /// Bucket name (env var)
145    pub bucket_env: Option<String>,
146
147    /// Access key ID (env var)
148    pub access_key_env: Option<String>,
149
150    /// Secret access key (env var)
151    pub secret_key_env: Option<String>,
152
153    /// Endpoint URL (for S3-compatible services)
154    pub endpoint_env: Option<String>,
155
156    /// Account ID (R2)
157    pub account_id_env: Option<String>,
158
159    /// Project ID (GCS)
160    pub project_id_env: Option<String>,
161
162    /// Credentials file/env (GCS)
163    pub credentials_env: Option<String>,
164
165    /// Local filesystem path
166    pub base_path: Option<String>,
167
168    /// Local serve path (for dev)
169    pub serve_path: Option<String>,
170
171    /// Public URL prefix
172    pub public_url: Option<String>,
173}
174
175/// Parse size string like "10MB" to bytes
176pub fn parse_size(size_str: &str) -> Result<usize, String> {
177    let size_str = size_str.trim().to_uppercase();
178
179    let (num_part, unit) =
180        if let Some(pos) = size_str.find(|c: char| !c.is_ascii_digit() && c != '.') {
181            (&size_str[..pos], &size_str[pos..])
182        } else {
183            (size_str.as_str(), "")
184        };
185
186    let num: f64 = num_part.parse().map_err(|_| format!("Invalid number: {}", num_part))?;
187
188    let multiplier = match unit.trim() {
189        "" | "B" => 1,
190        "KB" => 1024,
191        "MB" => 1024 * 1024,
192        "GB" => 1024 * 1024 * 1024,
193        _ => return Err(format!("Unknown unit: {}", unit)),
194    };
195
196    Ok((num * multiplier as f64) as usize)
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_parse_size() {
205        assert_eq!(parse_size("100").unwrap(), 100);
206        assert_eq!(parse_size("100B").unwrap(), 100);
207        assert_eq!(parse_size("10KB").unwrap(), 10 * 1024);
208        assert_eq!(parse_size("10MB").unwrap(), 10 * 1024 * 1024);
209        assert_eq!(parse_size("1GB").unwrap(), 1024 * 1024 * 1024);
210    }
211}