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 ignore_patterns: Vec::new(),
89 };
90
91 new_cache.insert(dir, vec![remote_entry]);
92 }
93
94 new_cache
95 }
96}
97
98pub struct MigrationManager {
100 migrators: Vec<Box<dyn CacheMigrator>>,
101 current_version: String,
102}
103
104impl MigrationManager {
105 pub fn new(current_version: String) -> Self {
106 let mut manager = Self {
107 migrators: Vec::new(),
108 current_version,
109 };
110
111 manager.register_migrator(Box::new(LegacyMigrator));
113
114 manager
115 }
116
117 fn register_migrator(&mut self, migrator: Box<dyn CacheMigrator>) {
118 self.migrators.push(migrator);
119 }
120
121 pub fn read_cache(&self, cache_path: &Path) -> Result<RemoteMap> {
122 if !cache_path.exists() {
123 return Ok(RemoteMap::new());
124 }
125
126 let data = fs::read(cache_path).context("Failed to read cache file")?;
128
129 if let Ok(versioned_cache) = serde_json::from_slice::<VersionedCache>(&data) {
131 println!("Using cache version {}", versioned_cache.version);
132
133 if versioned_cache.version == self.current_version {
135 return Ok(versioned_cache.entries);
136 }
137
138 println!(
140 "Cache version {} migrated to {}",
141 versioned_cache.version, self.current_version
142 );
143 return Ok(versioned_cache.entries);
144 }
145
146 for migrator in &self.migrators {
148 if migrator.can_migrate(&data) {
149 println!("Found compatible migrator: {}", migrator.version());
150 return migrator.migrate(&data, cache_path);
151 }
152 }
153
154 eprintln!("Warning: Could not migrate cache, creating new one");
156 Ok(RemoteMap::new())
157 }
158
159 pub fn save_cache(&self, cache_path: &Path, entries: &RemoteMap) -> Result<()> {
160 let cache = VersionedCache {
161 version: self.current_version.clone(),
162 entries: entries.clone(),
163 };
164
165 let file = File::create(cache_path).context("Failed to create cache file")?;
166 serde_json::to_writer_pretty(file, &cache).context("Failed to write cache file")
167 }
168}
169
170pub fn get_cache_path() -> Result<PathBuf> {
171 let config_dir = dirs::config_dir().context("Failed to find config directory")?;
172 let cache_dir = config_dir.join("sync-rs");
173 if !cache_dir.exists() {
174 fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
175 }
176 Ok(cache_dir.join("cache.json"))
177}