optional sqlx

This commit is contained in:
Robin Appelman 2021-05-03 18:30:41 +02:00
commit 06de88ec21
4 changed files with 294 additions and 96 deletions

View file

@ -17,6 +17,7 @@ jobs:
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
with: with:
command: check command: check
args: --all-features
test: test:
name: Tests name: Tests
@ -33,6 +34,7 @@ jobs:
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
with: with:
command: test command: test
args: --all-features
msrv: msrv:
name: Check MSRV name: Check MSRV

View file

@ -9,11 +9,13 @@ repository = "https://github.com/icewind1991/nextcloud-config-parser"
documentation = "https://docs.rs/nextcloud-config-parser" documentation = "https://docs.rs/nextcloud-config-parser"
[dependencies] [dependencies]
redis = { version = "0.20", features = ["tokio-comp", "aio", "cluster"] } redis = { version = "0.20" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1" thiserror = "1"
warp = "0.3"
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "any", "macros", "mysql", "sqlite", "postgres"] }
php-literal-parser = { version = "0.2", default-features = false } php-literal-parser = { version = "0.2", default-features = false }
parse-display = "0.4" sqlx = { version = "0.5", features = ["any", "mysql", "sqlite", "postgres"], optional = true }
[dev-dependencies]
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "any", "mysql", "sqlite", "postgres"] }
[features]
db-sqlx = ["sqlx"]

View file

