use super::{Conn, Engine, NewConn};
use crate::ini;
use anyhow::Result;
use std::path::PathBuf;
const KNOWN: [&str; 5] = ["host", "port", "database", "user", "password"];
const PREFIX: &str = "client";
pub fn cnf_path() -> PathBuf {
dirs::home_dir().unwrap_or_default().join(".my.cnf")
}
pub fn group_of(name: &str) -> String {
format!("{PREFIX}{name}")
}
pub fn name_of(group: &str) -> Option<String> {
let rest = group.strip_prefix(PREFIX)?;
(!rest.is_empty()).then(|| rest.to_string())
}
pub fn list() -> Vec<Conn> {
list_in(&cnf_path())
}
pub fn list_in(path: &std::path::Path) -> Vec<Conn> {
ini::read(path)
.into_iter()
.filter_map(|s| {
let name = name_of(&s.name)?;
Some(Conn {
engine: Engine::MySql,
name,
host: s.get("host").unwrap_or_default().to_string(),
port: s.get("port").unwrap_or_default().to_string(),
database: s.get("database").unwrap_or_default().to_string(),
user: s.get("user").unwrap_or_default().to_string(),
extra: s.rest(&KNOWN),
})
})
.collect()
}
pub fn has_password(name: &str) -> bool {
ini::read(&cnf_path())
.iter()
.any(|s| s.name == group_of(name) && s.get("password").is_some())
}
pub fn save(original: Option<&str>, nc: &NewConn) -> Result<()> {
let path = cnf_path();
let group = group_of(nc.name.trim());
let from = original.map(group_of);
let mut keys: Vec<(String, String)> = Vec::new();
for (k, v) in [
("host", &nc.host),
("port", &nc.port),
("database", &nc.database),
("user", &nc.user),
] {
let v = v.trim();
if !v.is_empty() {
keys.push((k.to_string(), v.to_string()));
}
}
let existing = from.as_deref().unwrap_or(&group);
if let Some(sec) = ini::read(&path).into_iter().find(|s| s.name == existing)
&& let Some(pw) = sec.get("password")
{
keys.push(("password".to_string(), pw.to_string()));
}
keys.extend(nc.extra.iter().cloned());
ini::upsert(&path, from.as_deref(), &group, &keys)
}
pub fn delete(name: &str) -> Result<()> {
ini::remove(&cnf_path(), &group_of(name))
}
pub fn set_password(name: &str, password: Option<&str>) -> Result<()> {
ini::set_key(&cnf_path(), &group_of(name), "password", password)
}
pub fn probe_argv(c: &Conn, s: &crate::settings::Settings) -> (Vec<String>, Vec<(String, String)>) {
let mut argv = c.connect_argv(s);
argv.push(format!("--connect-timeout={}", s.probe_timeout));
argv.push("-e".into());
argv.push("select 1".into());
(argv, Vec::new())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn client_and_foreign_groups_are_never_ours() {
assert_eq!(name_of("client"), None);
assert_eq!(name_of("mysqld"), None);
assert_eq!(name_of("mysqldump"), None);
assert_eq!(name_of(&group_of("prod")), Some("prod".to_string()));
}
#[test]
fn listing_skips_every_group_that_is_not_ours() {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path =
std::env::temp_dir().join(format!("easysql-mycnf-{}-{stamp}", std::process::id()));
fs::write(
&path,
"\
[client]
host=shared.example.com
[mysqld]
port=3306
[clientprod]
host=db.example.com
port=3306
database=app
user=me
connect_timeout=5
",
)
.unwrap();
let conns = list_in(&path);
let _ = fs::remove_file(&path);
assert_eq!(conns.len(), 1, "only [clientNAME] groups are connections");
assert_eq!(conns[0].name, "prod");
assert_eq!(conns[0].host, "db.example.com");
assert_eq!(
conns[0].extra,
vec![("connect_timeout".to_string(), "5".to_string())]
);
}
}