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 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}