use crate::{
commit_monitor::CommitMonitor, config::Config, errors::AtomicServerResult, search::SearchState,
};
use atomic_lib::{
agents::{generate_public_key, Agent},
commit::CommitResponse,
Storelike,
};
#[derive(Clone)]
pub struct AppState {
pub store: atomic_lib::Db,
pub config: Config,
pub commit_monitor: actix::Addr<CommitMonitor>,
pub search_state: SearchState,
}
impl AppState {
pub fn init(config: Config) -> AtomicServerResult<AppState> {
tracing::info!("Initializing AppState");
if config.opts.slow_mode {
tracing::warn!("Slow mode is enabled. This will introduce random delays in the server, to simulate a slow connection.");
}
if config.opts.development {
tracing::warn!("Development mode is enabled. This will use staging environments for services like LetsEncrypt.");
}
let should_init = !&config.store_path.exists() || config.initialize;
let mut store = atomic_lib::Db::init(&config.store_path, config.server_url.clone())?;
if should_init {
tracing::info!("Initialize: creating and populating new Database...");
atomic_lib::populate::populate_default_store(&store)
.map_err(|e| format!("Failed to populate default store. {}", e))?;
}
set_default_agent(&config, &store)?;
let search_state = SearchState::new(&config)
.map_err(|e| format!("Failed to start search service: {}", e))?;
let commit_monitor =
crate::commit_monitor::create_commit_monitor(store.clone(), search_state.clone());
let commit_monitor_clone = commit_monitor.clone();
let send_commit = move |commit_response: &CommitResponse| {
commit_monitor_clone.do_send(crate::actor_messages::CommitMessage {
commit_response: commit_response.clone(),
});
};
store.set_handle_commit(Box::new(send_commit));
if should_init {
atomic_lib::populate::populate_all(&store)?;
let store_clone = store.clone();
std::thread::spawn(move || {
let res = store_clone.build_index(true);
if let Err(e) = res {
tracing::error!("Failed to build index: {}", e);
}
});
set_up_initial_invite(&store)
.map_err(|e| format!("Error while setting up initial invite: {}", e))?;
tracing::info!("Adding all resources to search index");
search_state.add_all_resources(&store)?;
}
Ok(AppState {
store,
config,
commit_monitor,
search_state,
})
}
fn exit(&self) -> AtomicServerResult<()> {
self.search_state.writer.write()?.commit()?;
Ok(())
}
}
impl Drop for AppState {
fn drop(&mut self) {
if let Err(e) = self.exit() {
tracing::error!("Error during AppState exit: {}", e);
}
}
}
fn set_default_agent(config: &Config, store: &impl Storelike) -> AtomicServerResult<()> {
tracing::info!("Setting default agent");
let ag_cfg: atomic_lib::config::Config = match atomic_lib::config::read_config(Some(
&config.config_file_path,
)) {
Ok(agent_config) => {
match store.get_resource(&agent_config.agent) {
Ok(_) => agent_config,
Err(e) => {
if agent_config.agent.contains(&config.server_url) {
tracing::info!("Agent not retrievable, but config was found. Recreating Agent in new store.");
let recreated_agent = Agent::new_from_private_key(
"server".into(),
store,
&agent_config.private_key,
);
store.add_resource(&recreated_agent.to_resource()?)?;
agent_config
} else {
return Err(format!(
"An agent is present in {:?}, but this agent cannot be retrieved. Either make sure the agent is retrievable, or remove it from your config. {}",
config.config_file_path, e,
).into());
}
}
}
}
Err(_no_config) => {
let agent = store.create_agent(Some("server"))?;
let cfg = atomic_lib::config::Config {
agent: agent.subject.clone(),
server: config.server_url.clone(),
private_key: agent
.private_key
.expect("No private key for agent. Check the config file."),
};
let config_string =
atomic_lib::config::write_config(&config.config_file_path, cfg.clone())?;
tracing::warn!("No existing config found, created a new Config at {:?}. Copy this to your client machine (running atomic-cli or atomic-data-browser) to log in with these credentials. \n{}", &config.config_file_path, config_string);
cfg
}
};
let agent = Agent {
subject: ag_cfg.agent.clone(),
private_key: Some(ag_cfg.private_key.clone()),
public_key: generate_public_key(&ag_cfg.private_key).public,
created_at: 0,
name: None,
};
tracing::info!("Default Agent is set: {}", &agent.subject);
store.set_default_agent(agent);
Ok(())
}
fn set_up_initial_invite(store: &impl Storelike) -> AtomicServerResult<()> {
let subject = format!("{}/setup", store.get_server_url());
tracing::info!("Creating initial Invite at {}", subject);
let mut invite = store.get_resource_new(&subject);
invite.set_class(atomic_lib::urls::INVITE);
invite.set_subject(subject);
invite.set(
atomic_lib::urls::USAGES_LEFT.into(),
atomic_lib::Value::Integer(1),
store,
)?;
invite.set(
atomic_lib::urls::WRITE_BOOL.into(),
atomic_lib::Value::Boolean(true),
store,
)?;
invite.set(
atomic_lib::urls::TARGET.into(),
atomic_lib::Value::AtomicUrl(store.get_server_url().into()),
store,
)?;
invite.set(
atomic_lib::urls::PARENT.into(),
atomic_lib::Value::AtomicUrl(store.get_server_url().into()),
store,
)?;
invite.set(
atomic_lib::urls::NAME.into(),
atomic_lib::Value::String("Setup".into()),
store,
)?;
invite.set_string(
atomic_lib::urls::DESCRIPTION.into(),
"Use this Invite to create an Agent, or use an existing one. Accepting will grant your Agent the necessary rights to edit the data in your Atomic Server. This can only be used once. If you, for whatever reason, need a new `/setup` invite, you can pass the `--initialize` flag to `atomic-server`.",
store,
)?;
invite.save_locally(store)?;
Ok(())
}