use rand::random;
use std::path::{Path, PathBuf};
use tokio::sync::broadcast::{channel, Receiver, Sender};
use ockam::SqlxDatabase;
use ockam_core::env::get_env_with_default;
use ockam_node::database::{DatabaseConfiguration, DatabaseType};
use crate::cli_state::error::Result;
use crate::cli_state::CliStateError;
use crate::logs::ExportingEnabled;
use crate::terminal::notification::Notification;
pub const OCKAM_HOME: &str = "OCKAM_HOME";
const NOTIFICATIONS_CHANNEL_CAPACITY: usize = 16;
#[derive(Debug, Clone)]
pub struct CliState {
pub mode: CliStateMode,
database: SqlxDatabase,
application_database: SqlxDatabase,
exporting_enabled: ExportingEnabled,
notifications: Sender<Notification>,
}
impl CliState {
pub fn dir(&self) -> Result<PathBuf> {
match &self.mode {
CliStateMode::Persistent(dir) => Ok(dir.to_path_buf()),
CliStateMode::InMemory => Self::default_dir(),
}
}
pub fn database(&self) -> SqlxDatabase {
self.database.clone()
}
pub fn database_ref(&self) -> &SqlxDatabase {
&self.database
}
pub fn database_configuration(&self) -> Result<DatabaseConfiguration> {
Self::make_database_configuration(&self.mode)
}
pub fn is_using_in_memory_database(&self) -> Result<bool> {
match self.database_configuration()? {
DatabaseConfiguration::SqliteInMemory { .. } => Ok(true),
_ => Ok(false),
}
}
pub fn is_database_path(&self, path: &Path) -> bool {
let database_configuration = self.database_configuration().ok();
match database_configuration {
Some(c) => c.path() == Some(path.to_path_buf()),
None => false,
}
}
pub fn application_database(&self) -> SqlxDatabase {
self.application_database.clone()
}
pub fn application_database_configuration(&self) -> Result<DatabaseConfiguration> {
Self::make_application_database_configuration(&self.mode)
}
pub fn subscribe_to_notifications(&self) -> Receiver<Notification> {
self.notifications.subscribe()
}
pub fn notify_message(&self, message: impl Into<String>) {
self.notify(Notification::message(message));
}
pub fn notify_progress(&self, message: impl Into<String>) {
self.notify(Notification::progress(message));
}
pub fn notify_progress_finish(&self, message: impl Into<String>) {
self.notify(Notification::progress_finish(Some(message.into())));
}
pub fn notify_progress_finish_and_clear(&self) {
self.notify(Notification::progress_finish(None));
}
fn notify(&self, notification: Notification) {
let _ = self.notifications.send(notification);
}
}
impl CliState {
pub async fn new(in_memory: bool) -> Result<Self> {
let mode = if in_memory {
CliStateMode::InMemory
} else {
CliStateMode::with_default_dir()?
};
Self::create(mode).await
}
pub async fn reset(&self) -> Result<()> {
if Self::make_database_configuration(&self.mode)?.database_type() == DatabaseType::Postgres
{
Err(CliStateError::InvalidOperation(
"Cannot reset the database when using Postgres".to_string(),
))
} else {
self.delete_all_named_identities().await?;
self.delete_all_nodes().await?;
self.delete_all_named_vaults().await?;
self.delete().await
}
}
pub fn hard_reset() -> Result<()> {
let dir = Self::default_dir()?;
Self::delete_at(&dir)
}
pub async fn delete(&self) -> Result<()> {
self.delete_local_data()
}
pub fn delete_local_data(&self) -> Result<()> {
if let CliStateMode::Persistent(dir) = &self.mode {
Self::delete_at(dir)?;
}
Ok(())
}
pub async fn recreate(&self) -> Result<CliState> {
self.reset().await?;
Self::create(self.mode.clone()).await
}
pub async fn backup_and_reset() -> Result<()> {
let dir = Self::default_dir()?;
let backup_dir = Self::backup_default_dir()?;
if backup_dir.exists() {
let _ = std::fs::remove_dir_all(&backup_dir);
}
std::fs::create_dir_all(&backup_dir)?;
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
let from = entry.path();
let to = backup_dir.join(entry.file_name());
std::fs::rename(from, to)?;
}
Self::delete_at(&dir)?;
Self::create(CliStateMode::Persistent(dir.clone())).await?;
let backup_dir = CliState::backup_default_dir()?;
eprintln!("The {dir:?} directory has been reset and has been backed up to {backup_dir:?}");
Ok(())
}
pub fn backup_default_dir() -> Result<PathBuf> {
let dir = Self::default_dir()?;
let dir_name = dir.file_name().and_then(|n| n.to_str()).ok_or_else(|| {
CliStateError::InvalidOperation(
"The $OCKAM_HOME directory does not have a valid name".to_string(),
)
})?;
let parent = dir.parent().ok_or_else(|| {
CliStateError::InvalidOperation(
"The $OCKAM_HOME directory does not a valid parent directory".to_string(),
)
})?;
Ok(parent.join(format!("{dir_name}.bak")))
}
}
impl CliState {
pub async fn create(mode: CliStateMode) -> Result<Self> {
if let CliStateMode::Persistent(ref dir) = mode {
std::fs::create_dir_all(dir.as_path())?;
}
let database = SqlxDatabase::create(&Self::make_database_configuration(&mode)?).await?;
debug!("Opened the main database with options {:?}", database);
let application_database = SqlxDatabase::create_application_database(
&Self::make_application_database_configuration(&mode)?,
)
.await?;
debug!(
"Opened the application database with options {:?}",
application_database
);
let (notifications, _) = channel::<Notification>(NOTIFICATIONS_CHANNEL_CAPACITY);
let state = Self {
mode,
database,
application_database,
exporting_enabled: ExportingEnabled::Off,
notifications,
};
Ok(state)
}
pub fn is_tracing_enabled(&self) -> bool {
self.exporting_enabled == ExportingEnabled::On
}
pub fn set_tracing_enabled(self, enabled: bool) -> CliState {
CliState {
exporting_enabled: if enabled {
ExportingEnabled::On
} else {
ExportingEnabled::Off
},
..self
}
}
pub(super) fn make_database_configuration(
mode: &CliStateMode,
) -> Result<DatabaseConfiguration> {
match mode {
CliStateMode::Persistent(root_path) => {
let sqlite_path = root_path.join("database.sqlite3");
match DatabaseConfiguration::postgres_with_legacy_sqlite_path(Some(
sqlite_path.clone(),
))? {
Some(configuration) => Ok(configuration),
None => Ok(DatabaseConfiguration::sqlite(sqlite_path)),
}
}
CliStateMode::InMemory => Ok(DatabaseConfiguration::sqlite_in_memory()),
}
}
pub(super) fn make_application_database_configuration(
mode: &CliStateMode,
) -> Result<DatabaseConfiguration> {
match DatabaseConfiguration::postgres()? {
Some(configuration) => Ok(configuration),
None => match mode {
CliStateMode::Persistent(root_path) => Ok(DatabaseConfiguration::sqlite(
root_path.join("application_database.sqlite3"),
)),
CliStateMode::InMemory => Ok(DatabaseConfiguration::sqlite_in_memory()),
},
}
}
pub(super) fn make_node_dir_path(root_path: impl AsRef<Path>, node_name: &str) -> PathBuf {
Self::make_nodes_dir_path(root_path).join(node_name)
}
pub(super) fn make_command_log_path(
root_path: impl AsRef<Path>,
command_name: &str,
) -> PathBuf {
Self::make_commands_log_dir_path(root_path).join(command_name)
}
pub(super) fn make_nodes_dir_path(root_path: impl AsRef<Path>) -> PathBuf {
root_path.as_ref().join("nodes")
}
pub(super) fn make_commands_log_dir_path(root_path: impl AsRef<Path>) -> PathBuf {
root_path.as_ref().join("commands")
}
fn delete_at(root_path: &PathBuf) -> Result<()> {
let _ = std::fs::remove_dir_all(Self::make_nodes_dir_path(root_path));
let _ = std::fs::remove_dir_all(Self::make_commands_log_dir_path(root_path));
if let Some(path) =
Self::make_database_configuration(&CliStateMode::Persistent(root_path.clone()))?.path()
{
std::fs::remove_file(path)?;
};
Ok(())
}
pub(super) fn default_dir() -> Result<PathBuf> {
Ok(get_env_with_default::<PathBuf>(
"OCKAM_HOME",
home::home_dir()
.ok_or_else(|| CliStateError::InvalidPath("$HOME".to_string()))?
.join(".ockam"),
)?)
}
}
pub fn random_name() -> String {
petname::petname(2, "-").unwrap_or(hex::encode(random::<[u8; 4]>()))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CliStateMode {
Persistent(PathBuf),
InMemory,
}
impl CliStateMode {
pub fn with_default_dir() -> Result<Self> {
Ok(Self::Persistent(CliState::default_dir()?))
}
}
#[cfg(test)]
mod tests {
use super::*;
use itertools::Itertools;
use ockam_node::database::{skip_if_postgres, DatabaseType};
use std::fs;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_reset() -> Result<()> {
skip_if_postgres(|| async {
let db_file = NamedTempFile::new().unwrap();
let cli_state_directory = db_file.path().parent().unwrap().join(random_name());
let mode = CliStateMode::Persistent(cli_state_directory.clone());
let cli = CliState::create(mode).await?;
let _vault1 = cli.get_or_create_named_vault("vault1").await?;
let _vault2 = cli.get_or_create_named_vault("vault2").await?;
let identity1 = cli
.create_identity_with_name_and_vault("identity1", "vault1")
.await?;
let identity2 = cli
.create_identity_with_name_and_vault("identity2", "vault2")
.await?;
let _node1 = cli
.create_node_with_identifier("node1", &identity1.identifier())
.await?;
let _node2 = cli
.create_node_with_identifier("node2", &identity2.identifier())
.await?;
let file_names = list_file_names(&cli_state_directory);
let expected = match cli.database_configuration()?.database_type() {
DatabaseType::Sqlite => vec![
"vault-vault2".to_string(),
"application_database.sqlite3".to_string(),
"database.sqlite3".to_string(),
],
DatabaseType::Postgres => vec![],
};
assert_eq!(
file_names.iter().sorted().as_slice(),
expected.iter().sorted().as_slice()
);
cli.reset().await?;
let result = fs::read_dir(&cli_state_directory);
assert!(result.is_ok(), "the cli state directory is not deleted");
match cli.database_configuration()?.database_type() {
DatabaseType::Sqlite => {
let file_names = list_file_names(&cli_state_directory);
let expected = vec!["application_database.sqlite3".to_string()];
assert_eq!(file_names, expected);
}
DatabaseType::Postgres => (),
};
Ok(())
})
.await
}
fn list_file_names(dir: &Path) -> Vec<String> {
fs::read_dir(dir)
.unwrap()
.map(|f| f.unwrap().file_name().to_string_lossy().to_string())
.collect()
}
}