mirror of https://github.com/telemt/telemt.git
108 lines
3.4 KiB
Rust
108 lines
3.4 KiB
Rust
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<String> {
|
|
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<String, ApiFailure> {
|
|
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<ProxyConfig, ApiFailure> {
|
|
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<String, ApiFailure> {
|
|
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::<u64>()
|
|
);
|
|
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
|
|
}
|