feat(all): 重炼 crates/common 核心组件、上线 Web 运维看板与 Docker 容器化部署

This commit is contained in:
fmq
2026-07-28 10:31:57 +08:00
commit 4b4238d702
71 changed files with 163402 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
use anyhow::{Context, Result};
use reqwest::Client;
use sha2::{Digest, Sha256};
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use tracing::info;
#[cfg(feature = "embed-binaries")]
pub static TLUSTY_BIN: &[u8] = include_bytes!("../../../assets/tlusty_static");
#[cfg(not(feature = "embed-binaries"))]
pub static TLUSTY_BIN: &[u8] = &[];
#[cfg(feature = "embed-binaries")]
pub static SYNSPEC_BIN: &[u8] = include_bytes!("../../../assets/synspec_static");
#[cfg(not(feature = "embed-binaries"))]
pub static SYNSPEC_BIN: &[u8] = &[];
#[derive(Debug, Clone)]
pub struct RuntimePaths {
pub tlusty_exe: PathBuf,
pub synspec_exe: PathBuf,
pub data_dir: PathBuf,
pub linelist: PathBuf,
}
fn calc_hash(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize())
}
/// Ensures Fortran runtime binaries are unpacked and common partition function data files are fetched
pub async fn ensure_runtime(
runtime_dir: &Path,
server_url: &str,
client: &Client,
) -> Result<RuntimePaths> {
fs::create_dir_all(runtime_dir)
.with_context(|| format!("Failed to create runtime dir: {}", runtime_dir.display()))?;
let tlusty_exe = runtime_dir.join("tlusty_static");
let synspec_exe = runtime_dir.join("synspec_static");
let data_dir = runtime_dir.join("data");
let linelist = runtime_dir.join("gfVIS99.dat");
fs::create_dir_all(&data_dir)?;
// 1. Unpack tlusty_static & synspec_static binaries if embedded
if !TLUSTY_BIN.is_empty() {
write_if_changed(&tlusty_exe, TLUSTY_BIN, true)?;
}
if !SYNSPEC_BIN.is_empty() {
write_if_changed(&synspec_exe, SYNSPEC_BIN, true)?;
}
// 2. Fetch baseline equation of state partition function tables if missing locally
let common_files = &["irwin_bc.dat", "irwin_orig.dat", "tsuji.molec_bc2", "tsuji.molec_orig"];
ensure_specific_data_files(&data_dir, server_url, client, common_files).await?;
// 3. Check gfVIS99.dat
if !linelist.exists() {
let url = format!("{}/api/data/linelist", server_url);
info!("本地缺失主谱线库 gfVIS99.dat,开始从服务端下载: {}...", url);
let resp = client.get(&url).send().await?;
if resp.status().is_success() {
let bytes = resp.bytes().await?;
fs::write(&linelist, &bytes)?;
info!("成功下载并保存主谱线库 gfVIS99.dat");
} else {
anyhow::bail!("从服务端下载主谱线库 gfVIS99.dat 失败,HTTP 状态码: {}", resp.status());
}
}
let abs_runtime_dir = fs::canonicalize(runtime_dir).unwrap_or_else(|_| runtime_dir.to_path_buf());
let tlusty_exe = abs_runtime_dir.join("tlusty_static");
let synspec_exe = abs_runtime_dir.join("synspec_static");
let data_dir = abs_runtime_dir.join("data");
let linelist = abs_runtime_dir.join("gfVIS99.dat");
Ok(RuntimePaths {
tlusty_exe,
synspec_exe,
data_dir,
linelist,
})
}
static DATA_DOWNLOAD_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
/// Checks local `./runtime/data/` for specific required files. If missing, downloads ONLY those specific files from Server!
pub async fn ensure_specific_data_files(
data_dir: &Path,
server_url: &str,
client: &Client,
required_files: &[&str],
) -> Result<()> {
let _guard = DATA_DOWNLOAD_MUTEX.lock().await;
tokio::fs::create_dir_all(data_dir).await?;
for &filename in required_files {
let local_file = data_dir.join(filename);
if !local_file.exists() {
let file_url = format!("{}/api/data/file/{}", server_url, filename);
info!("本地缺失数据文件 {},开始从服务端拉取: {}", filename, file_url);
let resp = client.get(&file_url).send().await?;
if resp.status().is_success() {
let bytes = resp.bytes().await?;
let tmp_file = data_dir.join(format!("{}.{}.tmp", filename, uuid::Uuid::new_v4().simple()));
tokio::fs::write(&tmp_file, &bytes).await?;
tokio::fs::rename(&tmp_file, &local_file).await?;
info!("成功保存数据文件: {}", filename);
} else {
anyhow::bail!("服务端返回 HTTP {} 错误,数据文件: {}", resp.status(), filename);
}
}
}
Ok(())
}
fn write_if_changed(target_path: &Path, content: &[u8], executable: bool) -> Result<()> {
let should_write = if target_path.exists() {
match fs::read(target_path) {
Ok(existing) => calc_hash(&existing) != calc_hash(content),
Err(_) => true,
}
} else {
true
};
if should_write {
let mut file = File::create(target_path)?;
file.write_all(content)?;
file.flush()?;
#[cfg(unix)]
if executable {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(target_path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(target_path, perms)?;
}
}
Ok(())
}