use crate::ini;
use anyhow::Result;
use std::path::PathBuf;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Via {
pub host: String,
pub target: String,
pub port: String,
pub local: String,
}
impl Via {
pub fn spec(&self) -> String {
format!("{}:{}:{}", self.local, self.target, self.port)
}
pub fn command(&self) -> String {
format!("ssh -N -L {} {}", self.spec(), self.host)
}
}
pub fn store_path() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".config"))
.join("easysql")
.join("vias")
}
fn load_from(path: &std::path::Path) -> Vec<(String, Via)> {
ini::read(path)
.into_iter()
.filter_map(|s| {
let via = Via {
host: s.get("host")?.to_string(),
target: s.get("target")?.to_string(),
port: s.get("port")?.to_string(),
local: s.get("local")?.to_string(),
};
(!via.host.is_empty() && !via.target.is_empty()).then_some((s.name, via))
})
.collect()
}
pub fn get(key: &str) -> Option<Via> {
get_in(&store_path(), key)
}
pub fn get_in(path: &std::path::Path, key: &str) -> Option<Via> {
load_from(path)
.into_iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v)
}
pub fn set(key: &str, via: &Via) -> Result<()> {
set_in(&store_path(), key, via)
}
pub fn set_in(path: &std::path::Path, key: &str, via: &Via) -> Result<()> {
ini::upsert(
path,
Some(key),
key,
&[
("host".to_string(), via.host.clone()),
("target".to_string(), via.target.clone()),
("port".to_string(), via.port.clone()),
("local".to_string(), via.local.clone()),
],
)
}
pub fn rename(old_key: &str, new_key: &str) -> Result<()> {
if old_key == new_key {
return Ok(());
}
let Some(via) = get(old_key) else {
return Ok(());
};
set(new_key, &via)?;
remove(old_key)
}
pub fn remove(key: &str) -> Result<()> {
remove_in(&store_path(), key)
}
pub fn remove_in(path: &std::path::Path, key: &str) -> Result<()> {
if get_in(path, key).is_none() {
return Ok(());
}
ini::remove(path, key)
}
pub fn points_at_it(c: &crate::engines::Conn, via: &Via) -> bool {
let loopback = matches!(c.host.as_str(), "127.0.0.1" | "localhost" | "::1");
loopback && c.port_or_default() == via.local
}
pub fn ensure(key: &str) -> Option<Result<String>> {
let via = get(key)?;
if crate::tunnels::carrying(&via.local).is_some() {
return None;
}
Some(match crate::tunnels::open('L', &via.spec(), &via.host) {
Ok(t) => Ok(format!(
"reopened the tunnel through {} (pid {})",
via.host, t.pid
)),
Err(e) => Err(e),
})
}
pub fn owner_of(local: &str) -> Option<String> {
load_from(&store_path())
.into_iter()
.find(|(_, v)| v.local == local)
.map(|(key, _)| key)
}
pub fn all() -> Vec<(String, Via)> {
load_from(&store_path())
}
pub fn default_host(key: &str) -> Option<String> {
let all = all();
if let Some((_, v)) = all.iter().find(|(k, _)| k == key) {
return Some(v.host.clone());
}
let mut hosts: Vec<&str> = all.iter().map(|(_, v)| v.host.as_str()).collect();
hosts.sort_unstable();
hosts.dedup();
match hosts.as_slice() {
[only] => Some((*only).to_string()),
_ => None,
}
}