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",
|
"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]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -892,6 +913,12 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
@@ -1075,6 +1102,17 @@ dependencies = [
|
|||||||
"bitflags",
|
"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]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -1163,12 +1201,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "secrets"
|
name = "secrets"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"dotenvy",
|
"dirs",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "secrets"
|
name = "secrets"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.102"
|
||||||
chrono = { version = "0.4.44", features = ["serde"] }
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
clap = { version = "4.6.0", features = ["derive", "env"] }
|
clap = { version = "4.6.0", features = ["derive", "env"] }
|
||||||
dotenvy = "0.15.7"
|
dirs = "6.0.0"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "json", "chrono"] }
|
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 add;
|
||||||
|
pub mod config;
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod update;
|
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 commands;
|
||||||
|
mod config;
|
||||||
mod db;
|
mod db;
|
||||||
mod models;
|
mod models;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use dotenvy::dotenv;
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
@@ -13,8 +13,8 @@ use dotenvy::dotenv;
|
|||||||
about = "Secrets & config manager backed by PostgreSQL"
|
about = "Secrets & config manager backed by PostgreSQL"
|
||||||
)]
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// Database URL (or set DATABASE_URL env var)
|
/// Database URL, overrides saved config (one-time override)
|
||||||
#[arg(long, env = "DATABASE_URL", global = true, default_value = "")]
|
#[arg(long, global = true, default_value = "")]
|
||||||
db_url: String,
|
db_url: String,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
@@ -107,22 +107,44 @@ enum Commands {
|
|||||||
#[arg(long = "remove-secret")]
|
#[arg(long = "remove-secret")]
|
||||||
remove_secrets: Vec<String>,
|
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]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
dotenv().ok();
|
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
let db_url = if cli.db_url.is_empty() {
|
// config 子命令不需要数据库连接,提前处理
|
||||||
std::env::var("DATABASE_URL").map_err(|_| {
|
if let Commands::Config { action } = &cli.command {
|
||||||
anyhow::anyhow!("DATABASE_URL not set. Use --db-url or set DATABASE_URL env var.")
|
let cmd_action = match action {
|
||||||
})?
|
ConfigAction::SetDb { url } => {
|
||||||
} else {
|
commands::config::ConfigAction::SetDb { url: url.clone() }
|
||||||
cli.db_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?;
|
let pool = db::create_pool(&db_url).await?;
|
||||||
db::migrate(&pool).await?;
|
db::migrate(&pool).await?;
|
||||||
|
|
||||||
@@ -188,6 +210,7 @@ async fn main() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
Commands::Config { .. } => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user