Skip to main content

hypha/config/
files.rs

1use std::path::{Path, PathBuf};
2
3use agent_first_data::document::{
4    CreateOptions, Document, DocumentError, DocumentFile, Format, Value,
5};
6
7use super::HyphaConfig;
8
9const CONFIG_MAX_BYTES: u64 = 1024 * 1024;
10
11const CONFIG_KEYS: &[&str] = &[
12    "cache.path",
13    "cache.cmn_ttl_s",
14    "cache.key_trust_ttl_s",
15    "cache.key_trust_refresh_mode",
16    "cache.key_trust_synapse_witness_mode",
17    "cache.spore_max_download_bytes",
18    "cache.spore_max_extract_bytes",
19    "cache.spore_max_extract_files",
20    "cache.spore_max_extract_file_bytes",
21    "cache.spore_reject_path_components",
22    "cache.clock_skew_tolerance_s",
23    "cache.require_domain_first_key",
24    "defaults.synapse_domain",
25    "defaults.domain",
26    "defaults.taste.synapse_domain",
27    "defaults.taste.domain",
28];
29
30impl HyphaConfig {
31    pub fn load() -> Result<Self, crate::sink::HyphaError> {
32        use crate::sink::HyphaError;
33
34        let path = config_path();
35        match std::fs::symlink_metadata(&path) {
36            Ok(_) => {}
37            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
38                return Ok(Self::default());
39            }
40            Err(error) => {
41                return Err(HyphaError::with_hint(
42                    "config_read_failed",
43                    format!("Failed to inspect {}: {}", path.display(), error),
44                    "fix the file permissions and retry",
45                ));
46            }
47        }
48
49        let doc = DocumentFile::open_capped(&path, Some(Format::Toml), CONFIG_MAX_BYTES)
50            .map_err(|error| config_load_error(&path, &error))?;
51        agent_first_data::document::from_value(doc.value(), "")
52            .map_err(|error| config_load_error(&path, &error))
53    }
54
55    pub fn save(&self) -> Result<(), crate::sink::HyphaError> {
56        use crate::sink::HyphaError;
57
58        let path = config_path();
59        if let Some(parent) = path.parent() {
60            std::fs::create_dir_all(parent).map_err(|e| {
61                HyphaError::new(
62                    "config_save_failed",
63                    format!("Failed to create config directory: {}", e),
64                )
65            })?;
66        }
67
68        let desired_source = toml::to_string_pretty(self).map_err(|e| {
69            HyphaError::new(
70                "config_save_failed",
71                format!("Failed to serialize config: {}", e),
72            )
73        })?;
74        let desired = Document::parse(&desired_source, Format::Toml).map_err(|error| {
75            config_document_error("config_save_failed", &path, "prepare configuration", &error)
76        })?;
77        let default_source = toml::to_string_pretty(&Self::default()).map_err(|error| {
78            HyphaError::new(
79                "config_save_failed",
80                format!("Failed to serialize default config: {error}"),
81            )
82        })?;
83        let defaults = Document::parse(&default_source, Format::Toml).map_err(|error| {
84            config_document_error(
85                "config_save_failed",
86                &path,
87                "prepare default configuration",
88                &error,
89            )
90        })?;
91
92        match std::fs::symlink_metadata(&path) {
93            Ok(_) => save_existing_config(&path, &desired, &defaults),
94            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
95                let options = CreateOptions::new().unix_mode(0o600);
96                DocumentFile::create_atomic(&path, desired, options)
97                    .map(|_| ())
98                    .map_err(|error| {
99                        config_document_error(
100                            "config_save_failed",
101                            &path,
102                            "create configuration",
103                            &error,
104                        )
105                    })
106            }
107            Err(error) => Err(HyphaError::with_hint(
108                "config_save_failed",
109                format!("Failed to inspect {}: {}", path.display(), error),
110                "fix the file permissions and retry",
111            )),
112        }
113    }
114}
115
116fn save_existing_config(
117    path: &Path,
118    desired: &Document,
119    defaults: &Document,
120) -> Result<(), crate::sink::HyphaError> {
121    let mut current = DocumentFile::open_capped(path, Some(Format::Toml), CONFIG_MAX_BYTES)
122        .map_err(|error| config_load_error(path, &error))?;
123    let mut changed = false;
124
125    for key in CONFIG_KEYS {
126        let desired_value = optional_value(desired, key).map_err(|error| {
127            config_document_error(
128                "config_save_failed",
129                path,
130                "read prepared configuration",
131                &error,
132            )
133        })?;
134        let default_value = optional_value(defaults, key).map_err(|error| {
135            config_document_error(
136                "config_save_failed",
137                path,
138                "read default configuration",
139                &error,
140            )
141        })?;
142        let current_value = optional_value(&current, key).map_err(|error| {
143            config_document_error(
144                "config_save_failed",
145                path,
146                "read existing configuration",
147                &error,
148            )
149        })?;
150
151        match (desired_value, current_value) {
152            (Some(desired_value), Some(current_value)) if desired_value == current_value => {}
153            (Some(desired_value), None) if Some(&desired_value) == default_value.as_ref() => {}
154            (Some(desired_value), _) => {
155                current.set(key, desired_value).map_err(|error| {
156                    config_document_error(
157                        "config_save_failed",
158                        path,
159                        "stage configuration update",
160                        &error,
161                    )
162                })?;
163                changed = true;
164            }
165            (None, Some(_)) => {
166                current.unset(key).map_err(|error| {
167                    config_document_error(
168                        "config_save_failed",
169                        path,
170                        "stage configuration removal",
171                        &error,
172                    )
173                })?;
174                changed = true;
175            }
176            (None, None) => {}
177        }
178    }
179
180    if !changed {
181        return Ok(());
182    }
183
184    current.decode::<HyphaConfig>().map_err(|error| {
185        config_document_error(
186            "config_save_failed",
187            path,
188            "validate updated configuration",
189            &error,
190        )
191    })?;
192    current.save().map_err(|error| {
193        config_document_error("config_save_failed", path, "commit configuration", &error)
194    })
195}
196
197fn optional_value(doc: &Document, key: &str) -> Result<Option<Value>, DocumentError> {
198    match doc.value_at(key) {
199        Ok(value) => Ok(Some(value)),
200        Err(error) if error.code() == "document_path_not_found" => Ok(None),
201        Err(error) => Err(error),
202    }
203}
204
205fn config_load_error(path: &Path, error: &DocumentError) -> crate::sink::HyphaError {
206    let code = match error.code() {
207        "document_parse_failed" | "document_type_mismatch" => "config_parse_failed",
208        _ => "config_read_failed",
209    };
210    config_document_error(code, path, "load configuration", error)
211}
212
213fn config_document_error(
214    code: &str,
215    path: &Path,
216    operation: &str,
217    error: &DocumentError,
218) -> crate::sink::HyphaError {
219    crate::sink::HyphaError::with_hint(
220        code,
221        format!(
222            "Failed to {} at {}: {} ({})",
223            operation,
224            path.display(),
225            error.redacted_message(),
226            error.code()
227        ),
228        "fix the configuration file or its permissions and retry",
229    )
230}
231
232pub(super) fn write_text_file_atomic(
233    path: &Path,
234    content: &str,
235    mode: u32,
236    error_code: &str,
237    file_label: &str,
238) -> Result<(), crate::sink::HyphaError> {
239    use crate::sink::HyphaError;
240
241    let parent = path.parent().ok_or_else(|| {
242        HyphaError::new(
243            error_code,
244            format!("Failed to determine parent directory for {}", file_label),
245        )
246    })?;
247    crate::cache::fs::atomic_write_file(path, content.as_bytes(), parent, Some(mode))
248        .map_err(|e| HyphaError::new(error_code, format!("Failed to write {}: {}", file_label, e)))
249}
250
251pub fn config_path() -> PathBuf {
252    hypha_dir().join("config.toml")
253}
254
255/// $CMN_HOME/hypha/
256pub fn hypha_dir() -> PathBuf {
257    crate::site::get_cmn_home().join("hypha")
258}