Skip to main content

dot_agent_core/
metadata.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::Path;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9use crate::error::Result;
10
11const META_FILENAME: &str = ".dot-agent-meta.toml";
12
13#[derive(Debug, Serialize, Deserialize)]
14pub struct Metadata {
15    pub installed: InstalledInfo,
16    pub files: HashMap<String, String>,
17    /// Tracks merged JSON entries per profile: profile_name -> file_path -> [json_paths]
18    #[serde(default)]
19    pub merged: HashMap<String, HashMap<String, Vec<String>>>,
20}
21
22#[derive(Debug, Serialize, Deserialize)]
23pub struct InstalledInfo {
24    pub installed_at: DateTime<Utc>,
25    pub profiles: Vec<String>,
26    pub base_dir: String,
27}
28
29impl Metadata {
30    pub fn new(base_dir: &Path) -> Self {
31        Self {
32            installed: InstalledInfo {
33                installed_at: Utc::now(),
34                profiles: Vec::new(),
35                base_dir: base_dir.display().to_string(),
36            },
37            files: HashMap::new(),
38            merged: HashMap::new(),
39        }
40    }
41
42    pub fn load(target: &Path) -> Result<Option<Self>> {
43        let meta_path = target.join(META_FILENAME);
44        if !meta_path.exists() {
45            return Ok(None);
46        }
47        let content = fs::read_to_string(&meta_path)?;
48        let meta: Metadata = toml::from_str(&content)?;
49        Ok(Some(meta))
50    }
51
52    pub fn save(&self, target: &Path) -> Result<()> {
53        let meta_path = target.join(META_FILENAME);
54        let content = toml::to_string_pretty(self)?;
55        fs::write(&meta_path, content)?;
56        Ok(())
57    }
58
59    pub fn add_profile(&mut self, name: &str) {
60        if !self.installed.profiles.contains(&name.to_string()) {
61            self.installed.profiles.push(name.to_string());
62        }
63    }
64
65    pub fn remove_profile(&mut self, name: &str) {
66        self.installed.profiles.retain(|p| p != name);
67    }
68
69    pub fn add_file(&mut self, path: &str, hash: &str) {
70        self.files.insert(path.to_string(), hash.to_string());
71    }
72
73    pub fn remove_file(&mut self, path: &str) {
74        self.files.remove(path);
75    }
76
77    pub fn get_file_hash(&self, path: &str) -> Option<&String> {
78        self.files.get(path)
79    }
80
81    /// Record merged JSON paths for a profile
82    pub fn add_merged(&mut self, profile: &str, file_path: &str, json_paths: Vec<String>) {
83        let profile_merged = self.merged.entry(profile.to_string()).or_default();
84        profile_merged.insert(file_path.to_string(), json_paths);
85    }
86
87    /// Get merged JSON paths for a profile and file
88    pub fn get_merged(&self, profile: &str, file_path: &str) -> Option<&Vec<String>> {
89        self.merged.get(profile).and_then(|m| m.get(file_path))
90    }
91
92    /// Get all merged files for a profile
93    pub fn get_merged_files(&self, profile: &str) -> Option<&HashMap<String, Vec<String>>> {
94        self.merged.get(profile)
95    }
96
97    /// Remove all merged entries for a profile
98    pub fn remove_merged(&mut self, profile: &str) {
99        self.merged.remove(profile);
100    }
101}
102
103pub fn compute_hash(content: &[u8]) -> String {
104    let mut hasher = Sha256::new();
105    hasher.update(content);
106    let result = hasher.finalize();
107    format!("sha256:{}", hex::encode(result))
108}
109
110pub fn compute_file_hash(path: &Path) -> Result<String> {
111    let content = fs::read(path)?;
112    Ok(compute_hash(&content))
113}