1use std::fs;
9use std::path::{Path, PathBuf};
10
11use serde::{Deserialize, Serialize};
12
13use crate::exit::{CtlError, CtlResult};
14
15const DEFAULT_SERVER: &str = "http://127.0.0.1:8080";
16
17#[derive(Debug, Clone, Serialize, Deserialize, Default)]
18pub struct Config {
19 pub server_url: Option<String>,
21 pub api_token: Option<String>,
23}
24
25impl Config {
26 pub fn effective_server(&self) -> String {
28 self.server_url
29 .clone()
30 .or_else(|| std::env::var("CELLCTL_SERVER").ok())
31 .unwrap_or_else(|| DEFAULT_SERVER.to_string())
32 }
33
34 pub fn effective_token(&self) -> Option<String> {
36 self.api_token
37 .clone()
38 .or_else(|| std::env::var("CELLCTL_TOKEN").ok())
39 }
40}
41
42pub fn config_path() -> CtlResult<PathBuf> {
44 if let Ok(explicit) = std::env::var("CELLCTL_CONFIG") {
45 return Ok(PathBuf::from(explicit));
46 }
47 let home =
48 home::home_dir().ok_or_else(|| CtlError::usage("cannot determine home directory"))?;
49 Ok(home.join(".cellctl").join("config"))
50}
51
52pub fn load() -> CtlResult<Config> {
53 let path = config_path()?;
54 if !path.exists() {
55 return Ok(Config::default());
56 }
57 let raw = fs::read_to_string(&path)
58 .map_err(|e| CtlError::usage(format!("read {}: {e}", path.display())))?;
59 let cfg: Config = toml::from_str(&raw)
60 .map_err(|e| CtlError::usage(format!("parse {}: {e}", path.display())))?;
61 Ok(cfg)
62}
63
64pub fn save(cfg: &Config) -> CtlResult<()> {
65 let path = config_path()?;
66 if let Some(parent) = path.parent() {
67 ensure_dir(parent)?;
68 }
69 let raw = toml::to_string_pretty(cfg)
70 .map_err(|e| CtlError::usage(format!("serialize config: {e}")))?;
71 write_owner_only(&path, raw.as_bytes())
72 .map_err(|e| CtlError::usage(format!("write {}: {e}", path.display())))?;
73 Ok(())
74}
75
76fn write_owner_only(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
86 use std::io::Write as _;
87
88 #[cfg(unix)]
89 {
90 use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
91 let mut f = fs::OpenOptions::new()
92 .write(true)
93 .create(true)
94 .truncate(true)
95 .mode(0o600)
96 .open(path)?;
97 f.write_all(bytes)?;
98 f.sync_all()?;
99 fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
102 Ok(())
103 }
104
105 #[cfg(not(unix))]
106 {
107 let mut f = fs::OpenOptions::new()
108 .write(true)
109 .create(true)
110 .truncate(true)
111 .open(path)?;
112 f.write_all(bytes)?;
113 f.sync_all()?;
114 Ok(())
115 }
116}
117
118fn ensure_dir(p: &Path) -> CtlResult<()> {
119 fs::create_dir_all(p).map_err(|e| CtlError::usage(format!("mkdir {}: {e}", p.display())))
120}