use serde::Deserialize; use serde::de::Deserializer; use std::path::Path; use std::time::Duration; /// Windows 上 `/opt/gitdata/repos` 会退化成盘符相对路径(没有盘符前缀),git 读得到 /// refs 却读不到 object,表现为 `fatal: not a tree object`。这里统一锚定成带盘符的 /// 绝对路径;POSIX 平台本来就是绝对路径,保持原样。 pub fn anchor_local_path(raw: &str) -> String { let trimmed = raw.trim(); if trimmed.is_empty() { return raw.to_string(); } let path = Path::new(trimmed); if path.is_absolute() { return trimmed.to_string(); } match std::path::absolute(path) { Ok(anchored) => anchored.to_string_lossy().into_owned(), Err(_) => trimmed.to_string(), } } fn deserialize_anchored_path<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { String::deserialize(deserializer).map(|value| anchor_local_path(&value)) } mod duration_secs { use serde::{Deserialize, Deserializer}; use std::time::Duration; pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { u64::deserialize(deserializer).map(Duration::from_secs) } } mod opt_duration_secs { use serde::{Deserialize, Deserializer}; use std::time::Duration; pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { Option::::deserialize(deserializer).map(|o| o.map(Duration::from_secs)) } } #[derive(Clone, Debug, Deserialize)] #[serde(default)] pub struct GitConfig { #[serde(deserialize_with = "deserialize_anchored_path")] pub repo_root: String, #[serde(deserialize_with = "deserialize_anchored_path")] pub objects_root: String, pub max_blob_size: u64, pub git_command_timeout_secs: u64, pub git_rpc_timeout_secs: u64, pub ssh: GitSshConfig, pub base_url: String, pub http_clone_base_url: String, pub ssh_clone_host: String, pub ssh_clone_port: u16, pub ssh_clone_user: String, pub graphql_complexity_limit: u64, pub graphql_introspection: bool, pub kenshell_endpoint: String, pub kenshell_rpc_key: String, pub kenshell_timeout_secs: u64, } impl Default for GitConfig { fn default() -> Self { Self { repo_root: anchor_local_path("/opt/gitdata/repos"), objects_root: anchor_local_path("/opt/gitdata/objects"), max_blob_size: 100 * 1024 * 1024, git_command_timeout_secs: 300, git_rpc_timeout_secs: 3600, ssh: GitSshConfig::default(), base_url: "http://localhost:3000".to_string(), http_clone_base_url: String::new(), ssh_clone_host: String::new(), ssh_clone_port: 0, ssh_clone_user: "git".to_string(), graphql_complexity_limit: 1000, graphql_introspection: false, kenshell_endpoint: "http://127.0.0.1:50091".to_string(), kenshell_rpc_key: String::new(), kenshell_timeout_secs: 3600, } } } #[derive(Clone, Debug, Deserialize)] #[serde(default)] pub struct GitSshConfig { pub bind_addr: String, pub port: u16, pub host_key_path: String, pub banner: Option, pub max_sessions: usize, #[serde(deserialize_with = "duration_secs::deserialize")] pub auth_rejection_time: Duration, #[serde(deserialize_with = "opt_duration_secs::deserialize")] pub inactivity_timeout: Option, #[serde(deserialize_with = "duration_secs::deserialize")] pub keepalive_interval: Duration, pub keepalive_max: u32, } impl Default for GitSshConfig { fn default() -> Self { Self { bind_addr: "0.0.0.0".to_string(), port: 22, host_key_path: "/opt/gitdata/ssh_host_ed25519".to_string(), banner: None, max_sessions: 64, auth_rejection_time: Duration::from_secs(2), inactivity_timeout: Some(Duration::from_secs(300)), keepalive_interval: Duration::from_secs(30), keepalive_max: 3, } } }