Skip to main content

yggdrasil/utils/
server.rs

1//! 服务端共享工具(仅 `feature = "server"` 编译)。
2//!
3//! 集中跨模块重复的服务端常量与工具函数(issue #7 重复常量去重)。
4
5#![cfg(feature = "server")]
6
7use sha2::{Digest, Sha256};
8
9/// 明文 token / 任意字符串 → SHA-256 hex。
10///
11/// 此前 `auth/session.rs` 与 `mcp/auth.rs` 各有一份逐字相同的实现。
12pub fn sha256_hex(input: &str) -> String {
13    let mut hasher = Sha256::new();
14    hasher.update(input.as_bytes());
15    hex::encode(hasher.finalize())
16}
17
18/// 邮箱格式正则(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)。
19///
20/// 此前 `api/auth.rs` 与 `api/comments/helpers.rs` 各有一份逐字相同的 LazyLock。
21pub static EMAIL_REGEX: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
22    regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
23        .expect("EMAIL_REGEX 正则模式应在编译期通过校验")
24});
25
26/// 上传文件大小上限(5 MiB)。
27///
28/// 此前 `api/upload.rs` 与 `mcp/tools/media.rs` 各定义一份相同的常量。
29pub const MAX_FILE_SIZE: usize = 5 * 1024 * 1024;
30
31/// 启动期数据库迁移超时窗口(秒),由 `MIGRATE_STARTUP_TIMEOUT_SECS` 控制,默认 30。
32///
33/// 此前 `main.rs` 与 `db/pool.rs`(`get_conn_for_startup`、`ensure_database_exists` 两处)
34/// 各有一份逐字相同的 `.ok().and_then(parse).unwrap_or(30)` 解析链。
35pub fn parse_migrate_startup_timeout() -> u64 {
36    std::env::var("MIGRATE_STARTUP_TIMEOUT_SECS")
37        .ok()
38        .and_then(|s| s.parse::<u64>().ok())
39        .unwrap_or(30)
40}
41
42/// 默认 SSR 增量缓存时长(秒)。
43pub const DEFAULT_SSR_CACHE_SECS: u64 = 3600;
44
45pub fn parse_bool_value(value: Option<&str>, default: bool) -> bool {
46    match value.map(str::trim) {
47        Some(value)
48            if value.eq_ignore_ascii_case("1")
49                || value.eq_ignore_ascii_case("true")
50                || value.eq_ignore_ascii_case("yes")
51                || value.eq_ignore_ascii_case("on") =>
52        {
53            true
54        }
55        Some(value)
56            if value.eq_ignore_ascii_case("0")
57                || value.eq_ignore_ascii_case("false")
58                || value.eq_ignore_ascii_case("no")
59                || value.eq_ignore_ascii_case("off") =>
60        {
61            false
62        }
63        _ => default,
64    }
65}
66
67/// Read a boolean environment variable with an explicit default.
68pub fn parse_env_bool(name: &str, default: bool) -> bool {
69    let value = std::env::var(name).ok();
70    parse_bool_value(value.as_deref(), default)
71}
72
73/// SSR cache TTL from the environment, with the shared default.
74pub fn parse_ssr_cache_secs() -> u64 {
75    std::env::var("SSR_CACHE_SECS")
76        .ok()
77        .and_then(|value| value.trim().parse().ok())
78        .unwrap_or(DEFAULT_SSR_CACHE_SECS)
79}
80
81/// 转义 SQL `LIKE` 模式串中的特殊字符(`\`、`%`、`_`),配合 `ESCAPE '\\'` 使用。
82///
83/// 此前 `api/posts/list.rs`(逐字符循环)与 `api/posts/search.rs`、`mcp/tools/read.rs`
84///(replace 链)各有一份实现;统一为等价且更简洁的 replace 链风格。
85pub fn escape_like_pattern(s: &str) -> String {
86    s.replace('\\', "\\\\")
87        .replace('%', "\\%")
88        .replace('_', "\\_")
89}
90#[cfg(test)]
91mod tests {
92    use super::parse_bool_value;
93
94    #[test]
95    fn parse_bool_value_accepts_common_spellings() {
96        for value in ["1", "true", "yes", "on"] {
97            assert!(parse_bool_value(Some(value), false));
98        }
99        for value in ["0", "false", "no", "off"] {
100            assert!(!parse_bool_value(Some(value), true));
101        }
102    }
103
104    #[test]
105    fn parse_bool_value_uses_default_for_unknown_values() {
106        assert!(parse_bool_value(Some("maybe"), true));
107        assert!(!parse_bool_value(Some("maybe"), false));
108        assert!(parse_bool_value(None, true));
109        assert!(!parse_bool_value(None, false));
110    }
111}