192 lines
8.3 KiB
Rust
192 lines
8.3 KiB
Rust
use anyhow::{Context, Result};
|
|
use log::{debug, error, info, warn};
|
|
use serde::Deserialize;
|
|
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
// 문자열 정리 및 기본값("N/A") 설정 함수
|
|
fn clean_string(s: Option<&str>) -> String {
|
|
match s {
|
|
Some(val) => {
|
|
let trimmed = val.trim();
|
|
if trimmed.is_empty() {
|
|
"N/A".to_string() // 비어 있으면 "N/A" 반환
|
|
} else {
|
|
trimmed.to_string() // 앞뒤 공백 제거
|
|
}
|
|
}
|
|
None => "N/A".to_string(), // Option이 None이면 "N/A" 반환
|
|
}
|
|
}
|
|
|
|
// JSON 파일의 원본 데이터 구조체
|
|
#[derive(Deserialize, Debug, Clone)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub struct RawHwInfo {
|
|
pub hostname: Option<String>,
|
|
pub category: Option<String>,
|
|
pub manufacturer: Option<String>,
|
|
pub model: Option<String>,
|
|
pub serial: Option<String>, // 실제 시리얼 (합성 키 생성에 사용될 수 있음)
|
|
pub spec_value: Option<String>,
|
|
pub spec_unit: Option<String>,
|
|
pub port_or_slot: Option<String>, // 합성 키 생성에 사용
|
|
}
|
|
|
|
// 처리된 하드웨어 정보 구조체 (실제 사용될 데이터)
|
|
#[derive(Debug, Clone)]
|
|
pub struct ProcessedHwInfo {
|
|
pub hostname: String,
|
|
pub category: String,
|
|
pub manufacturer: String,
|
|
pub model: String,
|
|
pub serial_key: String, // <-- 합성 키 (비교 및 DB 전달용)
|
|
pub spec_value: String,
|
|
pub spec_unit: String,
|
|
}
|
|
|
|
// 지정된 디렉토리에서 패턴에 맞는 JSON 파일 목록 찾기
|
|
pub fn find_json_files(dir_path: &str) -> Result<Vec<PathBuf>> {
|
|
let mut json_files = Vec::new();
|
|
let path = Path::new(dir_path);
|
|
|
|
// 경로 유효성 검사
|
|
if !path.is_dir() {
|
|
anyhow::bail!("제공된 JSON 경로가 디렉토리가 아닙니다: {}", dir_path);
|
|
}
|
|
|
|
// 디렉토리 순회하며 파일 찾기
|
|
for entry in fs::read_dir(path).with_context(|| format!("디렉토리 읽기 오류: {}", dir_path))? {
|
|
let entry = entry.context("디렉토리 항목 읽기 실패")?;
|
|
let path = entry.path();
|
|
if path.is_file() {
|
|
// 파일 이름 패턴 확인 (HWInfo_*.json)
|
|
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
|
|
if filename.starts_with("HWInfo_") && filename.ends_with(".json") {
|
|
debug!("발견된 JSON 파일: {:?}", path);
|
|
json_files.push(path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(json_files)
|
|
}
|
|
|
|
// 단일 JSON 파일 파싱 및 데이터 처리 함수
|
|
fn parse_and_process_single_json(path: &Path) -> Result<Vec<ProcessedHwInfo>> {
|
|
// 파일 읽기 및 BOM 처리
|
|
let bytes = fs::read(path).with_context(|| format!("JSON 파일 읽기 실패: {:?}", path))?;
|
|
let json_content_without_bom = if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
|
|
std::str::from_utf8(&bytes[3..]).with_context(|| format!("UTF-8 변환 실패(BOM제거): {:?}", path))?
|
|
} else {
|
|
std::str::from_utf8(&bytes).with_context(|| format!("UTF-8 변환 실패: {:?}", path))?
|
|
};
|
|
|
|
// 빈 파일 처리
|
|
if json_content_without_bom.trim().is_empty() {
|
|
warn!("JSON 파일 내용이 비어 있습니다: {:?}", path);
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
// JSON 파싱
|
|
let raw_data: Vec<RawHwInfo> = serde_json::from_str(json_content_without_bom)
|
|
.with_context(|| format!("JSON 파싱 실패: {:?}", path))?;
|
|
|
|
let mut processed_list = Vec::new();
|
|
// 처리할 카테고리 목록 (필요시 DB와 동기화 또는 설정 파일로 분리)
|
|
let relevant_categories = ["CPU", "Mainboard", "Memory", "SSD", "HDD", "VGA"];
|
|
|
|
for raw_item in raw_data {
|
|
// 필수 필드(카테고리, 호스트명) 확인
|
|
if let (Some(cat_str), Some(host_str)) = (raw_item.category.as_deref(), raw_item.hostname.as_deref()) {
|
|
let category = clean_string(Some(cat_str));
|
|
let hostname = clean_string(Some(host_str));
|
|
|
|
// 관련 없는 카테고리 또는 유효하지 않은 호스트명 건너뛰기
|
|
if hostname == "N/A" || !relevant_categories.contains(&category.as_str()) {
|
|
continue;
|
|
}
|
|
|
|
let original_serial = clean_string(raw_item.serial.as_deref());
|
|
let port_or_slot = clean_string(raw_item.port_or_slot.as_deref());
|
|
let model = clean_string(raw_item.model.as_deref());
|
|
|
|
// --- 합성 키 생성 로직 ---
|
|
let serial_key: String;
|
|
let mut key_parts: Vec<String> = Vec::new();
|
|
// 실제 시리얼, 슬롯/포트, 호스트명을 조합하여 고유 키 생성 시도
|
|
if original_serial != "N/A" { key_parts.push(original_serial.replace('.', "")); }
|
|
if port_or_slot != "N/A" { key_parts.push(port_or_slot.replace('.', "")); }
|
|
if hostname != "N/A" { key_parts.push(hostname.replace('.', "")); }
|
|
let combined_key = key_parts.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join("_");
|
|
|
|
// 조합된 키가 비어있거나 호스트명만 있는 경우, 대체 키 생성 시도 (모델명+호스트명)
|
|
if combined_key.is_empty() {
|
|
let model_cleaned = model.replace('.', "");
|
|
let hostname_cleaned_fallback = hostname.replace('.', "");
|
|
if model != "N/A" && hostname != "N/A" {
|
|
serial_key = format!("{}_{}", model_cleaned, hostname_cleaned_fallback);
|
|
debug!("대체 Serial Key 생성 (모델+호스트): {}", serial_key);
|
|
} else {
|
|
// 대체 키 생성도 불가능하면 해당 항목 건너뛰기
|
|
warn!("고유 Key 생성 불가 (시리얼/슬롯/모델 정보 부족), 항목 건너<0xEB><0x9B><0x81>: {:?}", raw_item);
|
|
continue;
|
|
}
|
|
} else {
|
|
serial_key = combined_key;
|
|
}
|
|
// --- 합성 키 생성 로직 끝 ---
|
|
|
|
// 처리된 데이터 구조체 생성
|
|
let processed = ProcessedHwInfo {
|
|
hostname: hostname.clone(),
|
|
category, // clean_string 이미 적용됨
|
|
manufacturer: clean_string(raw_item.manufacturer.as_deref()),
|
|
model, // clean_string 이미 적용됨
|
|
serial_key, // 생성된 합성 키
|
|
spec_value: clean_string(raw_item.spec_value.as_deref()),
|
|
spec_unit: clean_string(raw_item.spec_unit.as_deref()),
|
|
};
|
|
processed_list.push(processed);
|
|
} else {
|
|
// 필수 필드 누락 시 경고 로그
|
|
warn!("필수 필드(Category 또는 Hostname) 누락 항목 건너<0xEB><0x9B><0x81>: {:?}", raw_item);
|
|
}
|
|
}
|
|
Ok(processed_list)
|
|
}
|
|
|
|
// 모든 JSON 파일을 읽고 처리하여 호스트명 기준으로 그룹화하는 함수
|
|
pub fn read_and_process_json_files(
|
|
json_dir_path: &str,
|
|
) -> Result<HashMap<String, Vec<ProcessedHwInfo>>> {
|
|
info!("'{}' 디렉토리에서 JSON 파일 검색 및 처리 시작...", json_dir_path);
|
|
let json_files = find_json_files(json_dir_path)?;
|
|
info!("총 {}개의 JSON 파일 발견.", json_files.len());
|
|
|
|
let mut all_processed_data: HashMap<String, Vec<ProcessedHwInfo>> = HashMap::new();
|
|
// 각 JSON 파일 처리
|
|
for json_path in json_files {
|
|
debug!("처리 중인 파일: {:?}", json_path);
|
|
match parse_and_process_single_json(&json_path) {
|
|
Ok(processed_items) => {
|
|
if !processed_items.is_empty() {
|
|
// 처리된 데이터를 호스트명 기준으로 그룹화
|
|
for item in processed_items {
|
|
all_processed_data.entry(item.hostname.clone()).or_default().push(item);
|
|
}
|
|
} else {
|
|
// 처리할 데이터가 없는 경우 디버그 로그
|
|
debug!("파일에서 처리할 관련 카테고리 데이터가 없습니다: {:?}", json_path);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
// 파일 처리 중 오류 발생 시 에러 로그
|
|
error!("파일 처리 중 오류 발생 {:?}: {}", json_path, e);
|
|
}
|
|
}
|
|
}
|
|
info!("JSON 파일 처리 완료. 총 {}개 호스트 데이터 생성.", all_processed_data.len());
|
|
Ok(all_processed_data)
|
|
} |