From 5bf8ed61db61676feab5432c5780edafeec0673b Mon Sep 17 00:00:00 2001 From: realaravinth Date: Fri, 9 Sep 2022 17:14:55 +0530 Subject: [PATCH] feat: init db and impl get and del site --- .gitignore | 1 + Cargo.lock | 52 ++++++++++++++ Cargo.toml | 2 + config/default.toml | 30 ++++----- src/db.rs | 160 ++++++++++++++++++++++++++++++++++++++++++++ src/settings.rs | 106 +++++++++++++++++------------ 6 files changed, 294 insertions(+), 57 deletions(-) create mode 100644 src/db.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..5f6d1fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.env.local diff --git a/Cargo.lock b/Cargo.lock index cfbc35f..1f20aa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -408,6 +408,45 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "3.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "config" version = "0.11.0" @@ -716,6 +755,7 @@ dependencies = [ "actix-web-codegen-const-routes", "argon2-creds", "base64", + "clap", "config", "derive_builder", "derive_more", @@ -1243,6 +1283,12 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + [[package]] name = "parking_lot" version = "0.11.2" @@ -2009,6 +2055,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + [[package]] name = "thiserror" version = "1.0.32" diff --git a/Cargo.toml b/Cargo.toml index 173e6e5..bdcd381 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ config = "0.11" derive_more = "0.99.17" url = { version = "2.2.2", features = ["serde"]} serde_json = "1" +sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] } +clap = { vesrion = "3.2.20", features = ["derive"]} [build-dependencies] diff --git a/config/default.toml b/config/default.toml index 6fd253d..b14e905 100644 --- a/config/default.toml +++ b/config/default.toml @@ -17,18 +17,18 @@ domain = "localhost" proxy_has_tls = false #url_prefix = "" -#[database] -## This section deals with the database location and how to access it -## Please note that at the moment, we have support for only postgresqa. -## Example, if you are Batman, your config would be: -## hostname = "batcave.org" -## port = "5432" -## username = "batman" -## password = "somereallycomplicatedBatmanpassword" -#hostname = "localhost" -#port = "5432" -#username = "postgres" -#password = "password" -#name = "postgres" -#pool = 4 -#database_type="postgres" # "postgres", "maria" +[database] +# This section deals with the database location and how to access it +# Please note that at the moment, we have support for only postgresqa. +# Example, if you are Batman, your config would be: +# hostname = "batcave.org" +# port = "5432" +# username = "batman" +# password = "somereallycomplicatedBatmanpassword" +hostname = "localhost" +port = "5432" +username = "postgres" +password = "password" +name = "postgres" +pool = 4 +database_type="postgres" # "postgres" diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..10203d7 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * 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 . + */ +use std::str::FromStr; + +use sqlx::postgres::PgPoolOptions; +//use sqlx::types::time::OffsetDateTime; +use sqlx::ConnectOptions; +use sqlx::PgPool; + +use crate::errors::*; + +/// Connect to databse +pub enum ConnectionOptions { + /// fresh connection + Fresh(Fresh), + /// existing connection + Existing(Conn), +} + +/// Use an existing database pool +pub struct Conn(pub PgPool); + +pub struct Fresh { + pub pool_options: PgPoolOptions, + pub disable_logging: bool, + pub url: String, +} + +impl ConnectionOptions { + async fn connect(self) -> ServiceResult { + let pool = match self { + Self::Fresh(fresh) => { + let mut connect_options = + sqlx::postgres::PgConnectOptions::from_str(&fresh.url).unwrap(); + if fresh.disable_logging { + connect_options.disable_statement_logging(); + } + sqlx::postgres::PgConnectOptions::from_str(&fresh.url) + .unwrap() + .disable_statement_logging(); + fresh + .pool_options + .connect_with(connect_options) + .await + .unwrap() + //.map_err(|e| DBError::DBError(Box::new(e)))? + } + + Self::Existing(conn) => conn.0, + }; + Ok(Database { pool }) + } +} + +#[derive(Clone)] +pub struct Database { + pub pool: PgPool, +} + +impl Database { + pub async fn migrate(&self) -> ServiceResult<()> { + sqlx::migrate!("./migrations/") + .run(&self.pool) + .await + .unwrap(); + //.map_err(|e| DBError::DBError(Box::new(e)))?; + Ok(()) + } + + pub async fn ping(&self) -> bool { + use sqlx::Connection; + + if let Ok(mut con) = self.pool.acquire().await { + con.ping().await.is_ok() + } else { + false + } + } + + /// register a new website + pub async fn add_site(&self, host: &str) -> ServiceResult<()> { + sqlx::query!( + "INSERT INTO forms_websites (hostname) VALUES ($1) ON CONFLICT DO NOTHING;", + &host, + ) + .execute(&self.pool) + .await + .unwrap(); + // res.map_err(map_register_err)?; + Ok(()) + } + + /// delete a new website + pub async fn delete_site(&self, host: &str) -> ServiceResult<()> { + sqlx::query!("DELETE FROM forms_websites WHERE hostname = ($1)", &host,) + .execute(&self.pool) + .await + .unwrap(); + // res.map_err(map_register_err)?; + Ok(()) + } +} + +pub async fn get_db(settings: &crate::settings::Settings) -> Database { + let pool_options = PgPoolOptions::new().max_connections(settings.database.pool); + ConnectionOptions::Fresh(Fresh { + pool_options, + url: settings.database.url.clone(), + disable_logging: !settings.debug, + }) + .connect() + .await + .unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::settings::Settings; + + #[actix_rt::test] + async fn db_works() { + let settings = Settings::new().unwrap(); + let pool_options = PgPoolOptions::new().max_connections(1); + let db = ConnectionOptions::Fresh(Fresh { + pool_options, + url: settings.database.url.clone(), + disable_logging: !settings.debug, + }) + .connect() + .await + .unwrap(); + assert!(db.ping().await); + + let urls = ["example.com", "example.com", "example.com", "example.net"]; + for url in urls.iter() { + db.delete_site(url).await.unwrap(); + // ensuring delete doesn't fail when record doesn't exist + db.delete_site(url).await.unwrap(); + println!("using {url}"); + db.add_site(url).await.unwrap(); + // ensuring add_site doesn't fail when record exists + db.add_site(url).await.unwrap(); + } + } +} diff --git a/src/settings.rs b/src/settings.rs index 9eecd71..a6b4ec1 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -18,8 +18,10 @@ use std::env; use std::path::Path; use config::{Config, ConfigError, Environment, File}; +use derive_more::Display; use log::warn; use serde::Deserialize; +use serde::Serialize; use url::Url; #[derive(Debug, Clone, Deserialize)] @@ -38,36 +40,36 @@ impl Server { } } -//#[derive(Deserialize, Serialize, Display, PartialEq, Clone, Debug)] -//#[serde(rename_all = "lowercase")] -//pub enum DBType { -// #[display(fmt = "postgres")] -// Postgres, -// #[display(fmt = "maria")] -// Maria, -//} -// -//impl DBType { -// fn from_url(url: &Url) -> Result { -// match url.scheme() { -// "mysql" => Ok(Self::Maria), -// "postgres" => Ok(Self::Postgres), -// _ => Err(ConfigError::Message("Unknown database type".into())), -// } -// } -//} -// -//#[derive(Debug, Clone, Deserialize)] -//pub struct Database { -// pub url: String, -// pub pool: u32, -// pub database_type: DBType, -//} +#[derive(Deserialize, Serialize, Display, Eq, PartialEq, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum DBType { + #[display(fmt = "postgres")] + Postgres, + // #[display(fmt = "maria")] + // Maria, +} + +impl DBType { + fn from_url(url: &Url) -> Result { + match url.scheme() { + // "mysql" => Ok(Self::Maria), + "postgres" => Ok(Self::Postgres), + _ => Err(ConfigError::Message("Unknown database type".into())), + } + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Database { + pub url: String, + pub pool: u32, + pub database_type: DBType, +} #[derive(Debug, Clone, Deserialize)] pub struct Settings { pub debug: bool, - // pub database: Database, + pub database: Database, pub server: Server, pub source_code: String, } @@ -102,23 +104,23 @@ impl Settings { Err(e) => warn!("couldn't interpret PORT: {}", e), } - // match env::var("DATABASE_URL") { - // Ok(val) => { - // let url = Url::parse(&val).expect("couldn't parse Database URL"); - // s.set("database.url", url.to_string()).unwrap(); - // let database_type = DBType::from_url(&url).unwrap(); - // s.set("database.database_type", database_type.to_string()) - // .unwrap(); - // } - // Err(e) => { - // set_database_url(&mut s); - // } - // } + match env::var("DATABASE_URL") { + Ok(val) => { + let url = Url::parse(&val).expect("couldn't parse Database URL"); + s.set("database.url", url.to_string()).unwrap(); + let database_type = DBType::from_url(&url).unwrap(); + s.set("database.database_type", database_type.to_string()) + .unwrap(); + } + Err(_e) => { + set_database_url(&mut s); + } + } - // setting default values - // #[cfg(test)] - // s.set("database.pool", 2.to_string()) - // .expect("Couldn't set database pool count"); + // // setting default values + // #[cfg(test)] + // s.set("database.pool", 2.to_string()) + // .expect("Couldn't set database pool count"); match s.try_into::() { Ok(val) => { @@ -129,6 +131,26 @@ impl Settings { } } +fn set_database_url(s: &mut Config) { + s.set( + "database.url", + format!( + r"postgres://{}:{}@{}:{}/{}", + s.get::("database.username") + .expect("Couldn't access database username"), + s.get::("database.password") + .expect("Couldn't access database password"), + s.get::("database.hostname") + .expect("Couldn't access database hostname"), + s.get::("database.port") + .expect("Couldn't access database port"), + s.get::("database.name") + .expect("Couldn't access database name") + ), + ) + .expect("Couldn't set database url"); +} + #[cfg(not(tarpaulin_include))] fn check_url(s: &Config) { let url = s