use std::io::Write; use std::path::{Path, PathBuf}; use hyper::header::IF_MATCH; use sha2::{Digest, Sha256}; use crate::config::ProxyConfig; use super::model::ApiFailure; pub(super) fn parse_if_match(headers: &hyper::HeaderMap) -> Option { headers .get(IF_MATCH) .and_then(|value| value.to_str().ok()) .map(str::trim) .filter(|value| !value.is_empty()) .map(|value| value.trim_matches('"').to_string()) } pub(super) async fn ensure_expected_revision( config_path: &Path, expected_revision: Option<&str>, ) -> Result<(), ApiFailure> { let Some(expected) = expected_revision else { return Ok(()); }; let current = current_revision(config_path).await?; if current != expected { return Err(ApiFailure::new( hyper::StatusCode::CONFLICT, "revision_conflict", "Config revision mismatch", )); } Ok(()) } pub(super) async fn current_revision(config_path: &Path) -> Result { let content = tokio::fs::read_to_string(config_path) .await .map_err(|e| ApiFailure::internal(format!("failed to read config: {}", e)))?; Ok(compute_revision(&content)) } pub(super) fn compute_revision(content: &str) -> String { let mut hasher = Sha256::new(); hasher.update(content.as_bytes()); hex::encode(hasher.finalize()) } pub(super) async fn load_config_from_disk(config_path: &Path) -> Result { let config_path = config_path.to_path_buf(); tokio::task::spawn_blocking(move || ProxyConfig::load(config_path)) .await .map_err(|e| ApiFailure::internal(format!("failed to join config loader: {}", e)))? .map_err(|e| ApiFailure::internal(format!("failed to load config: {}", e))) } pub(super) async fn save_config_to_disk( config_path: &Path, cfg: &ProxyConfig, ) -> Result { let serialized = toml::to_string_pretty(cfg) .map_err(|e| ApiFailure::internal(format!("failed to serialize config: {}", e)))?; write_atomic(config_path.to_path_buf(), serialized.clone()).await?; Ok(compute_revision(&serialized)) } async fn write_atomic(path: PathBuf, contents: String) -> Result<(), ApiFailure> { tokio::task::spawn_blocking(move || write_atomic_sync(&path, &contents)) .await .map_err(|e| ApiFailure::internal(format!("failed to join writer: {}", e)))? .map_err(|e| ApiFailure::internal(format!("failed to write config: {}", e))) } fn write_atomic_sync(path: &Path, contents: &str) -> std::io::Result<()> { let parent = path.parent().unwrap_or_else(|| Path::new(".")); std::fs::create_dir_all(parent)?; let tmp_name = format!( ".{}.tmp-{}", path.file_name() .and_then(|s| s.to_str()) .unwrap_or("config.toml"), rand::random::() ); let tmp_path = parent.join(tmp_name); let write_result = (|| { let mut file = std::fs::OpenOptions::new() .create_new(true) .write(true) .open(&tmp_path)?; file.write_all(contents.as_bytes())?; file.sync_all()?; std::fs::rename(&tmp_path, path)?; if let Ok(dir) = std::fs::File::open(parent) { let _ = dir.sync_all(); } Ok(()) })(); if write_result.is_err() { let _ = std::fs::remove_file(&tmp_path); } write_result }