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