feat(all): 重炼 crates/common 核心组件、上线 Web 运维看板与 Docker 容器化部署
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user