feat: add config system with --config argument and priority-based configuration
Co-authored-by: aider (openai/andrew/openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
This commit is contained in:
@@ -50,6 +50,7 @@ utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
|||||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||||
uzers = "0.12.1"
|
uzers = "0.12.1"
|
||||||
which = "8.0.0"
|
which = "8.0.0"
|
||||||
|
xdg = "2.5.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.3.0"
|
tempfile = "3.3.0"
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ pub struct ItemArgs {
|
|||||||
*/
|
*/
|
||||||
#[derive(Parser, Debug, Default, Clone)]
|
#[derive(Parser, Debug, Default, Clone)]
|
||||||
pub struct OptionsArgs {
|
pub struct OptionsArgs {
|
||||||
|
#[arg(long, env("KEEP_CONFIG"))]
|
||||||
|
#[arg(help("Specify the configuration file to use"))]
|
||||||
|
pub config: Option<PathBuf>,
|
||||||
|
|
||||||
#[arg(long, env("KEEP_DIR"))]
|
#[arg(long, env("KEEP_DIR"))]
|
||||||
#[arg(help("Specify the directory to use for storage"))]
|
#[arg(help("Specify the directory to use for storage"))]
|
||||||
pub dir: Option<PathBuf>,
|
pub dir: Option<PathBuf>,
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::fs;
|
||||||
|
use anyhow::{Result, Context};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
|
pub struct Config {
|
||||||
|
pub dir: Option<PathBuf>,
|
||||||
|
pub list_format: Option<String>,
|
||||||
|
pub human_readable: Option<bool>,
|
||||||
|
pub server: Option<ServerConfig>,
|
||||||
|
pub compression_plugin: Option<CompressionPluginConfig>,
|
||||||
|
pub meta_plugins: Option<Vec<MetaPluginConfig>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
pub address: Option<String>,
|
||||||
|
pub port: Option<u16>,
|
||||||
|
pub password_file: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct CompressionPluginConfig {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct MetaPluginConfig {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Load configuration from a file
|
||||||
|
pub fn from_file(path: &PathBuf) -> Result<Self> {
|
||||||
|
debug!("CONFIG: Loading config from {:?}", path);
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
debug!("CONFIG: Config file does not exist, using defaults");
|
||||||
|
return Ok(Config::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("Failed to read config file: {:?}", path))?;
|
||||||
|
|
||||||
|
let config: Config = serde_yaml::from_str(&content)
|
||||||
|
.with_context(|| format!("Failed to parse config file: {:?}", path))?;
|
||||||
|
|
||||||
|
debug!("CONFIG: Loaded config: {:?}", config);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default config file path
|
||||||
|
pub fn default_config_path() -> Result<PathBuf> {
|
||||||
|
let xdg_dirs = xdg::BaseDirectories::with_prefix("keep")?;
|
||||||
|
Ok(xdg_dirs.get_config_home().join("config.yml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read password from password_file if configured
|
||||||
|
pub fn get_server_password(&self) -> Result<Option<String>> {
|
||||||
|
if let Some(server) = &self.server {
|
||||||
|
if let Some(password_file) = &server.password_file {
|
||||||
|
debug!("CONFIG: Reading password from file: {:?}", password_file);
|
||||||
|
let password = fs::read_to_string(password_file)
|
||||||
|
.with_context(|| format!("Failed to read password file: {:?}", password_file))?
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
return Ok(Some(password));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge this config with another, giving priority to the other config
|
||||||
|
pub fn merge_with(&mut self, other: &Config) {
|
||||||
|
if other.dir.is_some() {
|
||||||
|
self.dir = other.dir.clone();
|
||||||
|
}
|
||||||
|
if other.list_format.is_some() {
|
||||||
|
self.list_format = other.list_format.clone();
|
||||||
|
}
|
||||||
|
if other.human_readable.is_some() {
|
||||||
|
self.human_readable = other.human_readable;
|
||||||
|
}
|
||||||
|
if other.server.is_some() {
|
||||||
|
self.server = other.server.clone();
|
||||||
|
}
|
||||||
|
if other.compression_plugin.is_some() {
|
||||||
|
self.compression_plugin = other.compression_plugin.clone();
|
||||||
|
}
|
||||||
|
if other.meta_plugins.is_some() {
|
||||||
|
self.meta_plugins = other.meta_plugins.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
76
src/main.rs
76
src/main.rs
@@ -1,6 +1,8 @@
|
|||||||
mod args;
|
mod args;
|
||||||
|
mod config;
|
||||||
mod modes;
|
mod modes;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
use anyhow::{Context, Error, Result, anyhow};
|
use anyhow::{Context, Error, Result, anyhow};
|
||||||
use clap::*;
|
use clap::*;
|
||||||
use clap::error::ErrorKind;
|
use clap::error::ErrorKind;
|
||||||
@@ -27,6 +29,7 @@ extern crate serde_yaml;
|
|||||||
extern crate serde;
|
extern crate serde;
|
||||||
|
|
||||||
use args::{Args, NumberOrString};
|
use args::{Args, NumberOrString};
|
||||||
|
use config::Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main function to handle command-line arguments and execute the appropriate mode.
|
* Main function to handle command-line arguments and execute the appropriate mode.
|
||||||
@@ -48,6 +51,22 @@ fn main() -> Result<(), Error> {
|
|||||||
|
|
||||||
debug!("MAIN: Start");
|
debug!("MAIN: Start");
|
||||||
|
|
||||||
|
// Load configuration with priority: CLI args > env vars > config file > defaults
|
||||||
|
let config_path = if let Some(config_path) = &args.options.config {
|
||||||
|
config_path.clone()
|
||||||
|
} else if let Ok(env_config) = std::env::var("KEEP_CONFIG") {
|
||||||
|
PathBuf::from(env_config)
|
||||||
|
} else {
|
||||||
|
Config::default_config_path().unwrap_or_else(|_| PathBuf::from("~/.config/keep/config.yml"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut config = Config::from_file(&config_path).unwrap_or_else(|e| {
|
||||||
|
debug!("CONFIG: Failed to load config: {}, using defaults", e);
|
||||||
|
Config::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
debug!("MAIN: Loaded config: {:?}", config);
|
||||||
|
|
||||||
let ids = &mut Vec::new();
|
let ids = &mut Vec::new();
|
||||||
let tags = &mut Vec::new();
|
let tags = &mut Vec::new();
|
||||||
|
|
||||||
@@ -95,6 +114,23 @@ fn main() -> Result<(), Error> {
|
|||||||
mode = KeepModes::Status;
|
mode = KeepModes::Status;
|
||||||
} else if args.mode.server.is_some() {
|
} else if args.mode.server.is_some() {
|
||||||
mode = KeepModes::Server;
|
mode = KeepModes::Server;
|
||||||
|
} else if config.server.is_some() && args.mode.server.is_none() {
|
||||||
|
// If server is configured in config file but not specified via CLI
|
||||||
|
if let Some(server_config) = &config.server {
|
||||||
|
let mut server_addr = String::new();
|
||||||
|
if let Some(address) = &server_config.address {
|
||||||
|
server_addr.push_str(address);
|
||||||
|
} else {
|
||||||
|
server_addr.push_str("127.0.0.1");
|
||||||
|
}
|
||||||
|
if let Some(port) = server_config.port {
|
||||||
|
server_addr.push_str(&format!(":{}", port));
|
||||||
|
} else {
|
||||||
|
server_addr.push_str(":8080");
|
||||||
|
}
|
||||||
|
args.mode.server = Some(server_addr);
|
||||||
|
mode = KeepModes::Server;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if mode == KeepModes::Unknown {
|
if mode == KeepModes::Unknown {
|
||||||
@@ -136,12 +172,52 @@ fn main() -> Result<(), Error> {
|
|||||||
debug!("MAIN: tags: {:?}", tags);
|
debug!("MAIN: tags: {:?}", tags);
|
||||||
debug!("MAIN: mode: {:?}", mode);
|
debug!("MAIN: mode: {:?}", mode);
|
||||||
|
|
||||||
|
// Apply configuration priority: CLI args > env vars > config file > defaults
|
||||||
if args.options.dir.is_none() {
|
if args.options.dir.is_none() {
|
||||||
|
if let Some(config_dir) = &config.dir {
|
||||||
|
args.options.dir = Some(config_dir.clone());
|
||||||
|
} else {
|
||||||
match proj_dirs {
|
match proj_dirs {
|
||||||
Some(proj_dirs) => args.options.dir = Some(proj_dirs.data_dir().to_path_buf()),
|
Some(proj_dirs) => args.options.dir = Some(proj_dirs.data_dir().to_path_buf()),
|
||||||
None => return Err(anyhow!("Unable to determine data directory")),
|
None => return Err(anyhow!("Unable to determine data directory")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply list_format from config if not set via CLI/env
|
||||||
|
if args.options.list_format == "id,time,size,tags,meta:hostname" {
|
||||||
|
if let Some(config_list_format) = &config.list_format {
|
||||||
|
args.options.list_format = config_list_format.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply human_readable from config if not set via CLI
|
||||||
|
if !args.options.human_readable {
|
||||||
|
if let Some(config_human_readable) = config.human_readable {
|
||||||
|
args.options.human_readable = config_human_readable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply server password from config file if not set via CLI/env
|
||||||
|
if args.options.server_password.is_none() {
|
||||||
|
if let Ok(Some(password)) = config.get_server_password() {
|
||||||
|
args.options.server_password = Some(password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply compression from config if not set via CLI/env
|
||||||
|
if args.item.compression.is_none() {
|
||||||
|
if let Some(compression_plugin) = &config.compression_plugin {
|
||||||
|
args.item.compression = Some(compression_plugin.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply meta_plugins from config if not set via CLI/env
|
||||||
|
if args.item.meta_plugins.is_empty() {
|
||||||
|
if let Some(meta_plugins) = &config.meta_plugins {
|
||||||
|
args.item.meta_plugins = meta_plugins.iter().map(|p| p.name.clone()).collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::umask(0o077);
|
libc::umask(0o077);
|
||||||
|
|||||||
Reference in New Issue
Block a user