diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 859b7849c7..987f1184c5 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -23,9 +23,11 @@ log = "0.4.14" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } serde_repr = "0.1" +serde-aux = "1.0.1" derive_more = {version = "0.99", features = ["display"]} protobuf = {version = "2.20.0"} uuid = { version = "0.8", features = ["serde", "v4"] } +config = { version = "0.10.1", default-features = false, features = ["yaml"] } flowy-log = { path = "../rust-lib/flowy-log" } flowy-user = { path = "../rust-lib/flowy-user" } diff --git a/backend/configuration/base.yaml b/backend/configuration/base.yaml new file mode 100644 index 0000000000..4b78eb1b42 --- /dev/null +++ b/backend/configuration/base.yaml @@ -0,0 +1,9 @@ +application: + port: 8000 + host: 0.0.0.0 +database: + host: "localhost" + port: 5433 + username: "postgres" + password: "password" + database_name: "flowy" diff --git a/backend/configuration/local.yaml b/backend/configuration/local.yaml new file mode 100644 index 0000000000..d7f541e28d --- /dev/null +++ b/backend/configuration/local.yaml @@ -0,0 +1,5 @@ +application: + host: 127.0.0.1 + base_url: "http://127.0.0.1" +database: + require_ssl: false diff --git a/backend/configuration/production.yaml b/backend/configuration/production.yaml new file mode 100644 index 0000000000..cd4608ab4f --- /dev/null +++ b/backend/configuration/production.yaml @@ -0,0 +1,4 @@ +application: + host: 0.0.0.0 +database: + require_ssl: true diff --git a/backend/scripts/database/db_init.sh b/backend/scripts/database/db_init.sh index ac59b7ac37..e6a12223ec 100755 --- a/backend/scripts/database/db_init.sh +++ b/backend/scripts/database/db_init.sh @@ -2,6 +2,22 @@ set -x set -eo pipefail +if ! [ -x "$(command -v psql)" ]; then + echo >&2 "Error: `psql` is not installed." + echo >&2 "install using brew: brew install libpq." + echo >&2 "link to /usr/local/bin: brew link --force libpq ail" + + exit 1 +fi + +if ! [ -x "$(command -v sqlx)" ]; then + echo >&2 "Error: `sqlx` is not installed." + echo >&2 "Use:" + echo >&2 " cargo install --version=0.5.5 sqlx-cli --no-default-features --features postgres" + echo >&2 "to install it." + exit 1 +fi + until psql -h "localhost" -U "${DB_USER}" -p "${DB_PORT}" -d "postgres" -c '\q'; do >&2 echo "Postgres is still unavailable - sleeping" diff --git a/backend/src/application.rs b/backend/src/application.rs new file mode 100644 index 0000000000..ec617cd2fd --- /dev/null +++ b/backend/src/application.rs @@ -0,0 +1,83 @@ +use crate::{ + config::{get_configuration, DatabaseSettings, Settings}, + context::AppContext, + routers::*, + user_service::Auth, + ws_service::WSServer, +}; +use actix::Actor; +use actix_web::{dev::Server, middleware, web, web::Data, App, HttpServer, Scope}; +use sqlx::{postgres::PgPoolOptions, PgPool}; +use std::{net::TcpListener, sync::Arc}; + +pub struct Application { + port: u16, + server: Server, + app_ctx: Arc, +} + +impl Application { + pub async fn build(configuration: Settings) -> Result { + let app_ctx = init_app_context(&configuration).await; + let address = format!( + "{}:{}", + configuration.application.host, configuration.application.port + ); + let listener = TcpListener::bind(&address)?; + let port = listener.local_addr().unwrap().port(); + let server = run(listener, app_ctx.clone())?; + Ok(Self { + port, + server, + app_ctx, + }) + } + + pub async fn run_until_stopped(self) -> Result<(), std::io::Error> { self.server.await } +} + +pub fn run(listener: TcpListener, app_ctx: Arc) -> Result { + let server = HttpServer::new(move || { + App::new() + .wrap(middleware::Logger::default()) + .app_data(web::JsonConfig::default().limit(4096)) + .service(ws_scope()) + .service(user_scope()) + .app_data(Data::new(app_ctx.ws_server.clone())) + .app_data(Data::new(app_ctx.db_pool.clone())) + .app_data(Data::new(app_ctx.auth.clone())) + }) + .listen(listener)? + .run(); + Ok(server) +} + +fn ws_scope() -> Scope { web::scope("/ws").service(ws::start_connection) } + +fn user_scope() -> Scope { + web::scope("/user").service(web::resource("/register").route(web::post().to(user::register))) +} + +async fn init_app_context(configuration: &Settings) -> Arc { + let _ = flowy_log::Builder::new("flowy").env_filter("Debug").build(); + let pg_pool = Arc::new( + get_connection_pool(&configuration.database) + .await + .expect("Failed to connect to Postgres."), + ); + + let ws_server = WSServer::new().start(); + + let auth = Arc::new(Auth::new(pg_pool.clone())); + + let ctx = AppContext::new(ws_server, pg_pool, auth); + + Arc::new(ctx) +} + +pub async fn get_connection_pool(configuration: &DatabaseSettings) -> Result { + PgPoolOptions::new() + .connect_timeout(std::time::Duration::from_secs(2)) + .connect_with(configuration.with_db()) + .await +} diff --git a/backend/src/config/config.rs b/backend/src/config/config.rs deleted file mode 100644 index f820a19c06..0000000000 --- a/backend/src/config/config.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::config::DatabaseConfig; -use std::{convert::TryFrom, sync::Arc}; - -pub struct Config { - pub http_port: u16, - pub database: Arc, -} - -impl Config { - pub fn new() -> Self { - Config { - http_port: 3030, - database: Arc::new(DatabaseConfig::default()), - } - } - - pub fn server_addr(&self) -> String { format!("0.0.0.0:{}", self.http_port) } -} - -pub enum Environment { - Local, - Production, -} - -impl Environment { - #[allow(dead_code)] - pub fn as_str(&self) -> &'static str { - match self { - Environment::Local => "local", - Environment::Production => "production", - } - } -} - -impl TryFrom for Environment { - type Error = String; - - fn try_from(s: String) -> Result { - match s.to_lowercase().as_str() { - "local" => Ok(Self::Local), - "production" => Ok(Self::Production), - other => Err(format!( - "{} is not a supported environment. Use either `local` or `production`.", - other - )), - } - } -} diff --git a/backend/src/config/configuration.rs b/backend/src/config/configuration.rs new file mode 100644 index 0000000000..d76de5463f --- /dev/null +++ b/backend/src/config/configuration.rs @@ -0,0 +1,99 @@ +use serde_aux::field_attributes::deserialize_number_from_string; +use sqlx::postgres::{PgConnectOptions, PgSslMode}; +use std::convert::{TryFrom, TryInto}; + +#[derive(serde::Deserialize, Clone)] +pub struct Settings { + pub database: DatabaseSettings, + pub application: ApplicationSettings, +} + +#[derive(serde::Deserialize, Clone)] +pub struct ApplicationSettings { + #[serde(deserialize_with = "deserialize_number_from_string")] + pub port: u16, + pub host: String, + pub base_url: String, +} + +#[derive(serde::Deserialize, Clone)] +pub struct DatabaseSettings { + pub username: String, + pub password: String, + #[serde(deserialize_with = "deserialize_number_from_string")] + pub port: u16, + pub host: String, + pub database_name: String, + pub require_ssl: bool, +} + +impl DatabaseSettings { + pub fn without_db(&self) -> PgConnectOptions { + let ssl_mode = if self.require_ssl { + PgSslMode::Require + } else { + PgSslMode::Prefer + }; + PgConnectOptions::new() + .host(&self.host) + .username(&self.username) + .password(&self.password) + .port(self.port) + .ssl_mode(ssl_mode) + } + + pub fn with_db(&self) -> PgConnectOptions { self.without_db().database(&self.database_name) } +} + +pub fn get_configuration() -> Result { + let mut settings = config::Config::default(); + let base_path = std::env::current_dir().expect("Failed to determine the current directory"); + let configuration_dir = base_path.join("configuration"); + + settings.merge(config::File::from(configuration_dir.join("base")).required(true))?; + + let environment: Environment = std::env::var("APP_ENVIRONMENT") + .unwrap_or_else(|_| "local".into()) + .try_into() + .expect("Failed to parse APP_ENVIRONMENT."); + + settings + .merge(config::File::from(configuration_dir.join(environment.as_str())).required(true))?; + + // Add in settings from environment variables (with a prefix of APP and '__' as + // separator) E.g. `APP_APPLICATION__PORT=5001 would set + // `Settings.application.port` + settings.merge(config::Environment::with_prefix("app").separator("__"))?; + + settings.try_into() +} + +/// The possible runtime environment for our application. +pub enum Environment { + Local, + Production, +} + +impl Environment { + pub fn as_str(&self) -> &'static str { + match self { + Environment::Local => "local", + Environment::Production => "production", + } + } +} + +impl TryFrom for Environment { + type Error = String; + + fn try_from(s: String) -> Result { + match s.to_lowercase().as_str() { + "local" => Ok(Self::Local), + "production" => Ok(Self::Production), + other => Err(format!( + "{} is not a supported environment. Use either `local` or `production`.", + other + )), + } + } +} diff --git a/backend/src/config/database/config.toml b/backend/src/config/database/config.toml deleted file mode 100644 index 2c3db17d38..0000000000 --- a/backend/src/config/database/config.toml +++ /dev/null @@ -1,5 +0,0 @@ -host = "localhost" -port = 5433 -username = "postgres" -password = "password" -database_name = "flowy" \ No newline at end of file diff --git a/backend/src/config/database/database.rs b/backend/src/config/database/database.rs deleted file mode 100644 index 1013f26f16..0000000000 --- a/backend/src/config/database/database.rs +++ /dev/null @@ -1,33 +0,0 @@ -use serde::Deserialize; - -#[derive(Deserialize)] -pub struct DatabaseConfig { - username: String, - password: String, - port: u16, - host: String, - database_name: String, -} - -impl DatabaseConfig { - pub fn connect_url(&self) -> String { - format!( - "postgres://{}:{}@{}:{}/{}", - self.username, self.password, self.host, self.port, self.database_name - ) - } - - pub fn set_env_db_url(&self) { - let url = self.connect_url(); - std::env::set_var("DATABASE_URL", url); - } -} - -impl std::default::Default for DatabaseConfig { - fn default() -> DatabaseConfig { - let toml_str: &str = include_str!("config.toml"); - let config: DatabaseConfig = toml::from_str(toml_str).unwrap(); - config.set_env_db_url(); - config - } -} diff --git a/backend/src/config/database/mod.rs b/backend/src/config/database/mod.rs deleted file mode 100644 index d505655ade..0000000000 --- a/backend/src/config/database/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod database; - -pub use database::*; diff --git a/backend/src/config/mod.rs b/backend/src/config/mod.rs index bb3ca0c2c3..d4610fe72f 100644 --- a/backend/src/config/mod.rs +++ b/backend/src/config/mod.rs @@ -1,7 +1,5 @@ -mod config; +mod configuration; mod const_define; -mod database; -pub use config::*; +pub use configuration::*; pub use const_define::*; -pub use database::*; diff --git a/backend/src/context.rs b/backend/src/context.rs index 9db0fd9f3b..89cc91c247 100644 --- a/backend/src/context.rs +++ b/backend/src/context.rs @@ -1,25 +1,18 @@ -use crate::{config::Config, user_service::Auth, ws_service::WSServer}; +use crate::{user_service::Auth, ws_service::WSServer}; use actix::Addr; use sqlx::PgPool; use std::sync::Arc; pub struct AppContext { - pub config: Arc, pub ws_server: Addr, pub db_pool: Arc, pub auth: Arc, } impl AppContext { - pub fn new( - config: Arc, - ws_server: Addr, - db_pool: Arc, - auth: Arc, - ) -> Self { + pub fn new(ws_server: Addr, db_pool: Arc, auth: Arc) -> Self { AppContext { - config, ws_server, db_pool, auth, diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 7a4a35d7b1..1a21a1e314 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -1,7 +1,7 @@ -mod config; +pub mod application; +pub mod config; mod context; mod entities; mod routers; -pub mod startup; pub mod user_service; pub mod ws_service; diff --git a/backend/src/main.rs b/backend/src/main.rs index eb9baaca7f..68334d63dd 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,10 +1,11 @@ -use backend::startup::{init_app_context, run}; +use backend::{application::Application, config::get_configuration}; use std::net::TcpListener; #[actix_web::main] async fn main() -> std::io::Result<()> { - let app_ctx = init_app_context().await; - let listener = - TcpListener::bind(app_ctx.config.server_addr()).expect("Failed to bind server address"); - run(app_ctx, listener)?.await + let configuration = get_configuration().expect("Failed to read configuration."); + let application = Application::build(configuration).await?; + application.run_until_stopped().await?; + + Ok(()) } diff --git a/backend/src/startup.rs b/backend/src/startup.rs deleted file mode 100644 index 12438c0990..0000000000 --- a/backend/src/startup.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::{ - config::Config, - context::AppContext, - routers::*, - user_service::Auth, - ws_service::WSServer, -}; -use actix::Actor; -use actix_web::{dev::Server, middleware, web, web::Data, App, HttpServer, Scope}; -use sqlx::PgPool; -use std::{net::TcpListener, sync::Arc}; - -pub fn run(app_ctx: Arc, listener: TcpListener) -> Result { - let server = HttpServer::new(move || { - App::new() - .wrap(middleware::Logger::default()) - .app_data(web::JsonConfig::default().limit(4096)) - .service(ws_scope()) - .service(user_scope()) - .app_data(Data::new(app_ctx.ws_server.clone())) - .app_data(Data::new(app_ctx.db_pool.clone())) - .app_data(Data::new(app_ctx.auth.clone())) - }) - .listen(listener)? - .run(); - Ok(server) -} - -fn ws_scope() -> Scope { web::scope("/ws").service(ws::start_connection) } - -fn user_scope() -> Scope { - web::scope("/user").service(web::resource("/register").route(web::post().to(user::register))) -} - -pub async fn init_app_context() -> Arc { - let _ = flowy_log::Builder::new("flowy").env_filter("Debug").build(); - let config = Arc::new(Config::new()); - - // TODO: what happened when PgPool connect fail? - let db_pool = Arc::new( - PgPool::connect(&config.database.connect_url()) - .await - .expect("Failed to connect to Postgres."), - ); - let ws_server = WSServer::new().start(); - let auth = Arc::new(Auth::new(db_pool.clone())); - - let ctx = AppContext::new(config, ws_server, db_pool, auth); - Arc::new(ctx) -}