@ -1,7 +1,6 @@
mod nc; mod nc;
use redis::{ConnectionAddr, ConnectionInfo}; use redis::{ConnectionAddr, ConnectionInfo};
use sqlx::any::AnyConnectOptions;
use std::iter::once; use std::iter::once;
use std::path::PathBuf; use std::path::PathBuf;
use thiserror::Error; use thiserror::Error;
@ -10,7 +9,7 @@ pub use nc::parse;
#[derive(Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
pub database: AnyConnectOptions, pub database: Database,
pub database_prefix: String, pub database_prefix: String,
pub redis: RedisConfig, pub redis: RedisConfig,
pub nextcloud_url: String, pub nextcloud_url: String,
@ -76,8 +75,8 @@ pub enum Error {
NotAConfig(#[from] NotAConfigError), NotAConfig(#[from] NotAConfigError),
#[error("Failed to read config file")] #[error("Failed to read config file")]
ReadFailed(std::io::Error, PathBuf), ReadFailed(std::io::Error, PathBuf),
#[error("unsupported database type {0}")] #[error("invalid database configuration: {0}")]
UnsupportedDb(String), InvalidDb(#[from] DbError),
#[error("no database configuration")] #[error("no database configuration")]
NoDb, NoDb,
#[error("Invalid redis configuration")] #[error("Invalid redis configuration")]
@ -86,6 +85,18 @@ pub enum Error {
NoUrl, NoUrl,
} }
#[derive(Debug, Error)]
pub enum DbError {
#[error("unsupported database type {0}")]
Unsupported(String),
#[error("no username set")]
NoUsername,
#[error("no password set")]
NoPassword,
#[error("no database name")]
NoName,
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum NotAConfigError { pub enum NotAConfigError {
#[error("$CONFIG not found in file")] #[error("$CONFIG not found in file")]
@ -93,3 +104,90 @@ pub enum NotAConfigError {
#[error("$CONFIG is not an array")] #[error("$CONFIG is not an array")]
NotAnArray(PathBuf), NotAnArray(PathBuf),
} }
#[derive(Debug)]
pub enum Database {
Sqlite {
database: PathBuf,
},
MySql {
database: String,
username: String,
password: String,
connect: DbConnect,
disable_ssl: bool,
},
Postgres {
database: String,
username: String,
password: String,
connect: DbConnect,
},
}
#[derive(Debug)]
pub enum DbConnect {
Tcp { host: String, port: u16 },
Socket(PathBuf),
}
#[cfg(feature = "db-sqlx")]
impl From<Database> for sqlx::any::AnyConnectOptions {
fn from(cfg: Database) -> Self {
use sqlx::{
mysql::{MySqlConnectOptions, MySqlSslMode},
postgres::PgConnectOptions,
sqlite::SqliteConnectOptions,
};
match cfg {
Database::Sqlite { database } => {
SqliteConnectOptions::default().filename(database).into()
}
Database::MySql {
database,
username,
password,
connect,
disable_ssl,
} => {
let mut options = MySqlConnectOptions::default()
.database(&database)
.username(&username)
.password(&password);
if disable_ssl {
options = options.ssl_mode(MySqlSslMode::Disabled);
}
match connect {
DbConnect::Socket(socket) => {
options = options.socket(socket);
}
DbConnect::Tcp { host, port } => {
options = options.host(&host).port(port);
}
}
options.into()
}
Database::Postgres {
database,
username,
password,
connect,
} => {
let mut options = PgConnectOptions::default()
.database(&database)
.username(&username)
.password(&password);
match connect {
DbConnect::Socket(socket) => {
options = options.socket(socket);
}
DbConnect::Tcp { host, port } => {
options = options.host(&host).port(port);
}
}
options.into()
}
}
}
}

290
src/nc.rs
View file

@ -1,11 +1,8 @@
use crate::{Config, Error, NotAConfigError, RedisConfig, Result}; use crate::{Config, Database, DbConnect, DbError, Error, NotAConfigError, RedisConfig, Result};
use php_literal_parser::Value; use php_literal_parser::Value;
use redis::{ConnectionAddr, ConnectionInfo}; use redis::{ConnectionAddr, ConnectionInfo};
use sqlx::any::AnyConnectOptions;
use sqlx::mysql::{MySqlConnectOptions, MySqlSslMode};
use sqlx::postgres::PgConnectOptions;
use sqlx::sqlite::SqliteConnectOptions;
use std::collections::HashMap; use std::collections::HashMap;
use std::net::IpAddr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
@ -106,72 +103,74 @@ pub fn parse(path: impl AsRef<Path>) -> Result<Config> {
}) })
} }
fn parse_db_options(parsed: &Value) -> Result<AnyConnectOptions> { fn parse_db_options(parsed: &Value) -> Result<Database> {
match parsed["dbtype"].as_str() { match parsed["dbtype"].as_str() {
Some("mysql") => { Some("mysql") => {
let mut options = MySqlConnectOptions::new(); let username = parsed["dbuser"].as_str().ok_or(DbError::NoUsername)?;
if let Some(username) = parsed["dbuser"].as_str() { let password = parsed["dbpassword"].as_str().ok_or(DbError::NoPassword)?;
options = options.username(username);
}
if let Some(password) = parsed["dbpassword"].as_str() {
options = options.password(password);
}
let socket_addr1 = PathBuf::from("/var/run/mysqld/mysqld.sock"); let socket_addr1 = PathBuf::from("/var/run/mysqld/mysqld.sock");
let socket_addr2 = PathBuf::from("/tmp/mysql.sock"); let socket_addr2 = PathBuf::from("/tmp/mysql.sock");
let socket_addr3 = PathBuf::from("/run/mysql/mysql.sock"); let socket_addr3 = PathBuf::from("/run/mysql/mysql.sock");
match split_host(parsed["dbhost"].as_str().unwrap_or_default()) { let (mut connect, disable_ssl) =
("localhost", None, None) if socket_addr1.exists() => { match split_host(parsed["dbhost"].as_str().unwrap_or_default()) {
options = options.socket(socket_addr1); ("localhost", None, None) if socket_addr1.exists() => {
} (DbConnect::Socket(socket_addr1), false)
("localhost", None, None) if socket_addr2.exists() => {
options = options.socket(socket_addr2);
}
("localhost", None, None) if socket_addr3.exists() => {
options = options.socket(socket_addr3);
}
(addr, None, None) => {
options = options.host(addr);
if IpAddr::from_str(addr).is_ok() {
options = options.ssl_mode(MySqlSslMode::Disabled);
} }
} ("localhost", None, None) if socket_addr2.exists() => {
(addr, Some(port), None) => { (DbConnect::Socket(socket_addr2), false)
options = options.host(addr).port(port);
if IpAddr::from_str(addr).is_ok() {
options = options.ssl_mode(MySqlSslMode::Disabled);
} }
} ("localhost", None, None) if socket_addr3.exists() => {
(_, None, Some(socket)) => { (DbConnect::Socket(socket_addr3), false)
options = options.socket(socket); }
} (addr, None, None) => (
(_, Some(_), Some(_)) => { DbConnect::Tcp {
unreachable!() host: addr.into(),
} port: 3306,
} },
IpAddr::from_str(addr).is_ok(),
),
(addr, Some(port), None) => (
DbConnect::Tcp {
host: addr.into(),
port,
},
IpAddr::from_str(addr).is_ok(),
),
(_, None, Some(socket)) => (DbConnect::Socket(socket.into()), false),
(_, Some(_), Some(_)) => {
unreachable!()
}
};
if let Some(port) = parsed["dbport"].clone().into_int() { if let Some(port) = parsed["dbport"].clone().into_int() {
options = options.port(port as u16); if let DbConnect::Tcp {
} port: connect_port, ..
if let Some(name) = parsed["dbname"].as_str() { } = &mut connect
options = options.database(name); {
*connect_port = port as u16;
}
} }
let database = parsed["dbname"].as_str().ok_or(DbError::NoName)?;
Ok(options.into()) Ok(Database::MySql {
database: database.into(),
username: username.into(),
password: password.into(),
connect,
disable_ssl,
})
} }
Some("pgsql") => { Some("pgsql") => {
let mut options = PgConnectOptions::new(); let username = parsed["dbuser"].as_str().ok_or(DbError::NoUsername)?;
if let Some(username) = parsed["dbuser"].as_str() { let password = parsed["dbpassword"].as_str().ok_or(DbError::NoPassword)?;
options = options.username(username); let mut connect = match split_host(parsed["dbhost"].as_str().unwrap_or_default()) {
} (addr, None, None) => DbConnect::Tcp {
if let Some(password) = parsed["dbpassword"].as_str() { host: addr.into(),
options = options.password(password); port: 5432,
} },
match split_host(parsed["dbhost"].as_str().unwrap_or_default()) { (addr, Some(port), None) => DbConnect::Tcp {
(addr, None, None) => { host: addr.into(),
options = options.host(addr); port,
} },
(addr, Some(port), None) => {
options = options.host(addr).port(port);
}
(_, None, Some(socket)) => { (_, None, Some(socket)) => {
let mut socket_path = Path::new(socket); let mut socket_path = Path::new(socket);
@ -183,32 +182,37 @@ fn parse_db_options(parsed: &Value) -> Result<AnyConnectOptions> {
{ {
socket_path = socket_path.parent().unwrap(); socket_path = socket_path.parent().unwrap();
} }
options = options.socket(socket_path); DbConnect::Socket(socket_path.into())
} }
(_, Some(_), Some(_)) => { (_, Some(_), Some(_)) => {
unreachable!() unreachable!()
} }
} };
if let Some(port) = parsed["dbport"].clone().into_int() { if let Some(port) = parsed["dbport"].clone().into_int() {
options = options.port(port as u16); if let DbConnect::Tcp {
port: connect_port, ..
} = &mut connect
{
*connect_port = port as u16;
}
} }
if let Some(name) = parsed["dbname"].as_str() { let database = parsed["dbname"].as_str().ok_or(DbError::NoName)?;
options = options.database(name);
} Ok(Database::Postgres {
Ok(options.into()) database: database.into(),
username: username.into(),
password: password.into(),
connect,
})
} }
Some("sqlite3") => { Some("sqlite3") => {
let mut options = SqliteConnectOptions::new(); let data_dir = parsed["datadirectory"].as_str().ok_or(DbError::NoName)?;
if let Some(data_dir) = parsed["datadirectory"].as_str() { let db_name = parsed["dbname"].as_str().ok_or(DbError::NoName)?;
let db_name = parsed["dbname"] Ok(Database::Sqlite {
.clone() database: format!("{}/{}.db", data_dir, db_name).into(),
.into_string() })
.unwrap_or_else(|| String::from("owncloud"));
options = options.filename(format!("{}/{}.db", data_dir, db_name));
}
Ok(options.into())
} }
Some(ty) => Err(Error::UnsupportedDb(ty.into())), Some(ty) => Err(Error::InvalidDb(DbError::Unsupported(ty.into()))),
None => Err(Error::NoDb), None => Err(Error::NoDb),
} }
} }
@ -304,13 +308,15 @@ fn test_redis_empty_password_none() {
#[cfg(test)] #[cfg(test)]
#[track_caller] #[track_caller]
fn assert_debug_equal<T: Debug, U: Debug>(a: T, b: U) { fn assert_debug_equal<T: Debug>(a: T, b: T) {
assert_eq!(format!("{:?}", a), format!("{:?}", b),); assert_eq!(format!("{:?}", a), format!("{:?}", b),);
} }
#[cfg(test)]
#[allow(unused_imports)]
use sqlx::{any::AnyConnectOptions, postgres::PgConnectOptions};
#[cfg(test)] #[cfg(test)]
use std::fmt::Debug; use std::fmt::Debug;
use std::net::IpAddr;
#[cfg(test)] #[cfg(test)]
fn config_from_file(path: &str) -> Config { fn config_from_file(path: &str) -> Config {
@ -322,12 +328,26 @@ fn test_parse_config_basic() {
let config = config_from_file("tests/configs/basic.php"); let config = config_from_file("tests/configs/basic.php");
assert_eq!("https://cloud.example.com", config.nextcloud_url); assert_eq!("https://cloud.example.com", config.nextcloud_url);
assert_eq!("oc_", config.database_prefix); assert_eq!("oc_", config.database_prefix);
assert_debug_equal(
&Database::MySql {
database: "nextcloud".to_string(),
username: "nextcloud".to_string(),
password: "secret".to_string(),
connect: DbConnect::Tcp {
host: "127.0.0.1".to_string(),
port: 3306,
},
disable_ssl: true,
},
&config.database,
);
#[cfg(feature = "db-sqlx")]
assert_debug_equal( assert_debug_equal(
AnyConnectOptions::from_str( AnyConnectOptions::from_str(
"mysql://nextcloud:secret@127.0.0.1/nextcloud?ssl-mode=disabled", "mysql://nextcloud:secret@127.0.0.1/nextcloud?ssl-mode=disabled",
) )
.unwrap(), .unwrap(),
config.database, config.database.into(),
); );
assert_debug_equal( assert_debug_equal(
RedisConfig::Single(ConnectionInfo::from_str("redis://127.0.0.1").unwrap()), RedisConfig::Single(ConnectionInfo::from_str("redis://127.0.0.1").unwrap()),
@ -373,12 +393,26 @@ fn test_parse_comment_whitespace() {
let config = config_from_file("tests/configs/comment_whitespace.php"); let config = config_from_file("tests/configs/comment_whitespace.php");
assert_eq!("https://cloud.example.com", config.nextcloud_url); assert_eq!("https://cloud.example.com", config.nextcloud_url);
assert_eq!("oc_", config.database_prefix); assert_eq!("oc_", config.database_prefix);
assert_debug_equal(
&Database::MySql {
database: "nextcloud".to_string(),
username: "nextcloud".to_string(),
password: "secret".to_string(),
connect: DbConnect::Tcp {
host: "127.0.0.1".to_string(),
port: 3306,
},
disable_ssl: true,
},
&config.database,
);
#[cfg(feature = "db-sqlx")]
assert_debug_equal( assert_debug_equal(
AnyConnectOptions::from_str( AnyConnectOptions::from_str(
"mysql://nextcloud:secret@127.0.0.1/nextcloud?ssl-mode=disabled", "mysql://nextcloud:secret@127.0.0.1/nextcloud?ssl-mode=disabled",
) )
.unwrap(), .unwrap(),
config.database, config.database.into(),
); );
assert_debug_equal( assert_debug_equal(
RedisConfig::Single(ConnectionInfo::from_str("redis://127.0.0.1").unwrap()), RedisConfig::Single(ConnectionInfo::from_str("redis://127.0.0.1").unwrap()),
@ -389,12 +423,26 @@ fn test_parse_comment_whitespace() {
#[test] #[test]
fn test_parse_port_in_host() { fn test_parse_port_in_host() {
let config = config_from_file("tests/configs/port_in_host.php"); let config = config_from_file("tests/configs/port_in_host.php");
assert_debug_equal(
&Database::MySql {
database: "nextcloud".to_string(),
username: "nextcloud".to_string(),
password: "secret".to_string(),
connect: DbConnect::Tcp {
host: "127.0.0.1".to_string(),
port: 1234,
},
disable_ssl: true,
},
&config.database,
);
#[cfg(feature = "db-sqlx")]
assert_debug_equal( assert_debug_equal(
AnyConnectOptions::from_str( AnyConnectOptions::from_str(
"mysql://nextcloud:secret@127.0.0.1:1234/nextcloud?ssl-mode=disabled", "mysql://nextcloud:secret@127.0.0.1:1234/nextcloud?ssl-mode=disabled",
) )
.unwrap(), .unwrap(),
config.database, config.database.into(),
); );
} }
@ -402,20 +450,15 @@ fn test_parse_port_in_host() {
fn test_parse_postgres_socket() { fn test_parse_postgres_socket() {
let config = config_from_file("tests/configs/postgres_socket.php"); let config = config_from_file("tests/configs/postgres_socket.php");
assert_debug_equal( assert_debug_equal(
AnyConnectOptions::from( &Database::Postgres {
PgConnectOptions::new() database: "nextcloud".to_string(),
.socket("/var/run/postgresql") username: "redacted".to_string(),
.username("redacted") password: "redacted".to_string(),
.password("redacted") connect: DbConnect::Socket("/var/run/postgresql".into()),
.database("nextcloud"), },
), &config.database,
config.database,
); );
} #[cfg(feature = "db-sqlx")]
#[test]
fn test_parse_postgres_socket_folder() {
let config = config_from_file("tests/configs/postgres_socket_folder.php");
assert_debug_equal( assert_debug_equal(
AnyConnectOptions::from( AnyConnectOptions::from(
PgConnectOptions::new() PgConnectOptions::new()
@ -424,7 +467,32 @@ fn test_parse_postgres_socket_folder() {
.password("redacted") .password("redacted")
.database("nextcloud"), .database("nextcloud"),
), ),
config.database, config.database.into(),
);
}
#[test]
fn test_parse_postgres_socket_folder() {
let config = config_from_file("tests/configs/postgres_socket_folder.php");
assert_debug_equal(
&Database::Postgres {
database: "nextcloud".to_string(),
username: "redacted".to_string(),
password: "redacted".to_string(),
connect: DbConnect::Socket("/var/run/postgresql".into()),
},
&config.database,
);
#[cfg(feature = "db-sqlx")]
assert_debug_equal(
AnyConnectOptions::from(
PgConnectOptions::new()
.socket("/var/run/postgresql")
.username("redacted")
.password("redacted")
.database("nextcloud"),
),
config.database.into(),
); );
} }
@ -451,12 +519,26 @@ fn test_parse_config_multiple() {
let config = config_from_file("tests/configs/multiple/config.php"); let config = config_from_file("tests/configs/multiple/config.php");
assert_eq!("https://cloud.example.com", config.nextcloud_url); assert_eq!("https://cloud.example.com", config.nextcloud_url);
assert_eq!("oc_", config.database_prefix); assert_eq!("oc_", config.database_prefix);
assert_debug_equal(
&Database::MySql {
database: "nextcloud".to_string(),
username: "nextcloud".to_string(),
password: "secret".to_string(),
connect: DbConnect::Tcp {
host: "127.0.0.1".to_string(),
port: 3306,
},
disable_ssl: true,
},
&config.database,
);
#[cfg(feature = "db-sqlx")]
assert_debug_equal( assert_debug_equal(
AnyConnectOptions::from_str( AnyConnectOptions::from_str(
"mysql://nextcloud:secret@127.0.0.1/nextcloud?ssl-mode=disabled", "mysql://nextcloud:secret@127.0.0.1/nextcloud?ssl-mode=disabled",
) )
.unwrap(), .unwrap(),
config.database, config.database.into(),
); );
assert_debug_equal( assert_debug_equal(
RedisConfig::Single(ConnectionInfo::from_str("redis://127.0.0.1").unwrap()), RedisConfig::Single(ConnectionInfo::from_str("redis://127.0.0.1").unwrap()),
@ -467,11 +549,25 @@ fn test_parse_config_multiple() {
#[test] #[test]
fn test_parse_config_mysql_fqdn() { fn test_parse_config_mysql_fqdn() {
let config = config_from_file("tests/configs/mysql_fqdn.php"); let config = config_from_file("tests/configs/mysql_fqdn.php");
assert_debug_equal(
&Database::MySql {
database: "nextcloud".to_string(),
username: "nextcloud".to_string(),
password: "secret".to_string(),
connect: DbConnect::Tcp {
host: "db.example.com".to_string(),
port: 3306,
},
disable_ssl: false,
},
&config.database,
);
#[cfg(feature = "db-sqlx")]
assert_debug_equal( assert_debug_equal(
AnyConnectOptions::from_str( AnyConnectOptions::from_str(
"mysql://nextcloud:secret@db.example.com/nextcloud?ssl-mode=preferred", "mysql://nextcloud:secret@db.example.com/nextcloud?ssl-mode=preferred",
) )
.unwrap(), .unwrap(),
config.database, config.database.into(),
); );
} }