sync_rs/
cache.rs

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// Legacy cache format for migration (v0)
18#[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
30// Migration trait - all migrators must implement this
31trait 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
37// Migrator for legacy cache format (no version field)
38struct 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        // Try parsing as legacy format
47        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        // Backup the old cache file
59        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
97// Migration registry
98pub 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        // Register all migrators in chronological order
111        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        // Read the cache file
126        let data = fs::read(cache_path).context("Failed to read cache file")?;
127
128        // Try parsing as versioned cache first
129        if let Ok(versioned_cache) = serde_json::from_slice::<VersionedCache>(&data) {
130            println!("Using cache version {}", versioned_cache.version);
131
132            // If already at current version, use as is
133            if versioned_cache.version == self.current_version {
134                return Ok(versioned_cache.entries);
135            }
136
137            // Future: Add specific version-to-version migrations here
138            println!(
139                "Cache version {} migrated to {}",
140                versioned_cache.version, self.current_version
141            );
142            return Ok(versioned_cache.entries);
143        }
144
145        // Try each migrator in sequence
146        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        // If no migrator works, log and return empty cache
154        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}