soar_core/database/
repository.rs

1use regex::Regex;
2use rusqlite::{params, Result, Transaction};
3
4use super::{models::RemotePackage, packages::PackageProvide, statements::DbStatements};
5
6pub struct PackageRepository<'a> {
7    tx: &'a Transaction<'a>,
8    statements: DbStatements<'a>,
9    repo_name: &'a str,
10}
11
12impl<'a> PackageRepository<'a> {
13    pub fn new(tx: &'a Transaction<'a>, statements: DbStatements<'a>, repo_name: &'a str) -> Self {
14        Self {
15            tx,
16            statements,
17            repo_name,
18        }
19    }
20
21    pub fn import_packages(&mut self, metadata: &[RemotePackage]) -> Result<()> {
22        self.statements
23            .repo_insert
24            // to prevent incomplete sync, etag should only be updated once
25            // all checks are done
26            .execute(params![self.repo_name, ""])?;
27
28        for package in metadata {
29            self.insert_package(package)?;
30        }
31        Ok(())
32    }
33
34    fn get_or_create_maintainer(&mut self, name: &str, contact: &str) -> Result<i64> {
35        self.statements
36            .maintainer_check
37            .query_row(params![contact], |row| row.get(0))
38            .or_else(|_| {
39                self.statements
40                    .maintainer_insert
41                    .execute(params![name, contact])?;
42                Ok(self.tx.last_insert_rowid())
43            })
44    }
45
46    fn extract_name_and_contact(&self, input: &str) -> Option<(String, String)> {
47        let re = Regex::new(r"^([^()]+) \(([^)]+)\)$").unwrap();
48
49        if let Some(captures) = re.captures(input) {
50            let name = captures.get(1).map_or("", |m| m.as_str()).to_string();
51            let contact = captures.get(2).map_or("", |m| m.as_str()).to_string();
52            Some((name, contact))
53        } else {
54            None
55        }
56    }
57
58    fn insert_package(&mut self, package: &RemotePackage) -> Result<()> {
59        let disabled_reason = serde_json::to_string(&package.disabled_reason).unwrap();
60        let licenses = serde_json::to_string(&package.licenses).unwrap();
61        let ghcr_files = serde_json::to_string(&package.ghcr_files).unwrap();
62        let homepages = serde_json::to_string(&package.homepages).unwrap();
63        let notes = serde_json::to_string(&package.notes).unwrap();
64        let source_urls = serde_json::to_string(&package.src_urls).unwrap();
65        let tags = serde_json::to_string(&package.tags).unwrap();
66        let categories = serde_json::to_string(&package.categories).unwrap();
67        let snapshots = serde_json::to_string(&package.snapshots).unwrap();
68        let repology = serde_json::to_string(&package.repology).unwrap();
69        let replaces = serde_json::to_string(&package.replaces).unwrap();
70
71        const PROVIDES_DELIMITERS: &[&str] = &["==", "=>", ":"];
72
73        let provides = package.provides.as_ref().map(|vec| {
74            vec.iter()
75                .filter_map(|p| {
76                    let include = *p == package.pkg_name
77                        || matches!(package.recurse_provides, Some(true))
78                        || p.strip_prefix(&package.pkg_name).is_some_and(|rest| {
79                            PROVIDES_DELIMITERS.iter().any(|d| rest.starts_with(d))
80                        });
81
82                    include.then(|| PackageProvide::from_string(p))
83                })
84                .collect::<Vec<_>>()
85        });
86
87        let provides = serde_json::to_string(&provides).unwrap();
88        let inserted = self.statements.package_insert.execute(params![
89            package.disabled,
90            disabled_reason,
91            package.rank,
92            package.pkg,
93            package.pkg_id,
94            package.pkg_name,
95            package.pkg_family,
96            package.pkg_type,
97            package.pkg_webpage,
98            package.app_id,
99            package.description,
100            package.version,
101            package.version_upstream,
102            licenses,
103            package.download_url,
104            package.size_raw,
105            package.ghcr_pkg,
106            package.ghcr_size_raw,
107            ghcr_files,
108            package.ghcr_blob,
109            package.ghcr_url,
110            package.bsum,
111            package.shasum,
112            package.icon,
113            package.desktop,
114            package.appstream,
115            homepages,
116            notes,
117            source_urls,
118            tags,
119            categories,
120            package.build_id,
121            package.build_date,
122            package.build_action,
123            package.build_script,
124            package.build_log,
125            provides,
126            snapshots,
127            repology,
128            replaces,
129            package.download_count,
130            package.download_count_week,
131            package.download_count_month,
132            package.bundle.unwrap_or(false),
133            package.bundle_type,
134            package.soar_syms.unwrap_or(false),
135            package.deprecated.unwrap_or(false),
136            package.desktop_integration,
137            package.external,
138            package.installable,
139            package.portable,
140            package.recurse_provides,
141            package.trusted,
142            package.version_latest,
143            package.version_outdated
144        ])?;
145        if inserted == 0 {
146            return Ok(());
147        }
148        let package_id = self.tx.last_insert_rowid();
149        for maintainer in &package.maintainers.clone().unwrap_or_default() {
150            let typed = self.extract_name_and_contact(maintainer);
151            if let Some((name, contact)) = typed {
152                let maintainer_id = self.get_or_create_maintainer(&name, &contact)?;
153                self.statements
154                    .pkg_maintainer_insert
155                    .execute(params![maintainer_id, package_id])?;
156            }
157        }
158
159        Ok(())
160    }
161}