greentic_component/cmd/
store.rs1use std::fs;
2use std::path::PathBuf;
3
4use anyhow::{Context, Result, anyhow};
5use clap::{Args, Subcommand};
6use serde_json::Value;
7
8use crate::path_safety::normalize_under_root;
9use greentic_distributor_client::{DistClient, DistOptions};
10
11#[derive(Subcommand, Debug, Clone)]
12pub enum StoreCommand {
13 Fetch(StoreFetchArgs),
15}
16
17#[derive(Args, Debug, Clone)]
18pub struct StoreFetchArgs {
19 #[arg(long, value_name = "DIR")]
21 pub out: PathBuf,
22 #[arg(long, value_name = "DIR")]
24 pub cache_dir: Option<PathBuf>,
25 #[arg(value_name = "SOURCE")]
27 pub source: String,
28}
29
30pub fn run(command: StoreCommand) -> Result<()> {
31 match command {
32 StoreCommand::Fetch(args) => fetch(args),
33 }
34}
35
36fn fetch(args: StoreFetchArgs) -> Result<()> {
37 let mut opts = DistOptions::default();
38 if let Some(cache_dir) = &args.cache_dir {
39 opts.cache_dir = cache_dir.clone();
40 }
41 let client = DistClient::new(opts);
42 let rt = tokio::runtime::Runtime::new().context("failed to create async runtime")?;
43 let resolved = rt
44 .block_on(async { client.ensure_cached(&args.source).await })
45 .context("store fetch failed")?;
46 let cache_path = resolved
47 .cache_path
48 .ok_or_else(|| anyhow!("resolved source has no cached component path"))?;
49 fs::create_dir_all(&args.out)
50 .with_context(|| format!("failed to create output dir {}", args.out.display()))?;
51 let manifest_cache_path = cache_path
52 .parent()
53 .map(|dir| dir.join("component.manifest.json"));
54 let manifest_out_path = args.out.join("component.manifest.json");
55 let mut wasm_out_path = args.out.join("component.wasm");
56 if let Some(manifest_cache_path) = manifest_cache_path
57 && manifest_cache_path.exists()
58 {
59 let manifest_bytes = fs::read(&manifest_cache_path).with_context(|| {
60 format!(
61 "failed to read cached manifest {}",
62 manifest_cache_path.display()
63 )
64 })?;
65 fs::write(&manifest_out_path, &manifest_bytes)
66 .with_context(|| format!("failed to write manifest {}", manifest_out_path.display()))?;
67 let manifest: Value = serde_json::from_slice(&manifest_bytes).with_context(|| {
68 format!(
69 "failed to parse component.manifest.json from {}",
70 manifest_cache_path.display()
71 )
72 })?;
73 if let Some(component_wasm) = manifest
74 .get("artifacts")
75 .and_then(|artifacts| artifacts.get("component_wasm"))
76 .and_then(|value| value.as_str())
77 {
78 let candidate = PathBuf::from(component_wasm);
79 wasm_out_path = normalize_under_root(&args.out, &candidate).with_context(|| {
80 format!("invalid artifacts.component_wasm path `{}`", component_wasm)
81 })?;
82 if let Some(parent) = wasm_out_path.parent() {
83 fs::create_dir_all(parent)
84 .with_context(|| format!("failed to create output dir {}", parent.display()))?;
85 }
86 }
87 }
88 fs::copy(&cache_path, &wasm_out_path).with_context(|| {
89 format!(
90 "failed to copy cached component {} to {}",
91 cache_path.display(),
92 wasm_out_path.display()
93 )
94 })?;
95 println!(
96 "Wrote {} (digest {}) for source {}",
97 wasm_out_path.display(),
98 resolved.digest,
99 args.source,
100 );
101 if manifest_out_path.exists() {
102 println!("Wrote {}", manifest_out_path.display());
103 }
104 Ok(())
105}