ssh_utils_lib/config/
app_config.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4use std::{fs, path::{Path, PathBuf}};
5
6use crate::helper::{get_file_path, CONFIG_FILE};
7
8#[derive(Serialize, Deserialize, Debug, Clone)]
9pub struct Server {
10    pub id: String,
11    pub name: String,
12    pub ip: String,
13    pub user: String,
14    pub shell: String,
15    pub port: u16,
16}
17
18impl Server {
19    pub fn new(name: String, ip: String, user: String, shell: String, port: u16) -> Self {
20        Self {
21            id: Uuid::new_v4().to_string(),
22            name,
23            ip,
24            user,
25            shell,
26            port,
27        }
28    }
29}
30
31#[derive(Serialize, Deserialize, Debug, Default)]
32pub struct Config {
33    pub servers: Vec<Server>,
34}
35
36impl Config {
37    /**
38        Save the current config to the specified file.
39    */
40    pub fn save(&self) -> Result<()> {
41        let file_path = get_file_path(CONFIG_FILE)?;
42        let config_str = toml::to_string(self).context("Failed to serialize config to TOML.")?;
43        
44        // Ensure the directory exists
45        let path = PathBuf::from(&file_path);
46        if let Some(parent) = path.parent() {
47            if !parent.exists() {
48                fs::create_dir_all(parent).context(format!(
49                    "Failed to create config directory at {:?}",
50                    parent
51                ))?;
52            }
53        }
54
55        // Write the config to the file
56        fs::write(&file_path, config_str)
57            .context(format!("Failed to write config to file at {:?}", file_path))?;
58
59        Ok(())
60    }
61
62    /**
63        Modify a server's information and save the config.
64    */
65    pub fn modify_server(&mut self, id: &str, new_server: Server) -> Result<()> {
66        if let Some(server) = self.servers.iter_mut().find(|server| server.id == id) {
67            server.name = new_server.name.clone();
68            server.ip = new_server.ip.clone();
69            server.user = new_server.user.clone();
70            server.shell = new_server.shell.clone();
71            server.port = new_server.port;
72            self.save()?;
73        } else {
74            return Err(anyhow::anyhow!("Server with id {} not found", id));
75        }
76        Ok(())
77    }
78
79    /**
80        Add a new server and save the config.
81    */
82    pub fn add_server(&mut self, new_server: Server) -> Result<()> {
83        self.servers.push(new_server);
84        self.save()?;
85        Ok(())
86    }
87
88    /**
89        Delete a server by id and save the config.
90    */
91    pub fn delete_server(&mut self, id: &str) -> Result<()> {
92        if let Some(pos) = self.servers.iter().position(|server| server.id == id) {
93            self.servers.remove(pos);
94            self.save()?;
95        } else {
96            return Err(anyhow::anyhow!("Server with id {} not found", id));
97        }
98        Ok(())
99    }
100}
101
102/**
103    check if config file and it's directory exists
104    if not exists, create them
105*/
106pub fn ensure_config_exists() -> Result<()> {
107    let mut config_dir: PathBuf = if cfg!(debug_assertions) {
108        ".".into() // current running dir
109    } else {
110        dirs::home_dir().context("Unable to reach user's home directory.")?
111    };
112    config_dir.push(".config/ssh-utils");
113
114    // Ensure the config directory exists
115    if !config_dir.exists() {
116        fs::create_dir_all(&config_dir).context(format!(
117            "Failed to create config directory at {:?}",
118            config_dir
119        ))?;
120    }
121
122    // Ensure the config file exists
123    let config_file_path = config_dir.join("config.toml");
124    if !config_file_path.exists() {
125        fs::File::create(&config_file_path).context(format!(
126            "Failed to create config file at {:?}",
127            config_file_path
128        ))?;
129    }
130
131    Ok(())
132}
133
134/**
135    read toml format config
136    from "~/.config/ssh-utils/config.toml"
137*/
138pub fn read_config() -> Result<Config> {
139    let config_path = get_config_path()?;
140    read_config_from_path(&config_path)
141}
142
143fn get_config_path() -> Result<PathBuf> {
144    if cfg!(debug_assertions) {
145        Ok(".config/ssh-utils/config.toml".into())
146    } else {
147        let mut path = dirs::home_dir().context("Unable to reach user's home directory.")?;
148        path.push(".config/ssh-utils/config.toml");
149        Ok(path)
150    }
151}
152
153fn read_config_from_path<P: AsRef<Path>>(config_path: P) -> Result<Config> {
154    let config_str = fs::read_to_string(&config_path)
155        .with_context(|| format!("Unable to read ssh-utils' config file at {:?}", config_path.as_ref()))?;
156
157    if config_str.trim().is_empty() {
158        return Ok(Config::default());
159    }
160
161    let config: Config = toml::from_str(&config_str)
162        .context("Failed to parse ssh-utils' config file.")?;
163
164    Ok(config)
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use tempfile::TempDir;
171    use std::fs;
172
173    #[test]
174    fn test_read_config_empty_file() {
175        let temp_dir = TempDir::new().unwrap();
176        let config_path = temp_dir.path().join("config.toml");
177        fs::write(&config_path, "").unwrap();
178        let config = read_config_from_path(&config_path).unwrap();
179        assert!(config.servers.is_empty());
180    }
181
182    #[test]
183    fn test_read_config_with_servers() {
184        let temp_dir = TempDir::new().unwrap();
185        let config_path = temp_dir.path().join("config.toml");
186        let config_content = r#"
187            [[servers]]
188            id = "1"
189            name = "Server1"
190            ip = "192.168.1.1"
191            user = "user1"
192            shell = "/bin/bash"
193            port = 22
194
195            [[servers]]
196            id = "2"
197            name = "Server2"
198            ip = "192.168.1.2"
199            user = "user2"
200            shell = "/bin/zsh"
201            port = 2222
202        "#;
203        fs::write(&config_path, config_content).unwrap();
204
205        let config = read_config_from_path(&config_path).unwrap();
206        assert_eq!(config.servers.len(), 2);
207        assert_eq!(config.servers[0].name, "Server1");
208        assert_eq!(config.servers[1].port, 2222);
209    }
210}