Skip to main content

robonix_cli/
database.rs

1// SPDX-License-Identifier: MulanPSL-2.0
2// Package database for system-installed packages (~/.robonix/packages)
3
4use anyhow::{Context, Result};
5use serde::{Deserialize, Serialize};
6use std::collections::{HashMap, HashSet};
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PackageInfo {
11    pub name: String,
12    pub version: String,
13    pub path: PathBuf,
14    pub manifest_path: PathBuf,
15    #[serde(default)]
16    pub capabilities: Vec<String>,
17    #[serde(default)]
18    pub depends: Vec<String>,
19    pub installed_at: String,
20    pub source: PackageSource,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub enum PackageSource {
25    Local {
26        path: PathBuf,
27    },
28    GitHub {
29        repo: String,
30        branch: Option<String>,
31        commit: String,
32    },
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct PackageDatabase {
37    packages: HashMap<String, PackageInfo>,
38}
39
40impl PackageDatabase {
41    pub fn db_path(storage_path: &Path) -> PathBuf {
42        storage_path.join("db.json")
43    }
44
45    pub fn load(storage_path: &Path) -> Result<Self> {
46        let db_path = Self::db_path(storage_path);
47        if !db_path.exists() {
48            return Ok(Self {
49                packages: HashMap::new(),
50            });
51        }
52        let content = std::fs::read_to_string(&db_path)
53            .with_context(|| format!("Failed to read database: {}", db_path.display()))?;
54        let db: PackageDatabase = serde_json::from_str(&content)
55            .with_context(|| format!("Failed to parse database: {}", db_path.display()))?;
56        Ok(db)
57    }
58
59    pub fn save(&self, storage_path: &Path) -> Result<()> {
60        let db_path = Self::db_path(storage_path);
61        let content = serde_json::to_string_pretty(self).context("Failed to serialize database")?;
62        std::fs::write(&db_path, content)
63            .with_context(|| format!("Failed to write database: {}", db_path.display()))?;
64        Ok(())
65    }
66
67    pub fn add_package(&mut self, info: PackageInfo) {
68        self.packages.insert(info.name.clone(), info);
69    }
70
71    pub fn remove_package(&mut self, name: &str) -> Option<PackageInfo> {
72        self.packages.remove(name)
73    }
74
75    pub fn get_package(&self, name: &str) -> Option<&PackageInfo> {
76        self.packages.get(name)
77    }
78
79    pub fn list_packages(&self) -> Vec<&PackageInfo> {
80        let mut packages: Vec<&PackageInfo> = self.packages.values().collect();
81        packages.sort_by(|a, b| a.name.cmp(&b.name));
82        packages
83    }
84
85    pub fn find_by_name(&self, name: &str) -> Option<&PackageInfo> {
86        self.packages.get(name)
87    }
88
89    pub fn sync(storage_path: &Path) -> Result<()> {
90        use crate::install::PackageInstaller;
91
92        let mut db = Self::load(storage_path)?;
93        let mut found_packages = HashSet::new();
94
95        if storage_path.exists() {
96            for entry in std::fs::read_dir(storage_path)
97                .with_context(|| format!("Failed to read storage: {}", storage_path.display()))?
98            {
99                let entry = entry?;
100                let path = entry.path();
101                if path.file_name().and_then(|n| n.to_str()) == Some("db.json") {
102                    continue;
103                }
104                if !path.is_dir() {
105                    continue;
106                }
107
108                let manifest_path = match crate::manifest::detect_manifest_path(&path) {
109                    Ok(p) => p,
110                    Err(_) => continue,
111                };
112                if !manifest_path.exists() {
113                    continue;
114                }
115
116                let package_name = match PackageInstaller::parse_manifest_name(&manifest_path) {
117                    Ok(n) => n,
118                    Err(e) => {
119                        log::warn!(
120                            "Failed to parse manifest at {}: {}",
121                            manifest_path.display(),
122                            e
123                        );
124                        continue;
125                    }
126                };
127                found_packages.insert(package_name.clone());
128
129                let source = db
130                    .get_package(&package_name)
131                    .filter(|e| e.path == path)
132                    .map(|e| e.source.clone())
133                    .unwrap_or(PackageSource::Local { path: path.clone() });
134
135                let summary = match crate::manifest::load_from_path(&manifest_path)
136                    .and_then(|m| m.validate_and_summarize())
137                {
138                    Ok(s) => s,
139                    Err(e) => {
140                        log::warn!("Failed to parse manifest at {}: {}", path.display(), e);
141                        continue;
142                    }
143                };
144
145                match PackageInstaller::create_package_info(&path, &manifest_path, &summary, source)
146                {
147                    Ok(info) => db.add_package(info),
148                    Err(e) => {
149                        log::warn!("Failed to create package info at {}: {}", path.display(), e)
150                    }
151                }
152            }
153        }
154
155        for name in db.packages.keys().cloned().collect::<Vec<_>>() {
156            if !found_packages.contains(&name)
157                && let Some(removed) = db.remove_package(&name)
158            {
159                log::info!(
160                    "Removed '{}' from database (not found: {})",
161                    name,
162                    removed.path.display()
163                );
164            }
165        }
166
167        db.save(storage_path)?;
168        Ok(())
169    }
170}