use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, OnceLock, RwLock};
use crate::error::RustMinidbError;
use crate::sql::executor::Executor;
use crate::storage::engine::SharedEngine;
use crate::storage::redb_engine::RedbEngine;
use crate::watcher;
pub static APP_TOKEN: OnceLock<String> = OnceLock::new();
#[derive(Clone)]
pub struct DbInstance {
pub engine: SharedEngine,
pub executor: Arc<Executor>,
}
impl DbInstance {
pub fn from_path(path: &str) -> Result<Self, RustMinidbError> {
let engine = RedbEngine::open(path)?;
let shared: SharedEngine = Arc::new(engine);
let executor = Arc::new(Executor::new(shared.clone()));
Ok(Self {
engine: shared,
executor,
})
}
pub fn reload(&mut self, path: &str) -> Result<(), RustMinidbError> {
let engine = RedbEngine::open(path)?;
let shared: SharedEngine = Arc::new(engine);
self.engine = shared.clone();
self.executor = Arc::new(Executor::new(shared));
Ok(())
}
}
#[derive(Clone)]
pub struct AppState {
pub inner: Arc<RwLock<AppStateInner>>,
}
pub struct AppStateInner {
pub current_db: String,
pub db_dir: String,
pub db_instance: DbInstance,
pub databases: HashMap<String, DbInstance>,
pub start_time: std::time::Instant,
pub api_token: Option<String>,
}
impl AppState {
pub fn new(db_dir: &str, db_name: &str, api_token: Option<String>) -> Result<Self, RustMinidbError> {
let db_path = Self::db_path(db_dir, db_name);
let db_instance = DbInstance::from_path(&db_path)?;
let mut databases = HashMap::new();
databases.insert(db_name.to_string(), db_instance.clone());
if let Some(ref token) = api_token {
let _ = APP_TOKEN.set(token.clone());
}
Ok(Self {
inner: Arc::new(RwLock::new(AppStateInner {
current_db: db_name.to_string(),
db_dir: db_dir.to_string(),
db_instance,
databases,
start_time: std::time::Instant::now(),
api_token,
})),
})
}
pub fn new_multi(
db_dir: &str,
db_names: &[String],
api_token: Option<String>,
) -> Result<Self, RustMinidbError>
{
if db_names.is_empty() {
return Err(RustMinidbError::Config(
"At least one database file must be specified".into(),
));
}
let mut databases = HashMap::new();
let mut db_instance = None;
for name in db_names {
let db_path = Self::db_path(db_dir, name);
let instance = DbInstance::from_path(&db_path)?;
if db_instance.is_none() {
db_instance = Some(instance.clone());
}
databases.insert(name.clone(), instance);
}
let current_db = db_names[0].clone();
let db_instance = db_instance.unwrap();
if let Some(ref token) = api_token {
let _ = APP_TOKEN.set(token.clone());
}
Ok(Self {
inner: Arc::new(RwLock::new(AppStateInner {
current_db,
db_dir: db_dir.to_string(),
db_instance,
databases,
start_time: std::time::Instant::now(),
api_token,
})),
})
}
pub fn from_engine(engine: SharedEngine, executor: Arc<Executor>, api_token: Option<String>) -> Self {
let db_instance = DbInstance {
engine,
executor,
};
let mut databases = HashMap::new();
databases.insert("data.db".to_string(), db_instance.clone());
if let Some(ref token) = api_token {
let _ = APP_TOKEN.set(token.clone());
}
Self {
inner: Arc::new(RwLock::new(AppStateInner {
current_db: "data.db".to_string(),
db_dir: ".".to_string(),
db_instance,
databases,
start_time: std::time::Instant::now(),
api_token,
})),
}
}
pub fn switch_db(&self, db_name: &str) -> Result<(), RustMinidbError> {
let mut inner = self.inner.write().expect("AppState lock poisoned");
if inner.databases.contains_key(db_name) {
let instance = inner.databases.get(db_name).unwrap().clone();
inner.current_db = db_name.to_string();
inner.db_instance = instance;
return Ok(());
}
let db_path = Self::db_path(&inner.db_dir, db_name);
let db_instance = DbInstance::from_path(&db_path)?;
inner.current_db = db_name.to_string();
inner.db_instance = db_instance.clone();
inner.databases.insert(db_name.to_string(), db_instance);
Ok(())
}
pub fn db(&self) -> std::sync::RwLockReadGuard<'_, AppStateInner> {
self.inner.read().expect("AppState lock poisoned")
}
pub fn db_mut(&self) -> std::sync::RwLockWriteGuard<'_, AppStateInner> {
self.inner.write().expect("AppState lock poisoned")
}
pub fn reload_database(&self, db_name: &str) -> Result<(), RustMinidbError> {
let mut inner = self.inner.write().expect("AppState lock poisoned");
let db_path = Self::db_path(&inner.db_dir, db_name);
let is_current = inner.current_db == db_name;
if inner.databases.contains_key(db_name) {
let updated_instance = {
let instance = inner.databases.get_mut(db_name).unwrap();
instance.reload(&db_path)?;
instance.clone()
};
if is_current {
inner.db_instance = updated_instance;
}
tracing::info!("Database auto-reloaded: {} (file changed)", db_name);
} else {
let instance = DbInstance::from_path(&db_path)?;
inner.databases.insert(db_name.to_string(), instance.clone());
if is_current {
inner.db_instance = instance;
}
tracing::info!("Database auto-opened: {} (file created)", db_name);
}
Ok(())
}
pub fn loaded_databases(&self) -> Vec<String> {
let inner = self.inner.read().expect("AppState lock poisoned");
inner.databases.keys().cloned().collect()
}
pub fn current_db_name(&self) -> String {
let inner = self.inner.read().expect("AppState lock poisoned");
inner.current_db.clone()
}
pub fn list_databases(&self) -> Result<Vec<String>, RustMinidbError> {
let inner = self.inner.read().expect("AppState lock poisoned");
let dir = Path::new(&inner.db_dir);
let mut databases: Vec<String> = Vec::new();
for name in inner.databases.keys() {
if !databases.contains(name) {
databases.push(name.clone());
}
}
if dir.exists() {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
if entry.path().extension().map_or(false, |e| e == "db") {
if let Some(name) = entry.file_name().to_str() {
let name = name.to_string();
if !databases.contains(&name) {
databases.push(name);
}
}
}
}
}
databases.sort();
Ok(databases)
}
pub fn create_database(&self, db_name: &str) -> Result<(), RustMinidbError> {
let inner = self.inner.read().expect("AppState lock poisoned");
let db_path = Self::db_path(&inner.db_dir, db_name);
let _ = RedbEngine::open(&db_path)?;
Ok(())
}
pub fn remove_database(&self, db_name: &str) {
let mut inner = self.inner.write().expect("AppState lock poisoned");
inner.databases.remove(db_name);
if inner.current_db == db_name {
let first_entry: Option<(String, DbInstance)> = {
let mut iter = inner.databases.iter();
iter.next().map(|(k, v)| (k.clone(), v.clone()))
};
if let Some((name, instance)) = first_entry {
inner.current_db = name;
inner.db_instance = instance;
}
}
}
fn db_path(db_dir: &str, db_name: &str) -> String {
let name = if db_name.ends_with(".db") {
db_name.to_string()
} else {
format!("{}.db", db_name)
};
Path::new(db_dir).join(&name).to_string_lossy().to_string()
}
pub fn uptime_secs(&self) -> u64 {
self.inner.read().expect("AppState lock poisoned").start_time.elapsed().as_secs()
}
pub fn start_file_watcher(&self) -> Result<watcher::FileWatcher, RustMinidbError> {
let state = self.clone();
let mut file_watcher = watcher::FileWatcher::new();
let cb: watcher::ChangeCallback = Arc::new(move |event| {
let db_name = match &event {
watcher::FileChangeEvent::DatabaseModified { db_name, .. } => db_name.clone(),
watcher::FileChangeEvent::DatabaseCreated { db_name, .. } => db_name.clone(),
watcher::FileChangeEvent::DatabaseDeleted { db_name, .. } => db_name.clone(),
};
match &event {
watcher::FileChangeEvent::DatabaseModified { .. } => {
let _ = state.reload_database(&db_name);
}
watcher::FileChangeEvent::DatabaseCreated { .. } => {
let _ = state.reload_database(&db_name);
}
watcher::FileChangeEvent::DatabaseDeleted { .. } => {
state.remove_database(&db_name);
tracing::info!("Database removed (file deleted): {}", db_name);
}
}
});
let dir = {
let inner = self.inner.read().expect("AppState lock poisoned");
inner.db_dir.clone()
};
file_watcher.watch(&dir, cb).map_err(|e| {
RustMinidbError::Config(format!("Failed to start file watcher: {}", e))
})?;
tracing::info!("File watcher started for directory: {}", dir);
Ok(file_watcher)
}
}