use std::collections::BTreeMap; use std::fs; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::query_db::QueryDbResult; #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ArtifactFileSummary { pub path: String, pub size_bytes: Option, pub modified_unix_secs: Option, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct QueryAuditArtifactSummary { pub status: Option, pub events_path: Option, pub events_count: Option, pub events_sha256: Option, pub writer_version: Option, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ArtifactManifestSummary { pub files: BTreeMap, pub query_audit: Option, pub cir_counts_available: bool, pub validation_events_indexed: bool, } impl ArtifactManifestSummary { pub fn artifact_paths(&self) -> BTreeMap { self.files .iter() .map(|(name, item)| (name.clone(), item.path.clone())) .collect() } } pub fn build_artifact_manifest( run_dir: &Path, query_audit: Option<&Value>, ) -> QueryDbResult { let mut manifest = ArtifactManifestSummary { query_audit: query_audit.map(query_audit_summary), cir_counts_available: false, validation_events_indexed: false, ..ArtifactManifestSummary::default() }; for name in [ "report.json", "input.cir", "result.cir", "result.ccr", "stage-timing.json", "run-summary.json", ] { let path = run_dir.join(name); if path.exists() { manifest .files .insert(name.to_string(), file_summary(&path)?); } } if let Some(events_path) = manifest .query_audit .as_ref() .and_then(|query_audit| query_audit.events_path.as_ref()) { let path = sidecar_path(run_dir, events_path); if path.exists() { manifest .files .insert("validationEvents".to_string(), file_summary(&path)?); } } Ok(manifest) } fn file_summary(path: &Path) -> QueryDbResult { let metadata = fs::metadata(path)?; let modified_unix_secs = metadata .modified() .ok() .and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok()) .map(|duration| duration.as_secs()); Ok(ArtifactFileSummary { path: path.display().to_string(), size_bytes: Some(metadata.len()), modified_unix_secs, }) } fn query_audit_summary(value: &Value) -> QueryAuditArtifactSummary { QueryAuditArtifactSummary { status: json_str(value, &["status"]).map(str::to_string), events_path: json_str(value, &["eventsPath"]) .or_else(|| json_str(value, &["events_path"])) .map(str::to_string), events_count: json_u64(value, &["eventsCount"]) .or_else(|| json_u64(value, &["events_count"])), events_sha256: json_str(value, &["eventsSha256"]) .or_else(|| json_str(value, &["events_sha256"])) .map(str::to_string), writer_version: json_u64(value, &["writerVersion"]) .or_else(|| json_u64(value, &["writer_version"])), } } pub fn sidecar_path(run_dir: &Path, path: &str) -> PathBuf { let path = PathBuf::from(path); if path.is_absolute() { path } else { run_dir.join(path) } } fn json_str<'a>(value: &'a Value, path: &[&str]) -> Option<&'a str> { let mut current = value; for key in path { current = current.get(*key)?; } current.as_str() } fn json_u64(value: &Value, path: &[&str]) -> Option { let mut current = value; for key in path { current = current.get(*key)?; } current.as_u64() }