1#![cfg(windows)]
13
14pub use winbrew_core as core;
15pub use winbrew_models as models;
16
17mod bootstrap;
18mod catalog;
19mod command_registry;
20mod config;
21pub mod connection;
22pub mod error;
23mod installed_packages;
24mod journal;
25mod migration;
26mod msi_inventory;
27
28use self::connection::SqliteConnectionManager;
29use crate::core::ResolvedPaths;
30use anyhow::{Context, Result};
31use r2d2::{Pool, PooledConnection};
32use std::cell::RefCell;
33use std::collections::HashMap;
34use std::path::PathBuf;
35use std::sync::{Mutex, OnceLock};
36
37pub type DbConnection = PooledConnection<SqliteConnectionManager>;
38
39pub use error::{CatalogNotFoundError, CatalogSchemaVersionMismatchError};
40
41pub use command_registry::{
42 CommandRegistryConflictError, find_command_owner, find_command_owners,
43 get_package_command_names, list_commands_for_package, parse_command_names,
44 sync_package_commands,
45};
46pub use config::{
47 Config, ConfigEnv, ConfigError, ConfigSection, ConfigSource, ConfigValidationError, CoreConfig,
48 PathsConfig, config_sections, config_set, config_unset, get_effective_value, suggest_key,
49};
50pub use installed_packages::{
51 PackageNotFoundError, commit_install, commit_install_with_commands, delete_package,
52 get_package, insert_package, list_installing_packages, list_packages, replay_committed_journal,
53 update_installing_identity, update_status, update_status_and_engine_metadata,
54};
55pub use journal::{
56 CommittedJournalPackage, FileHash, HashAlgo, JournalEntry, JournalReadError, JournalReader,
57 JournalReplayError, JournalShimBinding, JournalWriter, package_journal_key,
58};
59pub use msi_inventory::{
60 apply_snapshot, find_packages_by_normalized_path,
61 find_packages_by_normalized_registry_key_path, get_snapshot, replace_snapshot, upsert_receipt,
62};
63
64#[cfg(test)]
65mod tests {
66 use super::{JournalWriter, get_conn, init, package_journal_key};
67 use crate::core::resolved_paths;
68 use std::fs;
69 use tempfile::tempdir;
70
71 #[test]
72 fn init_bootstraps_primary_pool_and_journal_paths() {
73 let root = tempdir().expect("temp dir");
74 let paths = resolved_paths(
75 root.path(),
76 "${root}\\packages",
77 "${root}\\data",
78 "${root}\\data\\logs",
79 "${root}\\data\\cache",
80 );
81
82 init(&paths).expect("initialize database state");
83
84 let _conn = get_conn().expect("open primary database connection");
85 let journal_key = package_journal_key("winget/Contoso.App", "1.0.0");
86 fs::create_dir_all(paths.package_journal_dir(&journal_key))
87 .expect("create journal directory");
88 let writer = JournalWriter::open_for_package_in(&paths, "winget/Contoso.App", "1.0.0")
89 .expect("open journal writer");
90
91 assert_eq!(writer.path(), &paths.package_journal_file(&journal_key));
92 }
93}
94
95pub fn search(
97 conn: &rusqlite::Connection,
98 query: &str,
99) -> Result<Vec<crate::models::catalog::package::CatalogPackage>> {
100 catalog::search(conn, query)
101}
102
103pub fn get_package_by_id(
105 conn: &rusqlite::Connection,
106 package_id: &str,
107) -> Result<Option<crate::models::catalog::package::CatalogPackage>> {
108 catalog::get_package_by_id(conn, package_id)
109}
110
111pub fn get_installers(
113 conn: &rusqlite::Connection,
114 package_id: &str,
115) -> Result<Vec<crate::models::catalog::package::CatalogInstaller>> {
116 catalog::get_installers(conn, package_id)
117}
118
119thread_local! {
120 static CURRENT_PATHS: RefCell<Option<ResolvedPaths>> = const { RefCell::new(None) };
121}
122
123static DB_POOLS: OnceLock<Mutex<HashMap<PathBuf, &'static Pool<SqliteConnectionManager>>>> =
124 OnceLock::new();
125static CATALOG_DB_POOLS: OnceLock<Mutex<HashMap<PathBuf, &'static Pool<SqliteConnectionManager>>>> =
126 OnceLock::new();
127
128pub fn init(paths: &ResolvedPaths) -> Result<()> {
130 bootstrap::ensure_managed_root_dirs(paths)?;
131
132 CURRENT_PATHS.with(|current_paths| {
133 *current_paths.borrow_mut() = Some(paths.clone());
134 });
135
136 let _ = get_pool()?;
137
138 Ok(())
139}
140
141fn resolved_paths() -> Result<ResolvedPaths> {
142 CURRENT_PATHS.with(|current_paths| {
143 if current_paths.borrow().is_none() {
144 let paths = Config::load_current()?.resolved_paths();
145 *current_paths.borrow_mut() = Some(paths);
146 }
147
148 current_paths
149 .borrow()
150 .as_ref()
151 .cloned()
152 .context("failed to initialize database resolved paths")
153 })
154}
155
156fn get_pool() -> Result<&'static Pool<SqliteConnectionManager>> {
157 pool_for(
158 DB_POOLS.get_or_init(|| Mutex::new(HashMap::new())),
159 resolved_paths()?.db.clone(),
160 false,
161 10,
162 Some(migration::migrate),
163 )
164}
165
166pub fn get_conn() -> Result<PooledConnection<SqliteConnectionManager>> {
168 let pool = get_pool()?;
169 pool.get()
170 .context("failed to acquire database connection from pool")
171}
172
173pub fn get_catalog_conn() -> Result<PooledConnection<SqliteConnectionManager>> {
175 if !resolved_paths()?.catalog_db.exists() {
176 return Err(CatalogNotFoundError.into());
177 }
178
179 let pool = get_catalog_pool()?;
180 let conn = pool
181 .get()
182 .context("failed to acquire catalog database connection from pool")?;
183 catalog::ensure_schema_version(&conn)?;
184
185 Ok(conn)
186}
187
188fn get_catalog_pool() -> Result<&'static Pool<SqliteConnectionManager>> {
189 pool_for(
190 CATALOG_DB_POOLS.get_or_init(|| Mutex::new(HashMap::new())),
191 resolved_paths()?.catalog_db.clone(),
192 true,
193 4,
194 None,
195 )
196}
197
198fn pool_for(
199 pools: &'static Mutex<HashMap<PathBuf, &'static Pool<SqliteConnectionManager>>>,
200 path: PathBuf,
201 read_only: bool,
202 max_size: u32,
203 migrate: Option<fn(&rusqlite::Connection) -> Result<()>>,
204) -> Result<&'static Pool<SqliteConnectionManager>> {
205 let mut pools = pools
206 .lock()
207 .map_err(|_| anyhow::anyhow!("database pool registry lock poisoned"))?;
208
209 if let Some(pool) = pools.get(&path) {
210 return Ok(*pool);
211 }
212
213 let pool = Box::leak(Box::new(connection::build_pool(
214 path.clone(),
215 read_only,
216 max_size,
217 migrate,
218 )?)) as &'static Pool<SqliteConnectionManager>;
219
220 pools.insert(path, pool);
221 Ok(pool)
222}