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 ensure_storage_structure(&self) -> Vec<String> {
98        let mut errors = Vec::new();
99
100        if !self.storage_root.exists() {
101            if let Err(e) = std::fs::create_dir_all(&self.storage_root) {
102                errors.push(format!(
103                    "Failed to create storage root {}: {}",
104                    self.storage_root.display(),
105                    e
106                ));
107                return errors;
108            }
109        }
110
111        for dir in [self.files(), self.images()] {
112            if !dir.exists() {
113                if let Err(e) = std::fs::create_dir_all(&dir) {
114                    errors.push(format!("Failed to create {}: {}", dir.display(), e));
115                }
116            }
117        }
118
119        errors
120    }
121
122    pub fn storage(&self) -> &Path {
123        &self.storage_root
124    }
125
126    pub fn generated_images(&self) -> PathBuf {
127        self.storage_root.join(storage::GENERATED)
128    }
129
130    pub fn content_images(&self, source: &str) -> PathBuf {
131        self.storage_root.join(storage::IMAGES).join(source)
132    }
133
134    pub fn images(&self) -> PathBuf {
135        self.storage_root.join(storage::IMAGES)
136    }
137
138    pub fn files(&self) -> PathBuf {
139        self.storage_root.join(storage::FILES)
140    }
141
142    pub fn audio(&self) -> PathBuf {
143        self.storage_root.join(storage::AUDIO)
144    }
145
146    pub fn video(&self) -> PathBuf {
147        self.storage_root.join(storage::VIDEO)
148    }
149
150    pub fn documents(&self) -> PathBuf {
151        self.storage_root.join(storage::DOCUMENTS)
152    }
153
154    pub fn uploads(&self) -> PathBuf {
155        self.storage_root.join(storage::UPLOADS)
156    }
157
158    pub fn url_prefix(&self) -> &str {
159        &self.url_prefix
160    }
161
162    pub fn public_url(&self, relative_path: &str) -> String {
163        let path = relative_path.trim_start_matches('/');
164        format!("{}/{}", self.url_prefix, path)
165    }
166
167    pub fn image_url(&self, relative_to_images: &str) -> String {
168        let path = relative_to_images.trim_start_matches('/');
169        format!("{}/images/{}", self.url_prefix, path)
170    }
171
172    pub fn generated_image_url(&self, filename: &str) -> String {
173        let name = filename.trim_start_matches('/');
174        format!("{}/images/generated/{}", self.url_prefix, name)
175    }
176
177    pub fn content_image_url(&self, source: &str, filename: &str) -> String {
178        let name = filename.trim_start_matches('/');
179        format!("{}/images/{}/{}", self.url_prefix, source, name)
180    }
181
182    pub fn file_url(&self, relative_to_files: &str) -> String {
183        let path = relative_to_files.trim_start_matches('/');
184        format!("{}/files/{}", self.url_prefix, path)
185    }
186
187    pub fn audio_url(&self, filename: &str) -> String {
188        let name = filename.trim_start_matches('/');
189        format!("{}/files/audio/{}", self.url_prefix, name)
190    }
191
192    pub fn video_url(&self, filename: &str) -> String {
193        let name = filename.trim_start_matches('/');
194        format!("{}/files/video/{}", self.url_prefix, name)
195    }
196
197    pub fn document_url(&self, filename: &str) -> String {
198        let name = filename.trim_start_matches('/');
199        format!("{}/files/documents/{}", self.url_prefix, name)
200    }
201
202    pub fn upload_url(&self, filename: &str) -> String {
203        let name = filename.trim_start_matches('/');
204        format!("{}/files/uploads/{}", self.url_prefix, name)
205    }
206}