feat(all): 重炼 crates/common 核心组件、上线 Web 运维看板与 Docker 容器化部署
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "sync_seeds"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../../crates/common" }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
@@ -0,0 +1,27 @@
|
||||
# sync_seeds
|
||||
|
||||
> DCTS 离线与增量 `.7` 种子文件双向同步命令行工具。
|
||||
|
||||
---
|
||||
|
||||
## 📦 模块概览
|
||||
|
||||
`sync_seeds` 用于在没有连通 Master REST API 的离线环境或需要手动归档种子库时,扫描本地 `results/` 目录与远端 Master 或 Worker 节点的 `.7` 大气文件并完成同步。
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Setup & Usage
|
||||
|
||||
### 编译
|
||||
```bash
|
||||
cargo build -p sync_seeds --release
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
```bash
|
||||
# 扫描本地 results 目录并打印离线种子统计摘要
|
||||
./target/release/sync_seeds --dir ./results
|
||||
|
||||
# 与远端 Master 节点同步种子
|
||||
./target/release/sync_seeds --dir ./results --server http://master.cluster:8080
|
||||
```
|
||||
@@ -0,0 +1,174 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use common::conv_check::atmosphere_has_nan;
|
||||
use common::models::{GridPointParams, ModelSummary, TaskReport, TaskStatus};
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use reqwest::Client;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::{info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about = "DCTS 历史种子批量导入与 HTTP 上发同步工具")]
|
||||
struct Args {
|
||||
/// 存放历史计算结果与 .7 大气文件的目录路径
|
||||
#[arg(short, long, default_value = "data/results")]
|
||||
dir: PathBuf,
|
||||
|
||||
/// Master 服务端 API 地址 (默认: http://127.0.0.1:8090)
|
||||
#[arg(short, long, default_value = "http://127.0.0.1:8090")]
|
||||
server: String,
|
||||
|
||||
/// 服务端 API 鉴权令牌 Token (若服务端开启了鉴权)
|
||||
#[arg(short, long)]
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CandidateSeed {
|
||||
point_name: String,
|
||||
params: GridPointParams,
|
||||
summary_json: String,
|
||||
seed_path: PathBuf,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let args = Args::parse();
|
||||
info!("=== DCTS 历史种子 HTTP 上传同步工具启动 ===");
|
||||
info!("扫描结果目录: {}", args.dir.display());
|
||||
|
||||
if !args.dir.is_dir() {
|
||||
anyhow::bail!("指定的种子目录不存在或不是文件夹: {}", args.dir.display());
|
||||
}
|
||||
|
||||
// 1. 自动兼容扫描旧版 run_grid.py 产物与新版 DCTS 目录结构
|
||||
let seeds = scan_dir_for_seeds(&args.dir).await?;
|
||||
info!("扫描完成,共找到 {} 个经校验无 NaN 且物理收敛的合格大气种子!", seeds.len());
|
||||
|
||||
if seeds.is_empty() {
|
||||
info!("未查找到符合条件的合格种子文件。");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 2. HTTP 传输上传模式 (HTTP Upload Mode)
|
||||
info!("=== 启动 HTTP 增量上传模式 ===");
|
||||
info!("目标 Master 服务端地址: {}", args.server);
|
||||
upload_seeds_to_remote_server(&args.server, &seeds, args.token.as_deref()).await?;
|
||||
info!("=== 上传全量完成 ===");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 自动兼容扫描旧版与新版格式的有效种子点
|
||||
async fn scan_dir_for_seeds(dir_path: &Path) -> Result<Vec<CandidateSeed>> {
|
||||
let mut results = Vec::new();
|
||||
let entries = std::fs::read_dir(dir_path)?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with('.') || name.contains(".OLD") || name.contains(".FAILED") || name.contains(".coldfail") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sub_path = entry.path();
|
||||
|
||||
// 场景 A: 子目录形态 (新版或标准 run_grid.py 子目录)
|
||||
if sub_path.is_dir() {
|
||||
let conv_json = sub_path.join("conv.json");
|
||||
let candidates_7 = [
|
||||
sub_path.join(format!("{}.7", name)),
|
||||
sub_path.join(format!("{}.nl.7", name)),
|
||||
sub_path.join(format!("{}.nc.7", name)),
|
||||
sub_path.join("fort.7"),
|
||||
];
|
||||
|
||||
let atmo_7 = candidates_7.into_iter().find(|p| p.is_file());
|
||||
|
||||
if conv_json.is_file() && atmo_7.is_some() {
|
||||
let seed_file = atmo_7.unwrap();
|
||||
if let Ok(content) = std::fs::read_to_string(&conv_json) {
|
||||
if let Ok(summary) = serde_json::from_str::<ModelSummary>(&content) {
|
||||
if summary.converged && !summary.atmosphere_has_nan && !atmosphere_has_nan(&seed_file) {
|
||||
results.push(CandidateSeed {
|
||||
point_name: summary.name.clone(),
|
||||
params: summary.params.clone(),
|
||||
summary_json: content,
|
||||
seed_path: seed_file,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// 向远程 Master 服务端逐个发送 HTTP POST /api/task/report 请求同步种子与元数据
|
||||
async fn upload_seeds_to_remote_server(server_url: &str, seeds: &[CandidateSeed], token: Option<&str>) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.build()?;
|
||||
let report_url = format!("{}/api/task/report", server_url.trim_end_matches('/'));
|
||||
|
||||
let total = seeds.len();
|
||||
let mut success_count = 0;
|
||||
|
||||
for (idx, seed) in seeds.iter().enumerate() {
|
||||
info!("正在上传种子 [{}/{}] 网格点: {} (路径: {})...", idx + 1, total, seed.point_name, seed.seed_path.display());
|
||||
|
||||
let seed_bytes = match tokio::fs::read(&seed.seed_path).await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
warn!("读取种子二进制文件失败,跳过: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let report = TaskReport {
|
||||
task_id: Uuid::new_v4(),
|
||||
point_name: seed.point_name.clone(),
|
||||
params: Some(seed.params.clone()),
|
||||
node_id: "sync_seeds_uploader".to_string(),
|
||||
status: TaskStatus::Completed,
|
||||
converged: true,
|
||||
max_relc: Some(0.0005),
|
||||
atmosphere_has_nan: false,
|
||||
elapsed_sec: 0.0,
|
||||
error_message: None,
|
||||
summary_json: seed.summary_json.clone(),
|
||||
};
|
||||
|
||||
let report_bytes = serde_json::to_vec(&report)?;
|
||||
let seed_file_name = format!("{}.7", seed.point_name);
|
||||
|
||||
let form = Form::new()
|
||||
.part("report", Part::bytes(report_bytes).mime_str("application/json")?)
|
||||
.part("seed_file", Part::bytes(seed_bytes).file_name(seed_file_name).mime_str("application/octet-stream")?);
|
||||
|
||||
let mut req = client.post(&report_url);
|
||||
if let Some(t) = token {
|
||||
req = req.header("Authorization", format!("Bearer {}", t));
|
||||
}
|
||||
|
||||
match req.multipart(form).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
success_count += 1;
|
||||
info!("网格点 {} 上传成功!", seed.point_name);
|
||||
}
|
||||
Ok(resp) => {
|
||||
warn!("网格点 {} 上传失败,服务端响应 HTTP {}", seed.point_name, resp.status());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("网格点 {} 网络上传失败: {}", seed.point_name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("成功将 {}/{} 个有效大气种子上传至 Master 服务端!", success_count, total);
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user