roboticus_core/
config_utils.rs1use std::path::{Path, PathBuf};
7
8use chrono::Utc;
9
10use crate::RoboticusConfig;
11use crate::config::BackupsConfig;
12
13#[derive(Debug)]
15pub enum ConfigFileError {
16 Io(std::io::Error),
17 TomlDeserialize(toml::de::Error),
18 Validation(String),
19 MissingParent(PathBuf),
20}
21
22impl std::fmt::Display for ConfigFileError {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 match self {
25 Self::Io(e) => write!(f, "I/O error: {e}"),
26 Self::TomlDeserialize(e) => write!(f, "TOML parse error: {e}"),
27 Self::Validation(e) => write!(f, "validation failed: {e}"),
28 Self::MissingParent(p) => {
29 write!(
30 f,
31 "config parent directory is missing for '{}'",
32 p.display()
33 )
34 }
35 }
36 }
37}
38
39impl std::error::Error for ConfigFileError {}
40
41impl From<std::io::Error> for ConfigFileError {
42 fn from(value: std::io::Error) -> Self {
43 Self::Io(value)
44 }
45}
46
47impl From<toml::de::Error> for ConfigFileError {
48 fn from(value: toml::de::Error) -> Self {
49 Self::TomlDeserialize(value)
50 }
51}
52
53pub fn backup_config_file(
58 path: &Path,
59 max_count: usize,
60 max_age_days: u32,
61) -> Result<Option<PathBuf>, ConfigFileError> {
62 if !path.exists() {
63 return Ok(None);
64 }
65 let parent = path
66 .parent()
67 .ok_or_else(|| ConfigFileError::MissingParent(path.to_path_buf()))?;
68 let backup_dir = parent.join("backups");
69 std::fs::create_dir_all(&backup_dir)?;
70 let stamp = Utc::now().format("%Y%m%dT%H%M%S%.3fZ");
71 let file_name = path
72 .file_name()
73 .and_then(|v| v.to_str())
74 .unwrap_or("roboticus.toml");
75 let backup_name = format!("{file_name}.bak.{stamp}");
76 let backup_path = backup_dir.join(backup_name);
77 std::fs::copy(path, &backup_path)?;
78 let prefix = format!("{file_name}.bak.");
79 prune_old_backups(&backup_dir, &prefix, max_count, max_age_days);
80 Ok(Some(backup_path))
81}
82
83fn prune_old_backups(backup_dir: &Path, prefix: &str, max_count: usize, max_age_days: u32) {
88 let mut backups: Vec<PathBuf> = std::fs::read_dir(backup_dir)
89 .into_iter()
90 .flatten()
91 .filter_map(|e| e.ok())
92 .filter(|e| {
93 e.file_name()
94 .to_str()
95 .is_some_and(|name| name.starts_with(prefix))
96 })
97 .map(|e| e.path())
98 .collect();
99
100 backups.sort();
103
104 if max_age_days > 0 {
106 let cutoff = Utc::now() - chrono::Duration::days(i64::from(max_age_days));
107 let cutoff_stamp = cutoff.format("%Y%m%dT%H%M%S%.3fZ").to_string();
108 backups.retain(|p| {
109 let dominated_by_age = p
110 .file_name()
111 .and_then(|f| f.to_str())
112 .and_then(|name| name.strip_prefix(prefix))
113 .is_some_and(|ts| ts < cutoff_stamp.as_str());
114 if dominated_by_age {
115 let _ = std::fs::remove_file(p);
116 false
117 } else {
118 true
119 }
120 });
121 }
122
123 if max_count > 0 && backups.len() > max_count {
125 let to_remove = backups.len() - max_count;
126 for path in backups.into_iter().take(to_remove) {
127 let _ = std::fs::remove_file(&path);
128 }
129 }
130}
131
132pub fn parse_and_validate_toml(content: &str) -> Result<RoboticusConfig, ConfigFileError> {
134 RoboticusConfig::from_str(content).map_err(|e| ConfigFileError::Validation(e.to_string()))
135}
136
137pub fn parse_and_validate_file(path: &Path) -> Result<RoboticusConfig, ConfigFileError> {
139 let content = std::fs::read_to_string(path)?;
140 parse_and_validate_toml(&content)
141}
142
143pub fn resolve_default_config_path() -> PathBuf {
146 let local = PathBuf::from("roboticus.toml");
147 if local.exists() {
148 return local;
149 }
150 let home_cfg = crate::home_dir().join(".roboticus").join("roboticus.toml");
151 if home_cfg.exists() {
152 return home_cfg;
153 }
154 local
155}
156
157pub fn migrate_removed_legacy_config_file(
159 path: &Path,
160) -> Result<Option<crate::config::ConfigMigrationReport>, Box<dyn std::error::Error>> {
161 if !path.exists() {
162 return Ok(None);
163 }
164
165 let raw = std::fs::read_to_string(path)?;
166 let Some((rewritten, report)) = crate::config::migrate_removed_legacy_config(&raw)? else {
167 return Ok(None);
168 };
169
170 let defaults = BackupsConfig::default();
171 backup_config_file(path, defaults.max_count, defaults.max_age_days)?;
172 let tmp = path.with_extension("toml.tmp");
173 std::fs::write(&tmp, rewritten)?;
174 std::fs::rename(&tmp, path)?;
175 Ok(Some(report))
176}