Skip to main content

systemprompt_files/config/
validator.rs

1//! Domain validator for files configuration (size bounds, type sanity).
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use systemprompt_config::ProfileBootstrap;
7use systemprompt_models::AppPaths;
8use systemprompt_traits::validation_report::{
9    ValidationError, ValidationReport, ValidationWarning,
10};
11use systemprompt_traits::{ConfigProvider, DomainConfig, DomainConfigError};
12
13use super::FilesConfig;
14use super::types::FilesConfigYaml;
15
16const MAX_RECOMMENDED_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024;
17const MIN_VIDEO_FILE_SIZE: u64 = 100 * 1024 * 1024;
18
19fn is_valid_header_value(value: &str) -> bool {
20    !value.is_empty() && value.chars().all(|c| matches!(c, ' '..='~'))
21}
22
23#[derive(Debug, Default)]
24pub struct FilesConfigValidator {
25    config: Option<FilesConfigYaml>,
26}
27
28impl FilesConfigValidator {
29    pub fn new() -> Self {
30        Self::default()
31    }
32}
33
34impl DomainConfig for FilesConfigValidator {
35    fn domain_id(&self) -> &'static str {
36        "files"
37    }
38
39    fn priority(&self) -> u32 {
40        10
41    }
42
43    fn load(&mut self, _config: &dyn ConfigProvider) -> Result<(), DomainConfigError> {
44        let profile = ProfileBootstrap::get().map_err(|e| DomainConfigError::LoadError {
45            message: e.to_string(),
46        })?;
47        let paths =
48            AppPaths::from_profile(&profile.paths, profile.path_resolution()).map_err(|e| {
49                DomainConfigError::LoadError {
50                    message: e.to_string(),
51                }
52            })?;
53        let yaml_config =
54            FilesConfig::load_yaml_config(&paths).map_err(|e| DomainConfigError::LoadError {
55                message: e.to_string(),
56            })?;
57        self.config = Some(yaml_config);
58        Ok(())
59    }
60
61    fn validate(&self) -> Result<ValidationReport, DomainConfigError> {
62        let mut report = ValidationReport::new("files");
63
64        let config = self
65            .config
66            .as_ref()
67            .ok_or_else(|| DomainConfigError::ValidationError {
68                message: "Not loaded".into(),
69            })?;
70
71        if !config.url_prefix.starts_with('/') {
72            report.add_error(ValidationError::new(
73                "files.urlPrefix",
74                "URL prefix must start with '/'",
75            ));
76        }
77
78        if let Some(cache_control) = config.cache_control.as_deref()
79            && !is_valid_header_value(cache_control)
80        {
81            report.add_error(ValidationError::new(
82                "files.cacheControl",
83                "Cache-Control must be a non-empty string of printable ASCII characters",
84            ));
85        }
86
87        if config.upload.max_file_size_bytes > MAX_RECOMMENDED_FILE_SIZE {
88            report.add_warning(
89                ValidationWarning::new(
90                    "files.upload.maxFileSizeBytes",
91                    "Max file size > 2GB may cause memory issues",
92                )
93                .with_suggestion("Consider using a smaller max file size for better performance"),
94            );
95        }
96
97        if config.upload.allowed_types.video
98            && config.upload.max_file_size_bytes < MIN_VIDEO_FILE_SIZE
99        {
100            report.add_warning(
101                ValidationWarning::new(
102                    "files.upload.allowedTypes.video",
103                    "Video uploads enabled but max file size < 100MB",
104                )
105                .with_suggestion("Increase maxFileSizeBytes to at least 100MB for video uploads"),
106            );
107        }
108
109        Ok(report)
110    }
111}