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 { 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 { // 原子写:先写 .tmp 再 rename,避免半写后崩溃留下截断/损坏的可执行二进制。 // 历史上直接 File::create 覆盖目标,若 write_all 中途进程被杀/磁盘满,会留下 // 截断的 tlusty_static,且权限可能已设 0o755(可执行但损坏),node 尝试运行时 // 产生难以定位的 Fortran 崩溃。tmp + rename 保证目标要么是完整旧版、要么是完整新版。 let tmp_path = target_path.with_extension("tmp.write"); { let mut file = File::create(&tmp_path) .with_context(|| format!("创建临时文件失败: {}", tmp_path.display()))?; file.write_all(content)?; file.flush()?; // drop 前 sync_all 确保数据落盘,降低断电丢数据的概率。 let _ = file.sync_all(); } #[cfg(unix)] if executable { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(&tmp_path)?.permissions(); perms.set_mode(0o755); fs::set_permissions(&tmp_path, perms)?; } fs::rename(&tmp_path, target_path).with_context(|| { format!( "原子重命名 {} -> {} 失败", tmp_path.display(), target_path.display() ) })?; } Ok(()) }