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