Skip to main content

opencode_cloud_core/host/
storage.rs

1//! Host configuration storage
2//!
3//! Load and save hosts.json file.
4
5use std::fs::{self, File};
6use std::io::{Read, Write};
7
8use super::error::HostError;
9use super::schema::HostsFile;
10use crate::config::paths::get_hosts_path;
11
12/// Load hosts configuration from hosts.json
13///
14/// Returns empty HostsFile if file doesn't exist.
15pub fn load_hosts() -> Result<HostsFile, HostError> {
16    let hosts_path = get_hosts_path()
17        .ok_or_else(|| HostError::LoadFailed("Could not determine hosts file path".to_string()))?;
18
19    if !hosts_path.exists() {
20        tracing::debug!(
21            "Hosts file not found, returning empty: {}",
22            hosts_path.display()
23        );
24        return Ok(HostsFile::new());
25    }
26
27    let mut file = File::open(&hosts_path).map_err(|e| {
28        HostError::LoadFailed(format!("Failed to open {}: {}", hosts_path.display(), e))
29    })?;
30
31    let mut contents = String::new();
32    file.read_to_string(&mut contents).map_err(|e| {
33        HostError::LoadFailed(format!("Failed to read {}: {}", hosts_path.display(), e))
34    })?;
35
36    let hosts: HostsFile = serde_json::from_str(&contents).map_err(|e| {
37        HostError::LoadFailed(format!("Invalid JSON in {}: {}", hosts_path.display(), e))
38    })?;
39
40    tracing::debug!(
41        "Loaded {} hosts from {}",
42        hosts.hosts.len(),
43        hosts_path.display()
44    );
45    Ok(hosts)
46}
47
48/// Save hosts configuration to hosts.json
49///
50/// Creates the config directory if it doesn't exist.
51/// Creates a backup (.bak) if file already exists.
52pub fn save_hosts(hosts: &HostsFile) -> Result<(), HostError> {
53    let hosts_path = get_hosts_path()
54        .ok_or_else(|| HostError::SaveFailed("Could not determine hosts file path".to_string()))?;
55
56    // Ensure config directory exists
57    if let Some(parent) = hosts_path.parent() {
58        if !parent.exists() {
59            fs::create_dir_all(parent)
60                .map_err(|e| HostError::SaveFailed(format!("Failed to create directory: {e}")))?;
61        }
62    }
63
64    // Create backup if file exists
65    if hosts_path.exists() {
66        let backup_path = hosts_path.with_extension("json.bak");
67        fs::copy(&hosts_path, &backup_path)
68            .map_err(|e| HostError::SaveFailed(format!("Failed to create backup: {e}")))?;
69        tracing::debug!("Created hosts backup: {}", backup_path.display());
70    }
71
72    // Serialize with pretty formatting
73    let json = serde_json::to_string_pretty(hosts)
74        .map_err(|e| HostError::SaveFailed(format!("Failed to serialize: {e}")))?;
75
76    // Write to file
77    let mut file = File::create(&hosts_path).map_err(|e| {
78        HostError::SaveFailed(format!("Failed to create {}: {}", hosts_path.display(), e))
79    })?;
80
81    file.write_all(json.as_bytes()).map_err(|e| {
82        HostError::SaveFailed(format!("Failed to write {}: {}", hosts_path.display(), e))
83    })?;
84
85    tracing::debug!(
86        "Saved {} hosts to {}",
87        hosts.hosts.len(),
88        hosts_path.display()
89    );
90    Ok(())
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::host::schema::HostConfig;
97
98    #[test]
99    fn test_load_nonexistent_returns_empty() {
100        // This test relies on hosts.json not existing in a fresh environment
101        // In CI/testing, we'd mock the path, but for basic test:
102        let result = load_hosts();
103        // Should succeed with empty or existing hosts
104        assert!(result.is_ok());
105    }
106
107    #[test]
108    fn test_serialize_format() {
109        let mut hosts = HostsFile::new();
110        hosts.add_host("test", HostConfig::new("test.example.com"));
111
112        let json = serde_json::to_string_pretty(&hosts).unwrap();
113
114        // Verify it's valid JSON that can be read back
115        let parsed: HostsFile = serde_json::from_str(&json).unwrap();
116        assert!(parsed.has_host("test"));
117    }
118}