Compare commits

...

2 commits

Author SHA1 Message Date
a3a8295880 Add node command and default path 2025-05-30 09:26:47 +09:00
d2b63140fd Update database initialization 2025-05-28 12:55:09 +09:00
12 changed files with 200 additions and 35 deletions

View file

@ -14,10 +14,7 @@ pub mod tests {
use super::*;
pub async fn get_or_init_temporary_database() -> &'static DatabaseConnection {
global::GLOBAL.get_or_try_init_temporary_database( |x| async {
migration::Migrator::up(&x, None).await?;
Ok(x)
}).await.unwrap()
global::GLOBAL.get_or_try_init_temporary_database(migration::Migrator).await.unwrap()
}
}

View file

@ -15,16 +15,20 @@ base64 = "0.22.1"
chrono = "0.4.41"
chrono-tz = "0.10.3"
clap = { version = "4.5.38", features = ["derive"] }
dirs = "6.0.0"
futures = "0.3.31"
libp2p.workspace = true
sea-orm = { version = "1.1.11", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros", "with-chrono", "with-uuid"] }
sea-orm-migration.workspace = true
serde = { version = "1.0.219", features = ["derive"] }
tempfile = { version = "3.20.0", optional = true }
thiserror = "2.0.12"
tokio = { version = "1.45.0", features = ["macros", "rt"] }
toml = "0.8.22"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
uuid = { version = "1.17.0", features = ["v4"] }
[dev-dependencies]
lazy-supplements-migration.workspace = true
sea-orm-migration.workspace = true
tempfile = "3.20.0"
tempfile = "3.20.0"

View file

@ -1,13 +0,0 @@
use std::{net::IpAddr, path::PathBuf};
use clap::Args;
#[derive(Args, Debug)]
pub struct ConnectArgs {
#[arg(long)]
endpoint: IpAddr,
#[arg(long)]
port: i32,
#[arg(long)]
config: PathBuf,
}

View file

@ -1,7 +1,7 @@
use std::path::PathBuf;
mod connect;
mod init;
mod node;
mod server;
pub use server::ServerArgs;

View file

@ -0,0 +1,30 @@
use std::{net::IpAddr, path::PathBuf};
use clap::{Args, Subcommand};
use libp2p::PeerId;
#[derive(Args, Debug)]
pub struct NodeArgs {
#[command(subcommand)]
command: NodeCommand
}
#[derive(Args, Debug)]
pub struct JoinNodeArgs {
#[arg(long)]
endpoint: IpAddr,
#[arg(long)]
port: i32,
#[arg(long)]
peer_id: String,
#[arg(long)]
config: Option<PathBuf>,
}
#[derive(Debug, Subcommand)]
pub enum NodeCommand {
Ping(JoinNodeArgs),
Join(JoinNodeArgs),
}

View file

@ -1,7 +1,10 @@
mod node;
mod server;
use std::path::Path;
use crate::error::Error;
pub use node::NodeConfig;
use serde::{Deserialize, Serialize};
pub use server::{
PartialServerConfig,
ServerConfig,
@ -10,4 +13,51 @@ pub use server::{
DEFAULT_PARTIAL_SERVER_CONFIG,
DEFAULT_SERVER_CONFIG
};
use tokio::{fs::File, io::{AsyncReadExt, AsyncWriteExt}};
#[derive(Debug, Deserialize, Serialize)]
pub struct PartialConfig {
node: Option<NodeConfig>,
server: Option<ServerConfig>,
}
impl PartialConfig {
pub fn new() -> Self {
PartialConfig {
node: Some(NodeConfig::new()),
server: None,
}
}
pub async fn read_or_create<T>(path: T) -> Result<Self, Error>
where
T: AsRef<Path>
{
if !path.as_ref().exists() {
Self::new().write_to(&path).await?;
}
Self::read_from(&path).await
}
pub async fn read_from<T>(path:T) -> Result<Self, Error>
where
T: AsRef<Path>
{
let mut file = File::open(path.as_ref()).await?;
let mut content = String::new();
file.read_to_string(&mut content).await?;
let config: PartialConfig = toml::from_str(&content)?;
Ok(config)
}
pub async fn write_to<T>(&self, path:T) -> Result<(), Error>
where
T: AsRef<Path>
{
if !path.as_ref().exists() {
let _ = File::create(&path).await?;
}
let mut file = File::open(&path).await?;
file.write_all(toml::to_string(self)?.as_bytes()).await?;
Ok(())
}
}

View file

@ -1,10 +1,31 @@
use std::path::PathBuf;
use libp2p::identity::{self, Keypair};
use serde::{Deserialize, Serialize};
use crate::global::DEFAULT_DATABASE_FILE_PATH;
#[derive(Debug, Deserialize, Serialize)]
pub struct NodeConfig {
#[serde(with = "keypair")]
secret: Keypair,
database_path: Option<PathBuf>
}
impl NodeConfig {
pub fn new() -> Self {
Self {
secret: identity::Keypair::generate_ed25519(),
database_path: None,
}
}
pub fn get_database_path(&self) -> PathBuf {
if let Some(x) = self.database_path.clone() {
x
} else {
DEFAULT_DATABASE_FILE_PATH.clone()
}
}
}
mod keypair {
@ -39,6 +60,7 @@ mod tests {
let keypair = identity::Keypair::generate_ed25519();
let config = NodeConfig {
secret: keypair.clone(),
database_path: None,
};
let string = toml::to_string(&config).unwrap();
println!("Parsed config: {}", &string);

View file

@ -2,6 +2,12 @@
pub enum Error {
#[error("DB Error: {0}")]
Db(#[from]sea_orm::DbErr),
#[error("IO Error: {0}")]
Io(#[from]std::io::Error),
#[error("mandatory config `{0}` is missing")]
MissingConfig(String),
#[error("toml deserialization error: {0}")]
TomlDe(#[from] toml::de::Error),
#[error("toml serialization error: {0}")]
TomlSer(#[from] toml::ser::Error),
}

View file

@ -1,6 +1,7 @@
use std::path::Path;
use sea_orm::{ConnectOptions, Database, DbErr, DatabaseConnection};
use sea_orm_migration::MigratorTrait;
use crate::error::Error;
use tokio::sync::OnceCell;
@ -18,26 +19,25 @@ impl Global {
fn get_database(&self) -> Option<&DatabaseConnection> {
self.database.get()
}
async fn get_or_try_init_database<T, F, Fut>(&self, path: T, migration: F) -> Result<&DatabaseConnection, Error>
async fn get_or_try_init_database<T, U>(&self, path: T, _: U) -> Result<&DatabaseConnection, Error>
where
T: AsRef<Path>,
F: FnOnce(DatabaseConnection) -> Fut,
Fut: Future<Output = Result<DatabaseConnection, DbErr>>
U: MigratorTrait,
{
let url = "sqlite://".to_string() + path.as_ref().to_str().unwrap() + "?mode=rwc";
Ok(self.database.get_or_try_init(|| async {
let db = Database::connect(&url).await?;
Ok::<DatabaseConnection, DbErr>(migration(db).await?)
U::up(&db, None).await?;
Ok::<DatabaseConnection, DbErr>(db)
}).await?)
}
#[cfg(any(test, feature="test"))]
pub async fn get_or_try_init_temporary_database<F, Fut>(&self, migration: F) -> Result<&DatabaseConnection, Error>
pub async fn get_or_try_init_temporary_database<T>(&self, migrator: T) -> Result<&DatabaseConnection, Error>
where
F: FnOnce(DatabaseConnection) -> Fut,
Fut: Future<Output = Result<DatabaseConnection, DbErr>>
T: MigratorTrait,
{
self.get_or_try_init_database(&*TEST_DATABASE_URL, migration).await
self.get_or_try_init_database(&*TEST_DATABASE_URL, migrator).await
}
}
@ -56,10 +56,7 @@ pub mod tests {
use super::*;
pub async fn get_or_init_temporary_database() -> &'static DatabaseConnection {
GLOBAL.get_or_try_init_temporary_database( |x| async {
Migrator::up(&x, None).await?;
Ok(x)
}).await.unwrap()
GLOBAL.get_or_try_init_temporary_database( Migrator).await.unwrap()
}
#[tokio::test]

View file

@ -1,17 +1,70 @@
use crate::config::{ServerConfig};
use std::{path::PathBuf, sync::LazyLock};
use crate::config::{NodeConfig, ServerConfig};
use sea_orm::DatabaseConnection;
use tokio::sync::OnceCell;
mod database;
pub static PRODUCT_NAME: LazyLock<String> = LazyLock::new(|| {
env!("CARGO_PKG_NAME").to_string()
});
pub static DEFAULT_CONFIG_DIR_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
let dir = if let Some(x) = dirs::config_local_dir() {
x
} else {
todo!()
};
dir.join(&*PRODUCT_NAME)
});
pub static DEFAULT_CONFIG_FILE_NAME: LazyLock<PathBuf> = LazyLock::new(|| {
PathBuf::from(String::new() + env!("CARGO_PKG_NAME") + ".toml")
});
pub static DEFAULT_CONFIG_FILE_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
DEFAULT_CONFIG_DIR_PATH.join(&*DEFAULT_CONFIG_FILE_NAME)
});
pub static DEFAULT_DATA_DIR_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
let dir = if let Some(x) = dirs::data_local_dir() {
x
} else {
todo!()
};
dir.join(&*PRODUCT_NAME)
});
pub static DEFAULT_DATABASE_FILE_NAME: LazyLock<PathBuf> = LazyLock::new(|| {
PathBuf::from(String::new() + env!("CARGO_PKG_NAME") + ".sqlite")
});
pub static DEFAULT_DATABASE_FILE_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
DEFAULT_DATA_DIR_PATH.join(&*DEFAULT_DATABASE_FILE_NAME)
});
pub static GLOBAL: Global = Global{
node_config: OnceCell::const_new(),
server_config: OnceCell::const_new(),
database: OnceCell::const_new(),
};
pub struct Global {
server_config: OnceCell<ServerConfig>,
database: OnceCell<DatabaseConnection>,
pub server_config: OnceCell<ServerConfig>,
pub node_config: OnceCell<NodeConfig>,
pub database: OnceCell<DatabaseConnection>,
}
#[cfg(test)]
pub use database::tests::get_or_init_temporary_database;
pub use database::tests::get_or_init_temporary_database;
impl Global {
pub fn get_node_config(&self) -> Option<&NodeConfig> {
self.node_config.get()
}
pub async fn get_or_try_init_node_config(&self, config: NodeConfig) -> &NodeConfig {
self.node_config.get_or_init(|| async {config}).await
}
}

View file

@ -3,3 +3,5 @@ pub mod config;
pub mod entity;
pub mod error;
pub mod global;
#[cfg(any(test, feature="test"))]
pub mod tests;

View file

@ -0,0 +1,17 @@
use std::{path::PathBuf, sync::LazyLock};
use tempfile::TempDir;
pub static TEST_DIR_PATH: LazyLock<PathBuf> = LazyLock::new(|| {
let pkg_name = env!("CARGO_PKG_NAME");
let timestamp = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Nanos, false);
std::env::temp_dir().join(pkg_name).join( &timestamp)
});
pub static TEST_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
TempDir::new().unwrap().keep()
});
pub static TEST_DATABASE_PATH: std::sync::LazyLock<PathBuf> = std::sync::LazyLock::new(|| {
TEST_DIR_PATH.join("lazy-supplements.sqlite")
});