Skip to main content

aperture_cli/cache/
metadata.rs

1use crate::cache::models::{GlobalCacheMetadata, SpecMetadata, CACHE_FORMAT_VERSION};
2use crate::constants;
3use crate::error::Error;
4use crate::fs::FileSystem;
5use std::path::Path;
6
7/// Manages cache metadata for optimized version checking
8pub struct CacheMetadataManager<'a, F: FileSystem> {
9    fs: &'a F,
10}
11
12impl<'a, F: FileSystem> CacheMetadataManager<'a, F> {
13    pub const fn new(fs: &'a F) -> Self {
14        Self { fs }
15    }
16
17    /// Load global cache metadata, creating default if it doesn't exist
18    ///
19    /// # Errors
20    /// Returns an error if the metadata file exists but cannot be read or parsed
21    pub fn load_metadata<P: AsRef<Path>>(
22        &self,
23        cache_dir: P,
24    ) -> Result<GlobalCacheMetadata, Error> {
25        let metadata_path = cache_dir.as_ref().join(constants::CACHE_METADATA_FILENAME);
26
27        if !self.fs.exists(&metadata_path) {
28            // Create default metadata file
29            let metadata = GlobalCacheMetadata::default();
30            self.save_metadata(&cache_dir, &metadata)?;
31            return Ok(metadata);
32        }
33
34        let content = self.fs.read_to_string(&metadata_path)?;
35        serde_json::from_str(&content)
36            .map_err(|e| Error::invalid_config(format!("Failed to parse cache metadata: {e}")))
37    }
38
39    /// Save global cache metadata
40    ///
41    /// # Errors
42    /// Returns an error if the metadata cannot be serialized or written to disk
43    pub fn save_metadata<P: AsRef<Path>>(
44        &self,
45        cache_dir: P,
46        metadata: &GlobalCacheMetadata,
47    ) -> Result<(), Error> {
48        let metadata_path = cache_dir.as_ref().join(constants::CACHE_METADATA_FILENAME);
49
50        // Ensure cache directory exists
51        self.fs.create_dir_all(cache_dir.as_ref())?;
52
53        let content = serde_json::to_string_pretty(metadata).map_err(|e| {
54            Error::serialization_error(format!("Failed to serialize cache metadata: {e}"))
55        })?;
56
57        self.fs.atomic_write(&metadata_path, content.as_bytes())?;
58        Ok(())
59    }
60
61    /// Check if a spec's cache is compatible with current version
62    ///
63    /// # Errors
64    /// Returns an error if the metadata file cannot be loaded
65    pub fn check_spec_version<P: AsRef<Path>>(
66        &self,
67        cache_dir: P,
68        spec_name: &str,
69    ) -> Result<bool, Error> {
70        let metadata = self.load_metadata(&cache_dir)?;
71
72        // Check global format version
73        if metadata.cache_format_version != CACHE_FORMAT_VERSION {
74            return Ok(false);
75        }
76
77        // Check if spec exists in metadata
78        Ok(metadata.specs.contains_key(spec_name))
79    }
80
81    /// Update metadata for a specific spec
82    ///
83    /// # Errors
84    /// Returns an error if the metadata cannot be loaded or saved
85    pub fn update_spec_metadata<P: AsRef<Path>>(
86        &self,
87        cache_dir: P,
88        spec_name: &str,
89        file_size: u64,
90    ) -> Result<(), Error> {
91        self.update_spec_metadata_with_fingerprint(
92            cache_dir, spec_name, file_size, None, None, None,
93        )
94    }
95
96    /// Update metadata for a specific spec including fingerprint data for cache invalidation
97    ///
98    /// # Errors
99    /// Returns an error if the metadata cannot be loaded or saved
100    pub fn update_spec_metadata_with_fingerprint<P: AsRef<Path>>(
101        &self,
102        cache_dir: P,
103        spec_name: &str,
104        file_size: u64,
105        content_hash: Option<String>,
106        mtime_secs: Option<u64>,
107        spec_file_size: Option<u64>,
108    ) -> Result<(), Error> {
109        let mut metadata = self.load_metadata(&cache_dir)?;
110
111        let spec_metadata = SpecMetadata {
112            updated_at: chrono::Utc::now().to_rfc3339(),
113            file_size,
114            content_hash,
115            mtime_secs,
116            spec_file_size,
117        };
118
119        metadata.specs.insert(spec_name.to_string(), spec_metadata);
120        self.save_metadata(&cache_dir, &metadata)?;
121        Ok(())
122    }
123
124    /// Retrieve the stored fingerprint for a spec (content hash, mtime, file size).
125    ///
126    /// Returns `None` if the spec is not in metadata or has no fingerprint data
127    /// (legacy metadata created before fingerprinting was added).
128    ///
129    /// # Errors
130    /// Returns an error if the metadata file cannot be loaded
131    pub fn get_stored_fingerprint<P: AsRef<Path>>(
132        &self,
133        cache_dir: P,
134        spec_name: &str,
135    ) -> Result<Option<(String, u64, u64)>, Error> {
136        let metadata = self.load_metadata(&cache_dir)?;
137
138        let Some(spec_meta) = metadata.specs.get(spec_name) else {
139            return Ok(None);
140        };
141
142        // If no fingerprint data stored, treat as legacy
143        let (Some(stored_hash), Some(stored_mtime), Some(stored_size)) = (
144            &spec_meta.content_hash,
145            spec_meta.mtime_secs,
146            spec_meta.spec_file_size,
147        ) else {
148            return Ok(None);
149        };
150
151        Ok(Some((stored_hash.clone(), stored_mtime, stored_size)))
152    }
153
154    /// Remove spec from metadata
155    ///
156    /// # Errors
157    /// Returns an error if the metadata cannot be loaded or saved
158    pub fn remove_spec_metadata<P: AsRef<Path>>(
159        &self,
160        cache_dir: P,
161        spec_name: &str,
162    ) -> Result<(), Error> {
163        let mut metadata = self.load_metadata(&cache_dir)?;
164        metadata.specs.remove(spec_name);
165        self.save_metadata(&cache_dir, &metadata)?;
166        Ok(())
167    }
168
169    /// Get all specs in metadata
170    ///
171    /// # Errors
172    /// Returns an error if the metadata file cannot be loaded
173    pub fn list_cached_specs<P: AsRef<Path>>(&self, cache_dir: P) -> Result<Vec<String>, Error> {
174        let metadata = self.load_metadata(&cache_dir)?;
175        Ok(metadata.specs.keys().cloned().collect())
176    }
177}