systemprompt_files/config/
mod.rs1mod 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}