greentic_component/cmd/
store.rs1use std::fs;
2use std::path::{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 source = resolve_source(&args.source)?;
38 let mut opts = DistOptions::default();
39 if let Some(cache_dir) = &args.cache_dir {
40 opts.cache_dir = cache_dir.clone();
41 }
42 let client = DistClient::new(opts);
43 let rt = tokio::runtime::Runtime::new().context("failed to create async runtime")?;
44 let resolved = rt
45 .block_on(async { client.ensure_cached(&source).await })
46 .context("store fetch failed")?;
47 let cache_path = resolved
48 .cache_path
49 .ok_or_else(|| anyhow!("resolved source has no cached component path"))?;
50 let (out_dir, wasm_override) = resolve_output_paths(&args.out)?;
51 fs::create_dir_all(&out_dir)
52 .with_context(|| format!("failed to create output dir {}", out_dir.display()))?;
53 let manifest_cache_path = cache_path
54 .parent()
55 .map(|dir| dir.join("component.manifest.json"));
56 let manifest_out_path = out_dir.join("component.manifest.json");
57 let mut wasm_out_path = wasm_override
58 .clone()
59 .unwrap_or_else(|| out_dir.join("component.wasm"));
60 if let Some(manifest_cache_path) = manifest_cache_path
61 && manifest_cache_path.exists()
62 {
63 let manifest_bytes = fs::read(&manifest_cache_path).with_context(|| {
64 format!(
65 "failed to read cached manifest {}",
66 manifest_cache_path.display()
67 )
68 })?;
69 fs::write(&manifest_out_path, &manifest_bytes)
70 .with_context(|| format!("failed to write manifest {}", manifest_out_path.display()))?;
71 let manifest: Value = serde_json::from_slice(&manifest_bytes).with_context(|| {
72 format!(
73 "failed to parse component.manifest.json from {}",
74 manifest_cache_path.display()
75 )
76 })?;
77 if let Some(component_wasm) = manifest
78 .get("artifacts")
79 .and_then(|artifacts| artifacts.get("component_wasm"))
80 .and_then(|value| value.as_str())
81 {
82 let candidate = PathBuf::from(component_wasm);
83 if wasm_override.is_none() {
84 wasm_out_path = normalize_under_root(&out_dir, &candidate).with_context(|| {
85 format!("invalid artifacts.component_wasm path `{}`", component_wasm)
86 })?;
87 if let Some(parent) = wasm_out_path.parent() {
88 fs::create_dir_all(parent).with_context(|| {
89 format!("failed to create output dir {}", parent.display())
90 })?;
91 }
92 }
93 }
94 }
95 fs::copy(&cache_path, &wasm_out_path).with_context(|| {
96 format!(
97 "failed to copy cached component {} to {}",
98 cache_path.display(),
99 wasm_out_path.display()
100 )
101 })?;
102 println!(
103 "Wrote {} (digest {}) for source {}",
104 wasm_out_path.display(),
105 resolved.digest,
106 source,
107 );
108 if manifest_out_path.exists() {
109 println!("Wrote {}", manifest_out_path.display());
110 }
111 Ok(())
112}
113
114fn resolve_source(source: &str) -> Result<String> {
115 let (prefix, path_str) = if let Some(rest) = source.strip_prefix("file://") {
116 ("file://", rest)
117 } else {
118 ("", source)
119 };
120 let path = Path::new(path_str);
121 if !path.is_dir() {
122 return Ok(source.to_string());
123 }
124
125 let manifest_path = path.join("component.manifest.json");
126 if manifest_path.exists() {
127 let manifest_bytes = fs::read(&manifest_path).with_context(|| {
128 format!(
129 "failed to read component.manifest.json at {}",
130 manifest_path.display()
131 )
132 })?;
133 let manifest: Value = serde_json::from_slice(&manifest_bytes).with_context(|| {
134 format!(
135 "failed to parse component.manifest.json at {}",
136 manifest_path.display()
137 )
138 })?;
139 if let Some(component_wasm) = manifest
140 .get("artifacts")
141 .and_then(|artifacts| artifacts.get("component_wasm"))
142 .and_then(|value| value.as_str())
143 {
144 let wasm_path =
145 normalize_under_root(path, Path::new(component_wasm)).with_context(|| {
146 format!("invalid artifacts.component_wasm path `{}`", component_wasm)
147 })?;
148 return Ok(format!("{prefix}{}", wasm_path.display()));
149 }
150 }
151
152 let wasm_path = path.join("component.wasm");
153 if wasm_path.exists() {
154 return Ok(format!("{prefix}{}", wasm_path.display()));
155 }
156
157 Err(anyhow!(
158 "source directory {} does not contain component.manifest.json or component.wasm",
159 path.display()
160 ))
161}
162
163fn resolve_output_paths(out: &std::path::Path) -> Result<(PathBuf, Option<PathBuf>)> {
164 if out.exists() {
165 if out.is_dir() {
166 return Ok((out.to_path_buf(), None));
167 }
168 if let Some(parent) = out.parent() {
169 return Ok((parent.to_path_buf(), Some(out.to_path_buf())));
170 }
171 return Ok((PathBuf::from("."), Some(out.to_path_buf())));
172 }
173
174 if out.extension().is_some() {
175 let parent = out.parent().unwrap_or_else(|| std::path::Path::new("."));
176 return Ok((parent.to_path_buf(), Some(out.to_path_buf())));
177 }
178
179 Ok((out.to_path_buf(), None))
180}