1use anyhow::Result;
9use serde::{Deserialize, Serialize};
10use std::fs;
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
15pub struct SemanticVersion {
16 pub major: u32,
17 pub minor: u32,
18 pub patch: u32,
19 pub prerelease: Option<String>,
20}
21
22impl SemanticVersion {
23 pub fn parse(version_str: &str) -> Result<Self> {
25 let (base, prerelease) = if let Some(dash_pos) = version_str.find('-') {
26 (
27 &version_str[..dash_pos],
28 Some(version_str[dash_pos + 1..].to_string()),
29 )
30 } else {
31 (version_str, None)
32 };
33
34 let parts: Vec<&str> = base.split('.').collect();
35 if parts.len() < 3 {
36 anyhow::bail!("Invalid version format: {}", version_str);
37 }
38
39 let major = parts[0].parse::<u32>()?;
40 let minor = parts[1].parse::<u32>()?;
41 let patch = parts[2].parse::<u32>()?;
42
43 Ok(SemanticVersion {
44 major,
45 minor,
46 patch,
47 prerelease,
48 })
49 }
50
51 pub fn is_newer_than(&self, other: &SemanticVersion) -> bool {
53 if self.major != other.major {
54 return self.major > other.major;
55 }
56 if self.minor != other.minor {
57 return self.minor > other.minor;
58 }
59 if self.patch != other.patch {
60 return self.patch > other.patch;
61 }
62
63 match (&self.prerelease, &other.prerelease) {
65 (None, Some(_)) => true, (Some(_), None) => false, _ => false, }
69 }
70
71 pub fn is_breaking_change(&self, previous: &SemanticVersion) -> bool {
73 self.major != previous.major
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct UpgradeInfo {
80 pub name: String,
82 pub current_version: String,
84 pub new_version: String,
86 pub is_breaking: bool,
88 pub available_since: String,
90}
91
92impl UpgradeInfo {
93 pub fn is_available(current: &str, new: &str) -> Result<bool> {
95 let current_ver = SemanticVersion::parse(current)?;
96 let new_ver = SemanticVersion::parse(new)?;
97 Ok(new_ver.is_newer_than(¤t_ver))
98 }
99
100 pub fn new(name: String, current_version: String, new_version: String) -> Result<Self> {
102 let current_ver = SemanticVersion::parse(¤t_version)?;
103 let new_ver = SemanticVersion::parse(&new_version)?;
104
105 Ok(UpgradeInfo {
106 name,
107 current_version,
108 new_version,
109 is_breaking: new_ver.is_breaking_change(¤t_ver),
110 available_since: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
111 })
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct BackupRecord {
118 pub plugin_name: String,
120 pub version: String,
122 pub backup_path: PathBuf,
124 pub created_at: String,
126 pub valid: bool,
128}
129
130impl BackupRecord {
131 pub fn new(plugin_name: String, version: String, backup_path: PathBuf) -> Self {
133 Self {
134 plugin_name,
135 version,
136 backup_path,
137 created_at: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
138 valid: true,
139 }
140 }
141
142 pub fn invalidate(&mut self) {
144 self.valid = false;
145 }
146}
147
148pub struct BackupManager {
150 backup_dir: PathBuf,
151 records: Vec<BackupRecord>,
152}
153
154impl BackupManager {
155 pub fn new(backup_dir: PathBuf) -> Result<Self> {
157 fs::create_dir_all(&backup_dir)?;
158 Ok(Self {
159 backup_dir,
160 records: Vec::new(),
161 })
162 }
163
164 pub fn backup_plugin(
166 &mut self,
167 plugin_name: &str,
168 version: &str,
169 plugin_path: &Path,
170 ) -> Result<PathBuf> {
171 if !plugin_path.exists() {
172 anyhow::bail!("Plugin directory does not exist: {}", plugin_path.display());
173 }
174
175 let timestamp = std::time::SystemTime::now()
177 .duration_since(std::time::UNIX_EPOCH)?
178 .as_secs();
179
180 let backup_path = self
181 .backup_dir
182 .join(format!("{}-{}-{}", plugin_name, version, timestamp));
183
184 fs::create_dir_all(&backup_path)?;
185
186 copy_dir_recursive(plugin_path, &backup_path)?;
188
189 let record = BackupRecord::new(
191 plugin_name.to_string(),
192 version.to_string(),
193 backup_path.clone(),
194 );
195 self.records.push(record);
196
197 Ok(backup_path)
198 }
199
200 pub fn restore_plugin(&mut self, backup_path: &Path, restore_to: &Path) -> Result<()> {
202 if !backup_path.exists() {
203 anyhow::bail!("Backup directory does not exist: {}", backup_path.display());
204 }
205
206 if restore_to.exists() {
208 fs::remove_dir_all(restore_to)?;
209 }
210
211 copy_dir_recursive(backup_path, restore_to)?;
213
214 Ok(())
215 }
216
217 pub fn list_backups(&self, plugin_name: &str) -> Vec<BackupRecord> {
219 self.records
220 .iter()
221 .filter(|r| r.plugin_name == plugin_name && r.valid)
222 .cloned()
223 .collect()
224 }
225
226 pub fn prune_backups(&mut self, plugin_name: &str, keep_count: usize) -> Result<usize> {
228 let mut backups_for_plugin: Vec<usize> = self
229 .records
230 .iter()
231 .enumerate()
232 .filter(|(_, r)| r.plugin_name == plugin_name && r.valid)
233 .map(|(idx, _)| idx)
234 .collect();
235
236 backups_for_plugin.sort_by(|&idx_a, &idx_b| {
238 self.records[idx_b]
239 .created_at
240 .cmp(&self.records[idx_a].created_at)
241 });
242
243 let mut removed = 0;
244
245 for idx in backups_for_plugin.iter().skip(keep_count) {
247 if self.records[*idx].backup_path.exists() {
248 fs::remove_dir_all(&self.records[*idx].backup_path)?;
249 }
250 self.records[*idx].invalidate();
251 removed += 1;
252 }
253
254 Ok(removed)
255 }
256
257 pub fn count_backups(&self) -> usize {
259 self.records.iter().filter(|r| r.valid).count()
260 }
261}
262
263fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
265 fs::create_dir_all(dst)?;
266
267 for entry in fs::read_dir(src)? {
268 let entry = entry?;
269 let path = entry.path();
270 let file_name = entry.file_name();
271 let dst_path = dst.join(file_name);
272
273 if path.is_dir() {
274 copy_dir_recursive(&path, &dst_path)?;
275 } else {
276 fs::copy(&path, &dst_path)?;
277 }
278 }
279
280 Ok(())
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct UpgradeResult {
286 pub plugin_name: String,
288 pub from_version: String,
290 pub to_version: String,
292 pub success: bool,
294 pub backup_path: Option<PathBuf>,
296 pub error: Option<String>,
298}
299
300impl UpgradeResult {
301 pub fn success(
303 plugin_name: String,
304 from_version: String,
305 to_version: String,
306 backup_path: Option<PathBuf>,
307 ) -> Self {
308 Self {
309 plugin_name,
310 from_version,
311 to_version,
312 success: true,
313 backup_path,
314 error: None,
315 }
316 }
317
318 pub fn failure(
320 plugin_name: String,
321 from_version: String,
322 to_version: String,
323 error: String,
324 ) -> Self {
325 Self {
326 plugin_name,
327 from_version,
328 to_version,
329 success: false,
330 backup_path: None,
331 error: Some(error),
332 }
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn test_semantic_version_parse() {
342 let v = SemanticVersion::parse("1.2.3").unwrap();
343 assert_eq!(v.major, 1);
344 assert_eq!(v.minor, 2);
345 assert_eq!(v.patch, 3);
346 assert_eq!(v.prerelease, None);
347 }
348
349 #[test]
350 fn test_semantic_version_parse_prerelease() {
351 let v = SemanticVersion::parse("1.2.3-beta").unwrap();
352 assert_eq!(v.major, 1);
353 assert_eq!(v.minor, 2);
354 assert_eq!(v.patch, 3);
355 assert_eq!(v.prerelease, Some("beta".to_string()));
356 }
357
358 #[test]
359 fn test_version_comparison() {
360 let v1 = SemanticVersion::parse("1.2.4").unwrap();
361 let v2 = SemanticVersion::parse("1.2.3").unwrap();
362 assert!(v1.is_newer_than(&v2));
363 assert!(!v2.is_newer_than(&v1));
364 }
365
366 #[test]
367 fn test_breaking_change_detection() {
368 let v1 = SemanticVersion::parse("2.0.0").unwrap();
369 let v2 = SemanticVersion::parse("1.5.0").unwrap();
370 assert!(v1.is_breaking_change(&v2));
371 }
372
373 #[test]
374 fn test_upgrade_info_creation() {
375 let info = UpgradeInfo::new(
376 "test-plugin".to_string(),
377 "1.0.0".to_string(),
378 "1.1.0".to_string(),
379 )
380 .unwrap();
381 assert_eq!(info.name, "test-plugin");
382 assert_eq!(info.current_version, "1.0.0");
383 assert_eq!(info.new_version, "1.1.0");
384 assert!(!info.is_breaking);
385 }
386
387 #[test]
388 fn test_upgrade_info_breaking_change() {
389 let info = UpgradeInfo::new(
390 "test-plugin".to_string(),
391 "1.0.0".to_string(),
392 "2.0.0".to_string(),
393 )
394 .unwrap();
395 assert!(info.is_breaking);
396 }
397
398 #[test]
399 fn test_backup_record_creation() {
400 let record = BackupRecord::new(
401 "test-plugin".to_string(),
402 "1.0.0".to_string(),
403 PathBuf::from("/backups/test-plugin-1.0.0"),
404 );
405 assert_eq!(record.plugin_name, "test-plugin");
406 assert_eq!(record.version, "1.0.0");
407 assert!(record.valid);
408 }
409
410 #[test]
411 fn test_upgrade_result_success() {
412 let result = UpgradeResult::success(
413 "test-plugin".to_string(),
414 "1.0.0".to_string(),
415 "1.1.0".to_string(),
416 None,
417 );
418 assert!(result.success);
419 assert_eq!(result.from_version, "1.0.0");
420 assert_eq!(result.to_version, "1.1.0");
421 }
422
423 #[test]
424 fn test_upgrade_result_failure() {
425 let result = UpgradeResult::failure(
426 "test-plugin".to_string(),
427 "1.0.0".to_string(),
428 "1.1.0".to_string(),
429 "Installation failed".to_string(),
430 );
431 assert!(!result.success);
432 assert!(result.error.is_some());
433 }
434
435 #[test]
436 fn test_backup_manager_creation() -> Result<()> {
437 let temp_dir = tempfile::tempdir()?;
438 let manager = BackupManager::new(temp_dir.path().to_path_buf())?;
439 assert_eq!(manager.count_backups(), 0);
440 Ok(())
441 }
442}