feat(config): persist database URL to ~/.config/secrets/config.toml
- Add 'secrets config set-db/show/path' subcommands - Remove dotenvy and DATABASE_URL env var support - Config file created with 0600 permission - Bump version to 0.3.0 Made-with: Cursor
This commit is contained in:
42
Cargo.lock
generated
42
Cargo.lock
generated
@@ -305,6 +305,27 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
@@ -892,6 +913,12 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
@@ -1075,6 +1102,17 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -1163,12 +1201,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "secrets"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dotenvy",
|
||||
"dirs",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "secrets"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.102"
|
||||
chrono = { version = "0.4.44", features = ["serde"] }
|
||||
clap = { version = "4.6.0", features = ["derive", "env"] }
|
||||
dotenvy = "0.15.7"
|
||||
dirs = "6.0.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "json", "chrono"] }
|
||||
|
||||
54
src/commands/config.rs
Normal file
54
src/commands/config.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::config::{self, Config, config_path};
|
||||
use anyhow::Result;
|
||||
|
||||
pub enum ConfigAction {
|
||||
SetDb { url: String },
|
||||
Show,
|
||||
Path,
|
||||
}
|
||||
|
||||
pub async fn run(action: ConfigAction) -> Result<()> {
|
||||
match action {
|
||||
ConfigAction::SetDb { url } => {
|
||||
let cfg = Config {
|
||||
database_url: Some(url.clone()),
|
||||
};
|
||||
config::save_config(&cfg)?;
|
||||
println!("✓ 数据库连接串已保存到: {}", config_path().display());
|
||||
println!(" {}", mask_password(&url));
|
||||
}
|
||||
ConfigAction::Show => {
|
||||
let cfg = config::load_config()?;
|
||||
match cfg.database_url {
|
||||
Some(url) => {
|
||||
println!("database_url = {}", mask_password(&url));
|
||||
println!("配置文件: {}", config_path().display());
|
||||
}
|
||||
None => {
|
||||
println!("未配置数据库连接串。");
|
||||
println!("请运行:secrets config set-db <DATABASE_URL>");
|
||||
}
|
||||
}
|
||||
}
|
||||
ConfigAction::Path => {
|
||||
println!("{}", config_path().display());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将 postgres://user:password@host/db 中的密码替换为 ***
|
||||
fn mask_password(url: &str) -> String {
|
||||
if let Some(at_pos) = url.rfind('@')
|
||||
&& let Some(scheme_end) = url.find("://")
|
||||
{
|
||||
let prefix = &url[..scheme_end + 3];
|
||||
let credentials = &url[scheme_end + 3..at_pos];
|
||||
let rest = &url[at_pos..];
|
||||
if let Some(colon_pos) = credentials.find(':') {
|
||||
let user = &credentials[..colon_pos];
|
||||
return format!("{}{}:***{}", prefix, user, rest);
|
||||
}
|
||||
}
|
||||
url.to_string()
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod add;
|
||||
pub mod config;
|
||||
pub mod delete;
|
||||
pub mod search;
|
||||
pub mod update;
|
||||
|
||||
70
src/config.rs
Normal file
70
src/config.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Config {
|
||||
pub database_url: Option<String>,
|
||||
}
|
||||
|
||||
pub fn config_dir() -> PathBuf {
|
||||
dirs::config_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~/.config"))
|
||||
.join("secrets")
|
||||
}
|
||||
|
||||
pub fn config_path() -> PathBuf {
|
||||
config_dir().join("config.toml")
|
||||
}
|
||||
|
||||
pub fn load_config() -> Result<Config> {
|
||||
let path = config_path();
|
||||
if !path.exists() {
|
||||
return Ok(Config::default());
|
||||
}
|
||||
let content = fs::read_to_string(&path)
|
||||
.with_context(|| format!("读取配置文件失败: {}", path.display()))?;
|
||||
let config: Config = toml::from_str(&content)
|
||||
.with_context(|| format!("解析配置文件失败: {}", path.display()))?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn save_config(config: &Config) -> Result<()> {
|
||||
let dir = config_dir();
|
||||
fs::create_dir_all(&dir).with_context(|| format!("创建配置目录失败: {}", dir.display()))?;
|
||||
|
||||
let path = config_path();
|
||||
let content = toml::to_string_pretty(config).context("序列化配置失败")?;
|
||||
fs::write(&path, &content).with_context(|| format!("写入配置文件失败: {}", path.display()))?;
|
||||
|
||||
// 设置文件权限为 0600(仅所有者读写)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let perms = fs::Permissions::from_mode(0o600);
|
||||
fs::set_permissions(&path, perms)
|
||||
.with_context(|| format!("设置文件权限失败: {}", path.display()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 按优先级解析数据库连接串:
|
||||
/// 1. --db-url CLI 参数(非空时使用)
|
||||
/// 2. ~/.config/secrets/config.toml 中的 database_url
|
||||
/// 3. 报错并提示用户配置
|
||||
pub fn resolve_db_url(cli_db_url: &str) -> Result<String> {
|
||||
if !cli_db_url.is_empty() {
|
||||
return Ok(cli_db_url.to_string());
|
||||
}
|
||||
|
||||
let config = load_config()?;
|
||||
if let Some(url) = config.database_url
|
||||
&& !url.is_empty()
|
||||
{
|
||||
return Ok(url);
|
||||
}
|
||||
|
||||
anyhow::bail!("数据库未配置。请先运行:\n\n secrets config set-db <DATABASE_URL>\n")
|
||||
}
|
||||
47
src/main.rs
47
src/main.rs
@@ -1,10 +1,10 @@
|
||||
mod commands;
|
||||
mod config;
|
||||
mod db;
|
||||
mod models;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use dotenvy::dotenv;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
@@ -13,8 +13,8 @@ use dotenvy::dotenv;
|
||||
about = "Secrets & config manager backed by PostgreSQL"
|
||||
)]
|
||||
struct Cli {
|
||||
/// Database URL (or set DATABASE_URL env var)
|
||||
#[arg(long, env = "DATABASE_URL", global = true, default_value = "")]
|
||||
/// Database URL, overrides saved config (one-time override)
|
||||
#[arg(long, global = true, default_value = "")]
|
||||
db_url: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
@@ -107,22 +107,44 @@ enum Commands {
|
||||
#[arg(long = "remove-secret")]
|
||||
remove_secrets: Vec<String>,
|
||||
},
|
||||
|
||||
/// Manage CLI configuration (database connection, etc.)
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
action: ConfigAction,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ConfigAction {
|
||||
/// Save database URL to config file (~/.config/secrets/config.toml)
|
||||
SetDb {
|
||||
/// PostgreSQL connection string
|
||||
url: String,
|
||||
},
|
||||
/// Show current configuration
|
||||
Show,
|
||||
/// Print path to config file
|
||||
Path,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
dotenv().ok();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
let db_url = if cli.db_url.is_empty() {
|
||||
std::env::var("DATABASE_URL").map_err(|_| {
|
||||
anyhow::anyhow!("DATABASE_URL not set. Use --db-url or set DATABASE_URL env var.")
|
||||
})?
|
||||
} else {
|
||||
cli.db_url.clone()
|
||||
};
|
||||
// config 子命令不需要数据库连接,提前处理
|
||||
if let Commands::Config { action } = &cli.command {
|
||||
let cmd_action = match action {
|
||||
ConfigAction::SetDb { url } => {
|
||||
commands::config::ConfigAction::SetDb { url: url.clone() }
|
||||
}
|
||||
ConfigAction::Show => commands::config::ConfigAction::Show,
|
||||
ConfigAction::Path => commands::config::ConfigAction::Path,
|
||||
};
|
||||
return commands::config::run(cmd_action).await;
|
||||
}
|
||||
|
||||
let db_url = config::resolve_db_url(&cli.db_url)?;
|
||||
let pool = db::create_pool(&db_url).await?;
|
||||
db::migrate(&pool).await?;
|
||||
|
||||
@@ -188,6 +210,7 @@ async fn main() -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Commands::Config { .. } => unreachable!(),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user