Skip to main content

systemprompt_files/config/
mod.rs

1mod types;
2mod validator;
3
4pub use types::{AllowedFileTypes, FilePersistenceMode, FileUploadConfig, FilesConfigYaml};
5pub use validator::FilesConfigValidator;
6
7use anyhow::{anyhow, Context, Result};
8use std::path::{Path, PathBuf};
9use std::sync::OnceLock;
10use systemprompt_cloud::constants::storage;
11use systemprompt_models::profile_bootstrap::ProfileBootstrap;
12use systemprompt_models::AppPaths;
13
14use types::FilesConfigWrapper;
15
16static FILES_CONFIG: OnceLock<FilesConfig> = OnceLock::new();
17
18#[derive(Debug, Clone)]
19pub struct FilesConfig {
20    storage_root: PathBuf,
21    url_prefix: String,
22    upload: FileUploadConfig,
23}
24
25impl FilesConfig {
26    pub fn init() -> Result<()> {
27        if FILES_CONFIG.get().is_some() {
28            return Ok(());
29        }
30        let config = Self::from_profile()?;
31        config.validate()?;
32        let _ = FILES_CONFIG.set(config);
33        Ok(())
34    }
35
36    pub fn get() -> Result<&'static Self> {
37        FILES_CONFIG
38            .get()
39            .ok_or_else(|| anyhow!("FilesConfig::init() not called"))
40    }
41
42    pub fn get_optional() -> Option<&'static Self> {
43        FILES_CONFIG.get()
44    }
45
46    pub fn from_profile() -> Result<Self> {
47        let profile =
48            ProfileBootstrap::get().map_err(|e| anyhow!("Profile not initialized: {}", e))?;
49
50        let storage_root = profile
51            .paths
52            .storage
53            .as_ref()
54            .ok_or_else(|| anyhow!("paths.storage not configured in profile"))?
55            .clone();
56
57        let yaml_config = Self::load_yaml_config()?;
58
59        Ok(Self {
60            storage_root: PathBuf::from(storage_root),
61            url_prefix: yaml_config.url_prefix,
62            upload: yaml_config.upload,
63        })
64    }
65
66    pub(crate) fn load_yaml_config() -> Result<FilesConfigYaml> {
67        let paths = AppPaths::get().map_err(|e| anyhow!("{}", e))?;
68        let config_path = paths.system().services().join("config/files.yaml");
69
70        if !config_path.exists() {
71            return Ok(FilesConfigYaml::default());
72        }
73
74        let content = std::fs::read_to_string(&config_path)
75            .with_context(|| format!("Failed to read files.yaml: {}", config_path.display()))?;
76
77        let wrapper: FilesConfigWrapper = serde_yaml::from_str(&content)
78            .with_context(|| format!("Failed to parse files.yaml: {}", config_path.display()))?;
79
80        Ok(wrapper.files)
81    }
82
83    pub const fn upload(&self) -> &FileUploadConfig {
84        &self.upload
85    }
86
87    pub fn validate(&self) -> Result<()> {
88        if !self.storage_root.is_absolute() {
89            return Err(anyhow!(
90                "paths.storage must be absolute, got: {}",
91                self.storage_root.display()
92            ));
93        }
94        Ok(())
95    }
96
97    pub fn validate_storage_structure(&self) -> Vec<String> {
98        let mut errors = Vec::new();
99
100        if !self.storage_root.exists() {
101            errors.push(format!(
102                "Storage root not found: {}",
103                self.storage_root.display()
104            ));
105            return errors;
106        }
107
108        let images_dir = self.images();
109        if !images_dir.exists() {
110            errors.push(format!(
111                "Images directory not found: {}",
112                images_dir.display()
113            ));
114        }
115
116        let files_dir = self.files();
117        if !files_dir.exists() {
118            errors.push(format!(
119                "Files directory not found: {}",
120                files_dir.display()
121            ));
122        }
123
124        errors
125    }
126
127    pub fn storage(&self) -> &Path {
128        &self.storage_root
129    }
130
131    pub fn generated_images(&self) -> PathBuf {
132        self.storage_root.join(storage::GENERATED)
133    }
134
135    pub fn content_images(&self, source: &str) -> PathBuf {
136        self.storage_root.join(storage::IMAGES).join(source)
137    }
138
139    pub fn images(&self) -> PathBuf {
140        self.storage_root.join(storage::IMAGES)
141    }
142
143    pub fn files(&self) -> PathBuf {
144        self.storage_root.join(storage::FILES)
145    }
146
147    pub fn audio(&self) -> PathBuf {
148        self.storage_root.join(storage::AUDIO)
149    }
150
151    pub fn video(&self) -> PathBuf {
152        self.storage_root.join(storage::VIDEO)
153    }
154
155    pub fn documents(&self) -> PathBuf {
156        self.storage_root.join(storage::DOCUMENTS)
157    }
158
159    pub fn uploads(&self) -> PathBuf {
160        self.storage_root.join(storage::UPLOADS)
161    }
162
163    pub fn url_prefix(&self) -> &str {
164        &self.url_prefix
165    }
166
167    pub fn public_url(&self, relative_path: &str) -> String {
168        let path = relative_path.trim_start_matches('/');
169        format!("{}/{}", self.url_prefix, path)
170    }
171
172    pub fn image_url(&self, relative_to_images: &str) -> String {
173        let path = relative_to_images.trim_start_matches('/');
174        format!("{}/images/{}", self.url_prefix, path)
175    }
176
177    pub fn generated_image_url(&self, filename: &str) -> String {
178        let name = filename.trim_start_matches('/');
179        format!("{}/images/generated/{}", self.url_prefix, name)
180    }
181
182    pub fn content_image_url(&self, source: &str, filename: &str) -> String {
183        let name = filename.trim_start_matches('/');
184        format!("{}/images/{}/{}", self.url_prefix, source, name)
185    }
186
187    pub fn file_url(&self, relative_to_files: &str) -> String {
188        let path = relative_to_files.trim_start_matches('/');
189        format!("{}/files/{}", self.url_prefix, path)
190    }
191
192    pub fn audio_url(&self, filename: &str) -> String {
193        let name = filename.trim_start_matches('/');
194        format!("{}/files/audio/{}", self.url_prefix, name)
195    }
196
197    pub fn video_url(&self, filename: &str) -> String {
198        let name = filename.trim_start_matches('/');
199        format!("{}/files/video/{}", self.url_prefix, name)
200    }
201
202    pub fn document_url(&self, filename: &str) -> String {
203        let name = filename.trim_start_matches('/');
204        format!("{}/files/documents/{}", self.url_prefix, name)
205    }
206
207    pub fn upload_url(&self, filename: &str) -> String {
208        let name = filename.trim_start_matches('/');
209        format!("{}/files/uploads/{}", self.url_prefix, name)
210    }
211}