1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs::{self, File};
5use std::path::{Path, PathBuf};
6
7use crate::config::RemoteEntry;
8
9pub type RemoteMap = HashMap<String, Vec<RemoteEntry>>;
10
11#[derive(Debug, Serialize, Deserialize)]
12pub struct VersionedCache {
13 pub version: String,
14 pub entries: RemoteMap,
15}
16
17#[derive(Debug, Deserialize)]
19struct LegacyCacheEntry {
20 remote_host: String,
21 remote_dir: String,
22 #[serde(default)]
23 override_paths: Vec<String>,
24 #[serde(default)]
25 post_sync_command: Option<String>,
26}
27
28type LegacyCache = HashMap<String, LegacyCacheEntry>;
29
30trait CacheMigrator {
32 fn version(&self) -> &str;
33 fn can_migrate(&self, data: &[u8]) -> bool;
34 fn migrate(&self, data: &[u8], cache_path: &Path) -> Result<RemoteMap>;
35}
36
37struct LegacyMigrator;
39
40impl CacheMigrator for LegacyMigrator {
41 fn version(&self) -> &str {
42 "0.1.0"
43 }
44
45 fn can_migrate(&self, data: &[u8]) -> bool {
46 serde_json::from_slice::<LegacyCache>(data).is_ok()
48 }
49
50 fn migrate(&self, data: &[u8], cache_path: &Path) -> Result<RemoteMap> {
51 println!("Migrating from legacy cache format...");
52
53 let legacy_cache: LegacyCache =
54 serde_json::from_slice(data).context("Failed to parse legacy cache")?;
55
56 let migrated = self.convert_legacy_cache(legacy_cache);
57
58 let backup_path = cache_path.with_extension("json.bak");
60 fs::copy(cache_path, &backup_path).context("Failed to backup legacy cache file")?;
61
62 println!(
63 "Cache migration complete. Backup saved at {:?}",
64 backup_path
65 );
66
67 Ok(migrated)
68 }
69}
70
71impl LegacyMigrator {
72 fn convert_legacy_cache(&self, legacy_cache: LegacyCache) -> RemoteMap {
73 let mut new_cache = RemoteMap::new();
74
75 for (dir, entry) in legacy_cache {
76 let name = format!(
77 "{}_{}",
78 entry.remote_host,
79 entry.remote_dir.replace('/', "_")
80 );
81 let remote_entry = RemoteEntry {
82 name,
83 remote_host: entry.remote_host,
84 remote_dir: entry.remote_dir,
85 override_paths: entry.override_paths,
86 post_sync_command: entry.post_sync_command,
87 preferred: false,
88 };
89
90 new_cache.insert(dir, vec![remote_entry]);
91 }
92
93 new_cache
94 }
95}
96
97pub struct MigrationManager {
99 migrators: Vec<Box<dyn CacheMigrator>>,
100 current_version: String,
101}
102
103impl MigrationManager {
104 pub fn new(current_version: String) -> Self {
105 let mut manager = Self {
106 migrators: Vec::new(),
107 current_version,
108 };
109
110 manager.register_migrator(Box::new(LegacyMigrator));
112
113 manager
114 }
115
116 fn register_migrator(&mut self, migrator: Box<dyn CacheMigrator>) {
117 self.migrators.push(migrator);
118 }
119
120 pub fn read_cache(&self, cache_path: &Path) -> Result<RemoteMap> {
121 if !cache_path.exists() {
122 return Ok(RemoteMap::new());
123 }
124
125 let data = fs::read(cache_path).context("Failed to read cache file")?;
127
128 if let Ok(versioned_cache) = serde_json::from_slice::<VersionedCache>(&data) {
130 println!("Using cache version {}", versioned_cache.version);
131
132 if versioned_cache.version == self.current_version {
134 return Ok(versioned_cache.entries);
135 }
136
137 println!(
139 "Cache version {} migrated to {}",
140 versioned_cache.version, self.current_version
141 );
142 return Ok(versioned_cache.entries);
143 }
144
145 for migrator in &self.migrators {
147 if migrator.can_migrate(&data) {
148 println!("Found compatible migrator: {}", migrator.version());
149 return migrator.migrate(&data, cache_path);
150 }
151 }
152
153 eprintln!("Warning: Could not migrate cache, creating new one");
155 Ok(RemoteMap::new())
156 }
157
158 pub fn save_cache(&self, cache_path: &Path, entries: &RemoteMap) -> Result<()> {
159 let cache = VersionedCache {
160 version: self.current_version.clone(),
161 entries: entries.clone(),
162 };
163
164 let file = File::create(cache_path).context("Failed to create cache file")?;
165 serde_json::to_writer_pretty(file, &cache).context("Failed to write cache file")
166 }
167}
168
169pub fn get_cache_path() -> Result<PathBuf> {
170 let config_dir = dirs::config_dir().context("Failed to find config directory")?;
171 let cache_dir = config_dir.join("sync-rs");
172 if !cache_dir.exists() {
173 fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
174 }
175 Ok(cache_dir.join("cache.json"))
176}