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                ignore_patterns: Vec::new(),
89            };
90
91            new_cache.insert(dir, vec![remote_entry]);
92        }
93
94        new_cache
95    }
96}
97
98// Migration registry
99pub 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        // Register all migrators in chronological order
112        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        // Read the cache file
127        let data = fs::read(cache_path).context("Failed to read cache file")?;
128
129        // Try parsing as versioned cache first
130        if let Ok(versioned_cache) = serde_json::from_slice::<VersionedCache>(&data) {
131            println!("Using cache version {}", versioned_cache.version);
132
133            // If already at current version, use as is
134            if versioned_cache.version == self.current_version {
135                return Ok(versioned_cache.entries);
136            }
137
138            // Future: Add specific version-to-version migrations here
139            println!(
140                "Cache version {} migrated to {}",
141                versioned_cache.version, self.current_version
142            );
143            return Ok(versioned_cache.entries);
144        }
145
146        // Try each migrator in sequence
147        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        // If no migrator works, log and return empty cache
155        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}