Skip to main content

pwr_core/
config.rs

1use serde::{Deserialize, Serialize};
2use std::fs;
3use std::path::PathBuf;
4
5use crate::error::{PwrError, Result};
6
7/// Client configuration stored at `~/.config/pwr/config.toml`.
8///
9/// Contains the connection details for reaching the pwr-server daemon
10/// and the local directory root where projects are stored.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct PwrConfig {
13    /// Schema version for forward compatibility.
14    pub version: u32,
15
16    /// Hostname or IP address of the NAS running pwr-server.
17    pub server_host: String,
18
19    /// Port the server listens on (default: 9742).
20    #[serde(default = "default_port")]
21    pub server_port: u16,
22
23    /// Hex-encoded 256-bit pre-shared key for authentication.
24    pub server_psk: String,
25
26    /// Whether to use TLS when connecting to the server.
27    /// Should match the server configuration (the server always uses TLS).
28    #[serde(default = "default_use_tls")]
29    pub use_tls: bool,
30
31    /// SHA-256 fingerprint of the server's TLS certificate (hex).
32    /// Used for certificate pinning.
33    #[serde(default)]
34    pub server_fingerprint: Option<String>,
35
36    /// Local root directory where projects live, e.g. "/home/jacob/Projects".
37    pub local_root: String,
38
39    /// Connection timeout in seconds.
40    #[serde(default = "default_timeout")]
41    pub connect_timeout_secs: u64,
42
43    /// Transfer timeout in seconds.
44    #[serde(default = "default_transfer_timeout")]
45    pub transfer_timeout_secs: u64,
46}
47
48fn default_port() -> u16 {
49    9742
50}
51
52fn default_use_tls() -> bool {
53    true
54}
55
56fn default_timeout() -> u64 {
57    10
58}
59
60fn default_transfer_timeout() -> u64 {
61    300
62}
63
64impl PwrConfig {
65    /// Create a new client configuration.
66    pub fn new(
67        server_host: String,
68        server_port: u16,
69        server_psk: String,
70        local_root: String,
71    ) -> Self {
72        Self {
73            version: 2,
74            server_host,
75            server_port,
76            server_psk,
77            use_tls: true,
78            server_fingerprint: None,
79            local_root,
80            connect_timeout_secs: 10,
81            transfer_timeout_secs: 300,
82        }
83    }
84
85    /// Return the server address as "host:port".
86    pub fn server_addr(&self) -> String {
87        format!("{}:{}", self.server_host, self.server_port)
88    }
89}
90
91/// Determine the config directory: `~/.config/pwr/`.
92pub fn config_dir() -> PathBuf {
93    dirs::config_dir()
94        .unwrap_or_else(|| PathBuf::from("~/.config"))
95        .join("pwr")
96}
97
98/// Path to the main client config file.
99pub fn config_path() -> PathBuf {
100    config_dir().join("config.toml")
101}
102
103/// Path to the age identity file (client-side encryption key).
104pub fn identity_path() -> PathBuf {
105    config_dir().join("identity")
106}
107
108/// Path to the transaction log.
109pub fn transaction_log_path() -> PathBuf {
110    config_dir().join("transactions.log")
111}
112
113/// Check whether a config file exists without loading it.
114pub fn config_exists() -> bool {
115    config_path().exists()
116}
117
118/// Load the client config from disk, or return NoConfig if it doesn't exist.
119pub fn load_config() -> Result<PwrConfig> {
120    let path = config_path();
121    if !path.exists() {
122        return Err(PwrError::NoConfig);
123    }
124    let contents = fs::read_to_string(&path)?;
125    let config: PwrConfig =
126        toml::from_str(&contents).map_err(|e| PwrError::TomlParse {
127            path: path.to_string_lossy().to_string(),
128            source: e,
129        })?;
130    Ok(config)
131}
132
133/// Save the client config to disk.
134pub fn save_config(config: &PwrConfig) -> Result<()> {
135    let dir = config_dir();
136    fs::create_dir_all(&dir)?;
137    let contents = toml::to_string_pretty(config)?;
138    fs::write(config_path(), contents)?;
139    log::info!("Config saved to {}", config_path().display());
140    Ok(())
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_server_addr() {
149        let config = PwrConfig::new(
150            "nas.local".into(),
151            9742,
152            "abcdef0123456789".into(),
153            "/home/jacob/Projects".into(),
154        );
155        assert_eq!(config.server_addr(), "nas.local:9742");
156    }
157
158    #[test]
159    fn test_config_path() {
160        let path = config_path();
161        assert!(path.ends_with("pwr/config.toml"));
162    }
163
164    #[test]
165    fn test_default_port() {
166        assert_eq!(default_port(), 9742);
167    }
168}