Files
ChuSQL/chusql-core/storage/src/config.rs
T

355 lines
10 KiB
Rust
Raw Normal View History

use std::path::PathBuf;
use serde::Deserialize;
use crate::log::Level;
// 全局配置:只从 TOML 文件读,缺项回退内置默认值;
// 存储相关分区由本模块解析校验,其它层的分区一概不碰。
2026-10-01 20:48:38 +08:00
/// 配置文件名
pub const CONFIG_FILE_NAME: &str = "settings.toml";
2026-10-01 20:48:38 +08:00
/// 配置目录名(随系统惯例放在用户配置目录下)
pub const APP_DIR_NAME: &str = "ChuSQL";
/// Unix 下的数据目录名(XDG 惯例小写,跟安装脚本的默认安装目录一致)
pub const UNIX_APP_DIR_NAME: &str = "chusql";
/// 数据子目录名
pub const DATA_DIR_NAME: &str = "data";
2026-09-24 23:56:07 +08:00
pub const DEFAULT_POOL_SIZE: usize = 1024;
pub const DEFAULT_PAGE_SIZE: usize = 4096;
pub const DEFAULT_BTREE_ORDER: usize = 4;
pub const DEFAULT_LOG_LEVEL: &str = "info";
/// 默认日志输出目录(相对启动目录)
pub const DEFAULT_LOG_FILES: &str = "./logs";
const MIN_PAGE_SIZE: usize = 512;
const MAX_PAGE_SIZE: usize = 65536;
const MIN_BTREE_ORDER: usize = 3;
/// 默认配置文件路径(按平台惯例)
2026-10-01 20:48:38 +08:00
pub fn default_config_path() -> PathBuf {
#[cfg(windows)]
{
let appdata = std::env::var_os("APPDATA").filter(|v| !v.is_empty());
if let Some(appdata) = appdata {
return PathBuf::from(appdata).join(APP_DIR_NAME).join(CONFIG_FILE_NAME);
}
}
#[cfg(not(windows))]
{
let xdg = std::env::var_os("XDG_CONFIG_HOME").filter(|v| !v.is_empty());
if let Some(xdg) = xdg {
return PathBuf::from(xdg).join(APP_DIR_NAME).join(CONFIG_FILE_NAME);
}
let home = std::env::var_os("HOME").filter(|v| !v.is_empty());
if let Some(home) = home {
return PathBuf::from(home)
.join(".config")
.join(APP_DIR_NAME)
.join(CONFIG_FILE_NAME);
}
}
PathBuf::from(APP_DIR_NAME).join(CONFIG_FILE_NAME)
}
/// Windows 默认数据目录
2026-10-01 20:48:38 +08:00
#[cfg(windows)]
pub fn default_data_dir() -> PathBuf {
let local = std::env::var_os("LOCALAPPDATA").filter(|v| !v.is_empty());
if let Some(local) = local {
return PathBuf::from(local).join(APP_DIR_NAME).join(DATA_DIR_NAME);
}
let roaming = std::env::var_os("APPDATA").filter(|v| !v.is_empty());
if let Some(roaming) = roaming {
return PathBuf::from(roaming).join(APP_DIR_NAME).join(DATA_DIR_NAME);
}
PathBuf::from(APP_DIR_NAME).join(DATA_DIR_NAME)
}
/// Unix 默认数据目录(XDG 惯例)
2026-10-01 20:48:38 +08:00
#[cfg(not(windows))]
pub fn default_data_dir() -> PathBuf {
let xdg = std::env::var_os("XDG_DATA_HOME").filter(|v| !v.is_empty());
if let Some(xdg) = xdg {
return PathBuf::from(xdg)
.join(UNIX_APP_DIR_NAME)
.join(DATA_DIR_NAME);
}
let home = std::env::var_os("HOME").filter(|v| !v.is_empty());
if let Some(home) = home {
return PathBuf::from(home)
.join(".local")
.join("share")
.join(UNIX_APP_DIR_NAME)
.join(DATA_DIR_NAME);
}
PathBuf::from(UNIX_APP_DIR_NAME).join(DATA_DIR_NAME)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Config {
pub page_size: usize,
pub btree_order: usize,
2026-09-24 21:28:49 +08:00
pub pool_size: usize,
pub data_dir: PathBuf,
pub log_files: PathBuf,
pub log_level: Level,
}
impl Default for Config {
/// 内置默认配置
fn default() -> Self {
Config {
page_size: DEFAULT_PAGE_SIZE,
btree_order: DEFAULT_BTREE_ORDER,
2026-09-24 21:28:49 +08:00
pool_size: DEFAULT_POOL_SIZE,
2026-10-01 20:48:38 +08:00
data_dir: default_data_dir(),
log_files: PathBuf::from(DEFAULT_LOG_FILES),
log_level: Level::Info,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Origin {
Default,
File(PathBuf),
}
impl Origin {
2026-09-24 21:28:49 +08:00
/// 来源的文案
pub fn describe(&self) -> String {
match self {
Origin::Default => "default".to_string(),
Origin::File(p) => format!("file {}", p.display()),
}
}
}
#[derive(Debug, Clone)]
pub struct Loaded {
pub config: Config,
pub config_path: Option<PathBuf>,
pub origins: Vec<(&'static str, String, Origin)>,
}
#[derive(Debug, Default, Deserialize)]
struct FileConfig {
page: Option<FilePage>,
btree: Option<FileBtree>,
2026-09-24 21:28:49 +08:00
buffer: Option<FileBuffer>,
storage: Option<FileStorage>,
log: Option<FileLog>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
2026-09-24 21:28:49 +08:00
struct FileBuffer {
pool_size: Option<usize>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FilePage {
size: Option<usize>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileBtree {
order: Option<usize>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileStorage {
data_dir: Option<String>,
log_files: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileLog {
level: Option<String>,
}
/// 加载配置:显式路径优先,其次默认位置
2026-10-01 20:48:38 +08:00
pub fn load(explicit: Option<&str>) -> Result<Loaded, String> {
let explicit = explicit.map(str::trim).filter(|p| !p.is_empty());
let path = match explicit {
Some(p) => {
let p = PathBuf::from(p);
if !p.is_file() {
return Err(format!("config file not found: {}", p.display()));
}
Some(p)
}
None => {
let p = default_config_path();
if p.is_file() { Some(p) } else { None }
}
};
let text = match &path {
Some(p) => Some(
std::fs::read_to_string(p)
.map_err(|e| format!("cannot read config file {}: {}", p.display(), e))?,
),
None => None,
};
2026-10-01 20:48:38 +08:00
resolve(text.as_deref(), path)
}
2026-10-01 20:48:38 +08:00
/// 解析文本:文件 -> 默认
pub fn resolve(text: Option<&str>, path: Option<PathBuf>) -> Result<Loaded, String> {
let file: FileConfig = match text {
None => FileConfig::default(),
Some(t) => toml::from_str(t).map_err(|e| match &path {
Some(p) => format!("bad config file {}: {}", p.display(), e),
None => format!("bad config: {}", e),
})?,
};
let file_origin = Origin::File(
2026-10-01 20:48:38 +08:00
path.clone().unwrap_or_else(default_config_path),
);
let (page_size, page_origin) = pick(
file.page.and_then(|p| p.size),
DEFAULT_PAGE_SIZE,
&file_origin,
2026-10-01 20:48:38 +08:00
);
let (btree_order, order_origin) = pick(
file.btree.and_then(|b| b.order),
DEFAULT_BTREE_ORDER,
&file_origin,
2026-10-01 20:48:38 +08:00
);
2026-09-24 21:28:49 +08:00
let (pool_size, pool_origin) = pick(
file.buffer.and_then(|b| b.pool_size),
DEFAULT_POOL_SIZE,
&file_origin,
2026-10-01 20:48:38 +08:00
);
let (data_dir, dir_origin) = pick(
file.storage.as_ref().and_then(|s| s.data_dir.clone()),
2026-10-01 20:48:38 +08:00
default_data_dir().to_string_lossy().into_owned(),
&file_origin,
2026-10-01 20:48:38 +08:00
);
let (log_files, log_files_origin) = pick(
file.storage.as_ref().and_then(|s| s.log_files.clone()),
DEFAULT_LOG_FILES.to_string(),
&file_origin,
);
// 管名已经没有了:存储不再是独立进程,管道/套接字那一层随 FFI 一起删掉
let (log_level, log_origin) = pick(
file.log.and_then(|l| l.level),
DEFAULT_LOG_LEVEL.to_string(),
&file_origin,
2026-10-01 20:48:38 +08:00
);
let data_dir = data_dir.trim().to_string();
let log_files = log_files.trim().to_string();
2026-10-01 20:48:38 +08:00
let log_level = log_level.trim().to_string();
let log_level = Level::parse(&log_level)
.ok_or_else(|| format!("log.level is not one of off/error/warn/info/debug: {}", log_level))?;
if data_dir.is_empty() {
return Err("storage.data_dir must not be empty".to_string());
}
if log_files.is_empty() {
return Err("storage.log_files must not be empty".to_string());
}
validate_layout(page_size, btree_order)?;
2026-09-24 21:28:49 +08:00
if pool_size == 0 {
return Err("buffer.pool_size must be at least 1".to_string());
}
let origins = vec![
("page.size", page_size.to_string(), page_origin),
("btree.order", btree_order.to_string(), order_origin),
2026-09-25 17:41:51 +08:00
("buffer.pool_size", pool_size.to_string(), pool_origin),
("storage.data_dir", data_dir.clone(), dir_origin),
("storage.log_files", log_files.clone(), log_files_origin),
("log.level", log_level.name().to_string(), log_origin),
];
Ok(Loaded {
config: Config {
page_size,
btree_order,
2026-09-24 21:28:49 +08:00
pool_size,
data_dir: PathBuf::from(data_dir),
log_files: PathBuf::from(log_files),
log_level,
},
config_path: if text.is_some() { path } else { None },
origins,
})
}
2026-10-01 20:48:38 +08:00
/// 取一项:文件 -> 默认
fn pick<T>(file_value: Option<T>, default: T, file_origin: &Origin) -> (T, Origin) {
if let Some(value) = file_value {
2026-10-01 20:48:38 +08:00
return (value, file_origin.clone());
}
2026-10-01 20:48:38 +08:00
(default, Origin::Default)
}
2026-09-24 21:28:49 +08:00
/// 检查选项是否自洽
fn validate_layout(page_size: usize, btree_order: usize) -> Result<(), String> {
if !(MIN_PAGE_SIZE..=MAX_PAGE_SIZE).contains(&page_size) || !page_size.is_power_of_two() {
return Err(format!(
"page.size must be a power of two between {} and {}, got {}",
MIN_PAGE_SIZE, MAX_PAGE_SIZE, page_size
));
}
if btree_order < MIN_BTREE_ORDER {
return Err(format!(
"btree.order must be at least {}, got {}",
MIN_BTREE_ORDER, btree_order
));
}
if !crate::btree::fits_in_page(page_size, btree_order) {
return Err(format!(
"btree.order {} does not fit in a {}-byte page (max {})",
btree_order,
page_size,
crate::btree::max_order(page_size)
));
}
Ok(())
}
2026-10-01 20:48:38 +08:00
#[cfg(test)]
mod tests {
use super::*;
/// 别的层的分区不能被当成错误
2026-10-01 20:48:38 +08:00
#[test]
fn ignores_sections_owned_by_other_layers() {
let text = r#"
[page]
size = 8192
[storage]
data_dir = "../data"
[server]
listen_host = "127.0.0.1"
2026-10-01 20:48:38 +08:00
port = 7777
max_message = 1048576
[web]
port = 7778
2026-10-01 20:48:38 +08:00
"#;
let loaded = resolve(Some(text), Some(PathBuf::from("settings.toml"))).unwrap();
2026-10-01 20:48:38 +08:00
assert_eq!(loaded.config.page_size, 8192);
assert_eq!(loaded.config.data_dir, PathBuf::from("../data"));
}
/// 自己分区里的拼写错误仍然要报错
#[test]
fn still_rejects_typos_inside_its_own_sections() {
let text = "[page]\npage_size = 8192\n";
assert!(resolve(Some(text), None).is_err());
}
}