use std::path::Path;
use crate::metering::MeteringStore;
use crate::paths::ConfigSource;
pub async fn open_readonly(source: &ConfigSource) -> Option<MeteringStore> {
let config = crate::paths::load_config(source).await.ok()?;
let url = resolve_sqlite_url(&config.database.url, source.home())?;
let db = crate::db::connect(&url).await.ok()?;
Some(MeteringStore::new(db))
}
fn resolve_sqlite_url(url: &str, home: &Path) -> Option<String> {
let after_scheme = match url
.strip_prefix("sqlite://")
.or_else(|| url.strip_prefix("sqlite:"))
{
Some(rest) => rest,
None => return Some(url.to_string()),
};
let path_part = after_scheme.split('?').next().unwrap_or(after_scheme);
if path_part.is_empty() || path_part == ":memory:" {
return None;
}
let path = Path::new(path_part.strip_prefix("./").unwrap_or(path_part));
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
home.join(path)
};
if !absolute.exists() {
return None;
}
Some(format!("sqlite://{}?mode=ro", absolute.display()))
}
#[cfg(test)]
mod tests {
use super::resolve_sqlite_url;
use std::path::Path;
#[test]
fn non_sqlite_urls_pass_through() {
assert_eq!(
resolve_sqlite_url("postgres://db.internal/bitrouter", Path::new("/h")),
Some("postgres://db.internal/bitrouter".to_string())
);
}
#[test]
fn memory_and_empty_are_none() {
assert_eq!(resolve_sqlite_url("sqlite::memory:", Path::new("/h")), None);
assert_eq!(resolve_sqlite_url("sqlite://", Path::new("/h")), None);
}
#[test]
fn missing_file_is_none() {
assert_eq!(
resolve_sqlite_url(
"sqlite://./does-not-exist.db",
Path::new("/nonexistent-home")
),
None
);
}
#[test]
fn relative_path_anchors_to_home_when_present() {
let dir = std::env::temp_dir().join(format!("br-reader-test-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let db = dir.join("bitrouter.db");
let _ = std::fs::write(&db, b"");
let got = resolve_sqlite_url("sqlite://./bitrouter.db", &dir);
assert_eq!(got, Some(format!("sqlite://{}?mode=ro", db.display())));
let _ = std::fs::remove_dir_all(&dir);
}
}