|
|
@@ -1,662 +0,0 @@
|
|
|
-/* This file is part of DarkFi (https://dark.fi)
|
|
|
- *
|
|
|
- * Copyright (C) 2020-2023 Dyne.org foundation
|
|
|
- *
|
|
|
- * This program is free software: you can redistribute it and/or modify
|
|
|
- * it under the terms of the GNU Affero General Public License as
|
|
|
- * published by the Free Software Foundation, either version 3 of the
|
|
|
- * License, or (at your option) any later version.
|
|
|
- *
|
|
|
- * This program is distributed in the hope that it will be useful,
|
|
|
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
- * GNU Affero General Public License for more details.
|
|
|
- *
|
|
|
- * You should have received a copy of the GNU Affero General Public License
|
|
|
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
- */
|
|
|
-
|
|
|
-use std::{
|
|
|
- collections::HashMap,
|
|
|
- fs::{create_dir_all, read_dir, remove_file},
|
|
|
- io::stdin,
|
|
|
- path::{Path, PathBuf},
|
|
|
- process::exit,
|
|
|
-};
|
|
|
-
|
|
|
-use async_std::{
|
|
|
- stream::StreamExt,
|
|
|
- sync::{Arc, Mutex, RwLock},
|
|
|
- task,
|
|
|
-};
|
|
|
-use dryoc::classic::crypto_secretbox::{crypto_secretbox_keygen, Key};
|
|
|
-use futures::{select, FutureExt};
|
|
|
-use lazy_static::lazy_static;
|
|
|
-use log::{debug, error, info, warn};
|
|
|
-use signal_hook::consts::{SIGHUP, SIGINT, SIGQUIT, SIGTERM};
|
|
|
-use signal_hook_async_std::Signals;
|
|
|
-use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
|
|
|
-use url::Url;
|
|
|
-
|
|
|
-use darkfi::{
|
|
|
- async_daemonize, cli_desc, net,
|
|
|
- raft::{NetMsg, ProtocolRaft, Raft, RaftSettings},
|
|
|
- rpc::server::listen_and_serve,
|
|
|
- util::{
|
|
|
- file::{load_file, load_json_file, save_file, save_json_file},
|
|
|
- path::{expand_path, get_config_path},
|
|
|
- },
|
|
|
- Result,
|
|
|
-};
|
|
|
-
|
|
|
-mod jsonrpc;
|
|
|
-use jsonrpc::JsonRpcInterface;
|
|
|
-mod lcs;
|
|
|
-use lcs::Lcs;
|
|
|
-mod patch;
|
|
|
-use patch::{EncryptedPatch, OpMethod, Patch};
|
|
|
-mod util;
|
|
|
-use util::{decrypt_patch, encrypt_patch, get_docs_paths, parse_workspaces, path_to_id};
|
|
|
-
|
|
|
-type Patches = (Vec<Patch>, Vec<Patch>, Vec<Patch>, Vec<Patch>);
|
|
|
-
|
|
|
-lazy_static! {
|
|
|
- /// This is where we hold our workspaces, so we are also able to refresh them on SIGHUP.
|
|
|
- static ref WORKSPACES: RwLock<HashMap<String, Key>> = RwLock::new(HashMap::new());
|
|
|
-}
|
|
|
-
|
|
|
-pub const CONFIG_FILE: &str = "darkwikid_config.toml";
|
|
|
-pub const CONFIG_FILE_CONTENTS: &str = include_str!("../darkwikid_config.toml");
|
|
|
-
|
|
|
-const SYNC_ID_PATH: &str = "sync";
|
|
|
-const LOCAL_ID_PATH: &str = "local";
|
|
|
-
|
|
|
-#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
|
|
|
-#[serde(default)]
|
|
|
-#[structopt(name = "darkwikid", about = cli_desc!())]
|
|
|
-struct Args {
|
|
|
- /// Increase verbosity (-vvv supported)
|
|
|
- #[structopt(short, parse(from_occurrences))]
|
|
|
- verbose: u8,
|
|
|
-
|
|
|
- /// Configuration file to use
|
|
|
- #[structopt(short, long)]
|
|
|
- config: Option<String>,
|
|
|
-
|
|
|
- /// Workspace configuration (repeatable flag)
|
|
|
- #[structopt(short, long)]
|
|
|
- workspace: Vec<String>,
|
|
|
-
|
|
|
- /// Path where to store wiki's files
|
|
|
- #[structopt(short, long, default_value = "~/darkwiki")]
|
|
|
- docs: String,
|
|
|
-
|
|
|
- /// Sets author's name for patches
|
|
|
- #[structopt(long, default_value = "Anonymous")]
|
|
|
- author: String,
|
|
|
-
|
|
|
- /// Generate a new secret for a workspace
|
|
|
- #[structopt(long)]
|
|
|
- gen_secret: bool,
|
|
|
-
|
|
|
- /// JSON-RPC listen URL
|
|
|
- #[structopt(long, default_value = "tcp://localhost:24330")]
|
|
|
- rpc_listen: Url,
|
|
|
-
|
|
|
- /// Network settings
|
|
|
- #[structopt(flatten)]
|
|
|
- net: net::settings::SettingsOpt,
|
|
|
-}
|
|
|
-
|
|
|
-/// Settings struct used to hold some metadata for DarkWiki
|
|
|
-struct DarkWikiSettings {
|
|
|
- author: String,
|
|
|
- docs_path: PathBuf,
|
|
|
- store_path: PathBuf,
|
|
|
-}
|
|
|
-
|
|
|
-/// DarkWiki object
|
|
|
-struct DarkWiki {
|
|
|
- settings: DarkWikiSettings,
|
|
|
- #[allow(clippy::type_complexity)]
|
|
|
- rpc: (
|
|
|
- smol::channel::Sender<Vec<Vec<Patch>>>,
|
|
|
- smol::channel::Receiver<(String, bool, Vec<String>)>,
|
|
|
- ),
|
|
|
- raft: (smol::channel::Sender<EncryptedPatch>, smol::channel::Receiver<EncryptedPatch>),
|
|
|
-}
|
|
|
-
|
|
|
-impl DarkWiki {
|
|
|
- async fn start(&self) -> Result<()> {
|
|
|
- loop {
|
|
|
- select! {
|
|
|
- val = self.rpc.1.recv().fuse() => {
|
|
|
- let (cmd, dry, files) = match val {
|
|
|
- Ok(v) => v,
|
|
|
- Err(e) => {
|
|
|
- error!("Failed unwrapping val received from RPC: {}", e);
|
|
|
- continue
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- match cmd.as_str() {
|
|
|
- "update" => {
|
|
|
- if let Err(e) = self.on_receive_update(dry, files).await {
|
|
|
- error!("on_receive_update returned error: {}", e);
|
|
|
- continue
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- "restore" => {
|
|
|
- if let Err(e) = self.on_receive_restore(dry, files).await {
|
|
|
- error!("on_receive_restore returned error: {}", e);
|
|
|
- continue
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- x => {
|
|
|
- warn!("Received unsupported command: {}", x);
|
|
|
- continue
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- patch = self.raft.1.recv().fuse() => {
|
|
|
- let patch = match patch {
|
|
|
- Ok(v) => v,
|
|
|
- Err(e) => {
|
|
|
- error!("Failed unwrapping patch received from raft: {}", e);
|
|
|
- continue
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- for (workspace, key) in WORKSPACES.read().await.iter() {
|
|
|
- if let Ok(mut patch) = decrypt_patch(&patch, key) {
|
|
|
- info!("[{}] Receive a {:?}", workspace, patch);
|
|
|
- patch.workspace = workspace.clone();
|
|
|
- if let Err(e) = self.on_receive_patch(&patch) {
|
|
|
- error!("on_receive_patch returned error: {}", e);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- fn on_receive_patch(&self, received_patch: &Patch) -> Result<()> {
|
|
|
- let sync_id_path = self.settings.store_path.join(SYNC_ID_PATH).join(&received_patch.id);
|
|
|
- let local_id_path = self.settings.store_path.join(LOCAL_ID_PATH).join(&received_patch.id);
|
|
|
-
|
|
|
- if let Ok(mut sync_patch) = load_json_file::<Patch>(&sync_id_path) {
|
|
|
- if sync_patch.timestamp == received_patch.timestamp {
|
|
|
- return Ok(())
|
|
|
- }
|
|
|
-
|
|
|
- if let Ok(local_patch) = load_json_file::<Patch>(&local_id_path) {
|
|
|
- if local_patch.timestamp == sync_patch.timestamp {
|
|
|
- sync_patch.base = local_patch.to_string();
|
|
|
- sync_patch.set_ops(received_patch.ops());
|
|
|
- } else {
|
|
|
- sync_patch.extend_ops(received_patch.ops());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- sync_patch.timestamp = received_patch.timestamp;
|
|
|
- sync_patch.author = received_patch.author.clone();
|
|
|
- save_json_file::<Patch>(&sync_id_path, &sync_patch)?;
|
|
|
- } else if !received_patch.base.is_empty() {
|
|
|
- save_json_file::<Patch>(&sync_id_path, received_patch)?;
|
|
|
- }
|
|
|
-
|
|
|
- Ok(())
|
|
|
- }
|
|
|
-
|
|
|
- async fn on_receive_update(&self, dry: bool, files: Vec<String>) -> Result<()> {
|
|
|
- let (mut local, mut sync, mut merge) = (vec![], vec![], vec![]);
|
|
|
-
|
|
|
- for (workspace, key) in WORKSPACES.read().await.iter() {
|
|
|
- let (patches, l, s, m) = self.update(
|
|
|
- dry,
|
|
|
- &self.settings.docs_path.join(workspace),
|
|
|
- files.clone(),
|
|
|
- workspace,
|
|
|
- )?;
|
|
|
-
|
|
|
- local.extend(l);
|
|
|
- sync.extend(s);
|
|
|
- merge.extend(m);
|
|
|
-
|
|
|
- if !dry {
|
|
|
- for patch in patches {
|
|
|
- info!("Send a {:?}", patch);
|
|
|
- let encrypt_patch = encrypt_patch(&patch, key)?;
|
|
|
- self.raft.0.send(encrypt_patch).await?;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- self.rpc.0.send(vec![local, sync, merge]).await?;
|
|
|
- Ok(())
|
|
|
- }
|
|
|
-
|
|
|
- async fn on_receive_restore(&self, dry: bool, filenames: Vec<String>) -> Result<()> {
|
|
|
- let mut patches = vec![];
|
|
|
-
|
|
|
- for (workspace, _) in WORKSPACES.read().await.iter() {
|
|
|
- patches.extend(self.restore(
|
|
|
- dry,
|
|
|
- &self.settings.docs_path.join(workspace),
|
|
|
- &filenames,
|
|
|
- workspace,
|
|
|
- )?);
|
|
|
- }
|
|
|
-
|
|
|
- self.rpc.0.send(vec![patches]).await?;
|
|
|
- Ok(())
|
|
|
- }
|
|
|
-
|
|
|
- fn restore(
|
|
|
- &self,
|
|
|
- dry: bool,
|
|
|
- docs_path: &Path,
|
|
|
- filenames: &[String],
|
|
|
- workspace: &str,
|
|
|
- ) -> Result<Vec<Patch>> {
|
|
|
- let local_path = self.settings.store_path.join(LOCAL_ID_PATH);
|
|
|
- let mut patches = vec![];
|
|
|
-
|
|
|
- let local_files = read_dir(&local_path)?;
|
|
|
- for file in local_files {
|
|
|
- let file_id = file?.file_name();
|
|
|
- let file_path = local_path.join(&file_id);
|
|
|
- let local_patch: Patch = load_json_file(&file_path)?;
|
|
|
-
|
|
|
- if local_patch.workspace != workspace {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- // TODO: FIXME: Simplify this logic, what is this? Add comments.
|
|
|
- if !filenames.is_empty() && !filenames.contains(&local_patch.path.to_string()) {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- if let Ok(doc) = load_file(&docs_path.join(&local_patch.path)) {
|
|
|
- if local_patch.to_string() == doc {
|
|
|
- continue
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if !dry {
|
|
|
- self.save_doc(&local_patch.path, &local_patch.to_string(), workspace)?;
|
|
|
- }
|
|
|
-
|
|
|
- patches.push(local_patch);
|
|
|
- }
|
|
|
-
|
|
|
- Ok(patches)
|
|
|
- }
|
|
|
-
|
|
|
- // TODO: Add debug/info statements and refactor this function, there's too many things going on here.
|
|
|
- fn update(
|
|
|
- &self,
|
|
|
- dry: bool,
|
|
|
- docs_path: &Path,
|
|
|
- filenames: Vec<String>,
|
|
|
- workspace: &str,
|
|
|
- ) -> Result<Patches> {
|
|
|
- let (mut patches, mut local_patches, mut sync_patches, mut merge_patches) =
|
|
|
- (vec![], vec![], vec![], vec![]);
|
|
|
-
|
|
|
- let local_path = self.settings.store_path.join(LOCAL_ID_PATH);
|
|
|
- let sync_path = self.settings.store_path.join(SYNC_ID_PATH);
|
|
|
-
|
|
|
- // Save and compare docs in darkwiki and local dirs, then
|
|
|
- // merge with sync patches if any have been received.
|
|
|
- let mut docs = vec![];
|
|
|
- get_docs_paths(&mut docs, docs_path, None)?;
|
|
|
- for doc in docs {
|
|
|
- let doc_path = doc.to_str().unwrap();
|
|
|
-
|
|
|
- // FIXME: IDGI
|
|
|
- if !filenames.is_empty() && !filenames.contains(&doc_path.to_string()) {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- // Load doc content
|
|
|
- let edit = load_file(&docs_path.join(doc_path))?;
|
|
|
- if edit.is_empty() {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- let doc_id = path_to_id(doc_path, workspace);
|
|
|
-
|
|
|
- // Create new patch
|
|
|
- let mut new_patch = Patch::new(doc_path, &doc_id, &self.settings.author, workspace);
|
|
|
-
|
|
|
- // Check for any changes found with local doc and darkwiki doc
|
|
|
- if let Ok(local_patch) = load_json_file::<Patch>(&local_path.join(&doc_id)) {
|
|
|
- // No changes found
|
|
|
- if local_patch.to_string() == edit {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- // Check the differences with LCS algorithm
|
|
|
- let local_patch_str = local_patch.to_string();
|
|
|
- let lcs = Lcs::new(&local_patch_str, &edit);
|
|
|
- let lcs_ops = lcs.ops();
|
|
|
-
|
|
|
- // Add the change ops to the new patch
|
|
|
- for op in lcs_ops {
|
|
|
- new_patch.add_op(&op);
|
|
|
- }
|
|
|
-
|
|
|
- new_patch.base = local_patch.to_string();
|
|
|
- local_patches.push(new_patch.clone());
|
|
|
-
|
|
|
- let mut b_patch = new_patch.clone();
|
|
|
- b_patch.base = "".to_string();
|
|
|
- patches.push(b_patch);
|
|
|
-
|
|
|
- // Check if the same doc has received a patch from the network
|
|
|
- if let Ok(sync_patch) = load_json_file::<Patch>(&sync_path.join(&doc_id)) {
|
|
|
- if !Self::is_delete_patch(&sync_patch) {
|
|
|
- if sync_patch.timestamp != local_patch.timestamp {
|
|
|
- sync_patches.push(sync_patch.clone());
|
|
|
-
|
|
|
- let sync_patch_t = new_patch.transform(&sync_patch);
|
|
|
- new_patch = new_patch.merge(&sync_patch_t);
|
|
|
- if !dry {
|
|
|
- self.save_doc(doc_path, &new_patch.to_string(), workspace)?;
|
|
|
- }
|
|
|
- merge_patches.push(new_patch.clone());
|
|
|
- }
|
|
|
- } else {
|
|
|
- merge_patches.push(sync_patch);
|
|
|
- patches = vec![];
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- new_patch.base = edit.to_string();
|
|
|
- local_patches.push(new_patch.clone());
|
|
|
- patches.push(new_patch.clone());
|
|
|
- };
|
|
|
-
|
|
|
- if !dry {
|
|
|
- save_json_file(&local_path.join(&doc_id), &new_patch)?;
|
|
|
- save_json_file(&sync_path.join(&doc_id), &new_patch)?;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Check if a new patch is received and save the new changes
|
|
|
- // in both local and darkwiki dirs.
|
|
|
- let sync_files = read_dir(&sync_path)?;
|
|
|
- for file in sync_files {
|
|
|
- let file_id = file?.file_name();
|
|
|
- let file_path = sync_path.join(&file_id);
|
|
|
- let sync_patch: Patch = load_json_file(&file_path)?;
|
|
|
-
|
|
|
- if sync_patch.workspace != workspace {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- if Self::is_delete_patch(&sync_patch) {
|
|
|
- if local_path.join(&sync_patch.id).exists() {
|
|
|
- sync_patches.push(sync_patch.clone());
|
|
|
- }
|
|
|
-
|
|
|
- if !dry {
|
|
|
- remove_file(docs_path.join(&sync_patch.path))?;
|
|
|
- remove_file(local_path.join(&sync_patch.id))?;
|
|
|
- remove_file(file_path)?;
|
|
|
- }
|
|
|
-
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- if let Ok(local_patch) = load_json_file::<Patch>(&local_path.join(&file_id)) {
|
|
|
- if local_patch.timestamp == sync_patch.timestamp {
|
|
|
- continue
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // TODO: FIXME: IDGI AGAIN, HALP
|
|
|
- if !filenames.is_empty() && !filenames.contains(&sync_patch.path.to_string()) {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- if !dry {
|
|
|
- self.save_doc(&sync_patch.path, &sync_patch.to_string(), workspace)?;
|
|
|
- save_json_file(&local_path.join(file_id), &sync_patch)?;
|
|
|
- }
|
|
|
-
|
|
|
- if !sync_patches.contains(&sync_patch) {
|
|
|
- sync_patches.push(sync_patch);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Check if any doc is removed from darkwiki filesystem.
|
|
|
- let local_files = read_dir(&local_path)?;
|
|
|
- for file in local_files {
|
|
|
- let file_id = file?.file_name();
|
|
|
- let file_path = local_path.join(&file_id);
|
|
|
- let local_patch: Patch = load_json_file(&file_path)?;
|
|
|
-
|
|
|
- if local_patch.workspace != workspace {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- // TODO: FIXME: Is it just supposed to check that filenames doesn't contain the local_patch?
|
|
|
- if !filenames.is_empty() && !filenames.contains(&local_patch.path.to_string()) {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- if !docs_path.join(&local_patch.path).exists() {
|
|
|
- let mut new_patch = Patch::new(
|
|
|
- &local_patch.path,
|
|
|
- &local_patch.id,
|
|
|
- &self.settings.author,
|
|
|
- &local_patch.workspace,
|
|
|
- );
|
|
|
- new_patch.add_op(&OpMethod::Delete(local_patch.to_string().len() as u64));
|
|
|
- patches.push(new_patch.clone());
|
|
|
-
|
|
|
- new_patch.base = local_patch.base;
|
|
|
- local_patches.push(new_patch);
|
|
|
-
|
|
|
- if !dry {
|
|
|
- remove_file(file_path)?;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- Ok((patches, local_patches, sync_patches, merge_patches))
|
|
|
- }
|
|
|
-
|
|
|
- fn save_doc(&self, path: &str, edit: &str, workspace: &str) -> Result<()> {
|
|
|
- let path = self.settings.docs_path.join(workspace).join(path);
|
|
|
- if let Some(p) = path.parent() {
|
|
|
- if !p.exists() && !p.to_str().unwrap().is_empty() {
|
|
|
- create_dir_all(p)?;
|
|
|
- }
|
|
|
- }
|
|
|
- save_file(&path, edit)
|
|
|
- }
|
|
|
-
|
|
|
- fn is_delete_patch(patch: &Patch) -> bool {
|
|
|
- if patch.ops().0.len() != 1 {
|
|
|
- return false
|
|
|
- }
|
|
|
-
|
|
|
- if let OpMethod::Delete(d) = patch.ops().0[0] {
|
|
|
- if patch.base.len() as u64 == d {
|
|
|
- return true
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async fn handle_signals(
|
|
|
- mut signals: Signals,
|
|
|
- cfg_path: PathBuf,
|
|
|
- term_tx: smol::channel::Sender<()>,
|
|
|
-) {
|
|
|
- debug!("Started signal handler");
|
|
|
- while let Some(signal) = signals.next().await {
|
|
|
- match signal {
|
|
|
- SIGHUP => {
|
|
|
- info!("Caught SIGHUP");
|
|
|
- let toml_contents = match std::fs::read_to_string(cfg_path.clone()) {
|
|
|
- Ok(v) => v,
|
|
|
- Err(e) => {
|
|
|
- error!("Couldn't load configuration file: {}", e);
|
|
|
- continue
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- *WORKSPACES.write().await = parse_workspaces(&toml_contents);
|
|
|
- info!("Reloaded workspaces");
|
|
|
- }
|
|
|
-
|
|
|
- SIGTERM | SIGINT | SIGQUIT => {
|
|
|
- term_tx.send(()).await.unwrap();
|
|
|
- }
|
|
|
-
|
|
|
- _ => unreachable!(),
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async_daemonize!(realmain);
|
|
|
-async fn realmain(args: Args, executor: Arc<smol::Executor<'_>>) -> Result<()> {
|
|
|
- let cfg_path = get_config_path(args.config, CONFIG_FILE)?;
|
|
|
- let docs_path = expand_path(&args.docs)?;
|
|
|
- let store_path = expand_path(docs_path.join(".log").to_str().unwrap())?;
|
|
|
-
|
|
|
- create_dir_all(docs_path.clone())?;
|
|
|
- create_dir_all(store_path.clone())?;
|
|
|
- create_dir_all(store_path.join(LOCAL_ID_PATH))?;
|
|
|
- create_dir_all(store_path.join(SYNC_ID_PATH))?;
|
|
|
-
|
|
|
- if args.gen_secret {
|
|
|
- eprintln!("Generating a new workspace");
|
|
|
- loop {
|
|
|
- eprint!("Input the name for the new workspace (use ascii chars): ");
|
|
|
- let mut workspace = String::new();
|
|
|
- stdin().read_line(&mut workspace)?;
|
|
|
- // Non-exhaustive
|
|
|
- let workspace =
|
|
|
- workspace.replace(['\t', '\r', ' ', '/', '\\', '\'', '&', '~', ':'], "_");
|
|
|
-
|
|
|
- if workspace.is_empty() || workspace.len() < 3 {
|
|
|
- eprintln!("Error: Workspace name is empty or less than 3 characters. Try again.");
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- let secret = bs58::encode(crypto_secretbox_keygen()).into_string();
|
|
|
- create_dir_all(docs_path.join(workspace.clone()))?;
|
|
|
-
|
|
|
- println!("Created workspace: {}:{}", workspace, secret);
|
|
|
- eprintln!("Please add it to the config file.");
|
|
|
- return Ok(())
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Signal handling for config reload and graceful termination.
|
|
|
- let signals = Signals::new([SIGHUP, SIGTERM, SIGINT, SIGQUIT])?;
|
|
|
- let handle = signals.handle();
|
|
|
- let (term_tx, term_rx) = smol::channel::bounded::<()>(1);
|
|
|
- let signals_task = task::spawn(handle_signals(signals, cfg_path.clone(), term_tx));
|
|
|
- info!("Set up signal handling");
|
|
|
-
|
|
|
- {
|
|
|
- info!("Parsing configuration file for workspaces");
|
|
|
- let toml_contents = std::fs::read_to_string(cfg_path.clone())?;
|
|
|
- *WORKSPACES.write().await = parse_workspaces(&toml_contents);
|
|
|
- if WORKSPACES.read().await.is_empty() {
|
|
|
- eprintln!("Please add atleast one workspace to the config file.");
|
|
|
- eprintln!("Run \"$ darkwikid --gen-secret\" to create a new workspace.");
|
|
|
- exit(1);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- let (rpc_tx, rpc_rx) = smol::channel::unbounded::<(String, bool, Vec<String>)>();
|
|
|
- let (notify_tx, notify_rx) = smol::channel::unbounded::<Vec<Vec<Patch>>>();
|
|
|
-
|
|
|
- // ===============
|
|
|
- // JSON-RPC server
|
|
|
- // ===============
|
|
|
- let rpc_iface = Arc::new(JsonRpcInterface::new(rpc_tx, notify_rx));
|
|
|
- let _ex = executor.clone();
|
|
|
- executor.spawn(listen_and_serve(args.rpc_listen, rpc_iface, _ex)).detach();
|
|
|
-
|
|
|
- // ====
|
|
|
- // Raft
|
|
|
- // ====
|
|
|
- let seen_net_msgs = Arc::new(Mutex::new(HashMap::new()));
|
|
|
- let store_raft = store_path.join("darkwiki.db");
|
|
|
- let raft_settings = RaftSettings { datastore_path: store_raft, ..RaftSettings::default() };
|
|
|
- // FIXME: This is a bad design, and needs a proper rework.
|
|
|
- let raft =
|
|
|
- Arc::new(Mutex::new(Raft::<EncryptedPatch>::new(raft_settings, seen_net_msgs.clone())?));
|
|
|
-
|
|
|
- // =========
|
|
|
- // P2P setup
|
|
|
- // =========
|
|
|
- let mut net_settings = args.net.clone();
|
|
|
- net_settings.app_version = Some(option_env!("CARGO_PKG_VERSION").unwrap_or("").to_string());
|
|
|
- let (p2p_tx, p2p_rx) = smol::channel::unbounded::<NetMsg>();
|
|
|
- let p2p = net::P2p::new(net_settings.into()).await;
|
|
|
- let registry = p2p.protocol_registry();
|
|
|
-
|
|
|
- let raft_node_id = raft.lock().await.id();
|
|
|
- registry.register(net::SESSION_ALL, move | channel, p2p| {
|
|
|
- let raft_node_id = raft_node_id.clone();
|
|
|
- let sender = p2p_tx.clone();
|
|
|
- let seen_net_msgs = seen_net_msgs.clone();
|
|
|
- async move {
|
|
|
- ProtocolRaft::init(raft_node_id, channel, sender, p2p, seen_net_msgs).await
|
|
|
- }
|
|
|
- }).await;
|
|
|
-
|
|
|
- p2p.clone().start(executor.clone()).await?;
|
|
|
- executor.spawn(p2p.clone().run(executor.clone())).detach();
|
|
|
-
|
|
|
- // ==============
|
|
|
- // Darkwiki start
|
|
|
- // ==============
|
|
|
- let raft_tx = raft.lock().await.sender();
|
|
|
- let raft_rx = raft.lock().await.receiver();
|
|
|
- executor
|
|
|
- .spawn(async move {
|
|
|
- let settings = DarkWikiSettings { author: args.author, store_path, docs_path };
|
|
|
- let dw = DarkWiki { settings, raft: (raft_tx, raft_rx), rpc: (notify_tx, rpc_rx) };
|
|
|
- dw.start().await.unwrap();
|
|
|
- })
|
|
|
- .detach();
|
|
|
-
|
|
|
- let (raft_term_tx, raft_term_rx) = smol::channel::bounded::<()>(1);
|
|
|
- let _p2p = p2p.clone();
|
|
|
- let _ex = executor.clone();
|
|
|
- executor
|
|
|
- .spawn(async move { raft.lock().await.run(_p2p, p2p_rx, _ex, raft_term_rx).await.unwrap() })
|
|
|
- .detach();
|
|
|
-
|
|
|
- // Wait for termination signal
|
|
|
- term_rx.recv().await?;
|
|
|
- eprint!("\r");
|
|
|
- info!("Caught termination signal, cleaning up and exiting...");
|
|
|
- handle.close();
|
|
|
- signals_task.await;
|
|
|
-
|
|
|
- info!("Stopping Raft...");
|
|
|
- raft_term_tx.send(()).await.unwrap();
|
|
|
-
|
|
|
- info!("Stopping P2P network...");
|
|
|
- p2p.stop().await;
|
|
|
-
|
|
|
- info!("Bye.");
|
|
|
- Ok(())
|
|
|
-}
|