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