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::cmd::i18n;
9use crate::path_safety::normalize_under_root;
10use greentic_distributor_client::{CachePolicy, DistClient, DistOptions, ResolvePolicy};
11
12#[derive(Subcommand, Debug, Clone)]
13pub enum StoreCommand {
14 Fetch(StoreFetchArgs),
16}
17
18#[derive(Args, Debug, Clone)]
19pub struct StoreFetchArgs {
20 #[arg(long, value_name = "DIR")]
22 pub out: PathBuf,
23 #[arg(long, value_name = "DIR")]
25 pub cache_dir: Option<PathBuf>,
26 #[arg(value_name = "SOURCE")]
28 pub source: String,
29}
30
31pub fn run(command: StoreCommand) -> Result<()> {
32 match command {
33 StoreCommand::Fetch(args) => fetch(args),
34 }
35}
36
37fn fetch(args: StoreFetchArgs) -> Result<()> {
38 let source = resolve_source(&args.source)?;
39 let mut opts = DistOptions::default();
40 if let Some(cache_dir) = &args.cache_dir {
41 opts.cache_dir = cache_dir.clone();
42 }
43 let client = DistClient::new(opts);
44 let rt =
45 tokio::runtime::Runtime::new().context(i18n::tr_lit("failed to create async runtime"))?;
46 let parsed = client.parse_source(&source)?;
47 let resolved = rt
48 .block_on(async {
49 let descriptor = client.resolve(parsed, ResolvePolicy).await?;
50 client.fetch(&descriptor, CachePolicy).await
51 })
52 .context(i18n::tr_lit("store fetch failed"))?;
53 let cache_path = resolved
54 .cache_path
55 .ok_or_else(|| anyhow!(i18n::tr_lit("resolved source has no cached component path")))?;
56 let (out_dir, wasm_override) = resolve_output_paths(&args.out)?;
57 fs::create_dir_all(&out_dir).with_context(|| {
58 i18n::tr_lit("failed to create output dir {}").replacen(
59 "{}",
60 &out_dir.display().to_string(),
61 1,
62 )
63 })?;
64 let manifest_cache_path = cache_path
65 .parent()
66 .map(|dir| dir.join("component.manifest.json"));
67 let manifest_out_path = out_dir.join("component.manifest.json");
68 let mut wasm_out_path = wasm_override
69 .clone()
70 .unwrap_or_else(|| out_dir.join("component.wasm"));
71 if let Some(manifest_cache_path) = manifest_cache_path
72 && manifest_cache_path.exists()
73 {
74 let manifest_bytes = fs::read(&manifest_cache_path).with_context(|| {
75 i18n::tr_lit("failed to read cached manifest {}").replacen(
76 "{}",
77 &manifest_cache_path.display().to_string(),
78 1,
79 )
80 })?;
81 fs::write(&manifest_out_path, &manifest_bytes).with_context(|| {
82 i18n::tr_lit("failed to write manifest {}").replacen(
83 "{}",
84 &manifest_out_path.display().to_string(),
85 1,
86 )
87 })?;
88 let manifest: Value = serde_json::from_slice(&manifest_bytes).with_context(|| {
89 i18n::tr_lit("failed to parse component.manifest.json from {}").replacen(
90 "{}",
91 &manifest_cache_path.display().to_string(),
92 1,
93 )
94 })?;
95 if let Some(component_wasm) = manifest
96 .get("artifacts")
97 .and_then(|artifacts| artifacts.get("component_wasm"))
98 .and_then(|value| value.as_str())
99 {
100 let candidate = PathBuf::from(component_wasm);
101 if wasm_override.is_none() {
102 wasm_out_path = normalize_under_root(&out_dir, &candidate).with_context(|| {
103 i18n::tr_lit("invalid artifacts.component_wasm path `{}`").replacen(
104 "{}",
105 component_wasm,
106 1,
107 )
108 })?;
109 if let Some(parent) = wasm_out_path.parent() {
110 fs::create_dir_all(parent).with_context(|| {
111 i18n::tr_lit("failed to create output dir {}").replacen(
112 "{}",
113 &parent.display().to_string(),
114 1,
115 )
116 })?;
117 }
118 }
119 }
120 }
121 fs::copy(&cache_path, &wasm_out_path).with_context(|| {
122 i18n::tr_lit("failed to copy cached component {} to {}")
123 .replacen("{}", &cache_path.display().to_string(), 1)
124 .replacen("{}", &wasm_out_path.display().to_string(), 1)
125 })?;
126 println!(
127 "{}",
128 i18n::tr_lit("Wrote {} (digest {}) for source {}")
129 .replacen("{}", &wasm_out_path.display().to_string(), 1)
130 .replacen("{}", &resolved.digest.to_string(), 1)
131 .replacen("{}", &source, 1)
132 );
133 if manifest_out_path.exists() {
134 println!(
135 "{}",
136 i18n::tr_lit("Wrote {}").replacen("{}", &manifest_out_path.display().to_string(), 1)
137 );
138 }
139 Ok(())
140}
141
142fn resolve_source(source: &str) -> Result<String> {
143 let (prefix, path_str) = if let Some(rest) = source.strip_prefix("file://") {
144 ("file://", rest)
145 } else {
146 ("", source)
147 };
148 let path = Path::new(path_str);
149 if !path.is_dir() {
150 return Ok(source.to_string());
151 }
152
153 let manifest_path = path.join("component.manifest.json");
154 if manifest_path.exists() {
155 let manifest_bytes = fs::read(&manifest_path).with_context(|| {
156 i18n::tr_lit("failed to read component.manifest.json at {}").replacen(
157 "{}",
158 &manifest_path.display().to_string(),
159 1,
160 )
161 })?;
162 let manifest: Value = serde_json::from_slice(&manifest_bytes).with_context(|| {
163 i18n::tr_lit("failed to parse component.manifest.json at {}").replacen(
164 "{}",
165 &manifest_path.display().to_string(),
166 1,
167 )
168 })?;
169 if let Some(component_wasm) = manifest
170 .get("artifacts")
171 .and_then(|artifacts| artifacts.get("component_wasm"))
172 .and_then(|value| value.as_str())
173 {
174 let wasm_path =
175 normalize_under_root(path, Path::new(component_wasm)).with_context(|| {
176 i18n::tr_lit("invalid artifacts.component_wasm path `{}`").replacen(
177 "{}",
178 component_wasm,
179 1,
180 )
181 })?;
182 return Ok(format!("{prefix}{}", wasm_path.display()));
183 }
184 }
185
186 let wasm_path = path.join("component.wasm");
187 if wasm_path.exists() {
188 return Ok(format!("{prefix}{}", wasm_path.display()));
189 }
190
191 Err(anyhow!(
192 "{}",
193 i18n::tr_lit(
194 "source directory {} does not contain component.manifest.json or component.wasm"
195 )
196 .replacen("{}", &path.display().to_string(), 1)
197 ))
198}
199
200fn resolve_output_paths(out: &std::path::Path) -> Result<(PathBuf, Option<PathBuf>)> {
201 if out.exists() {
202 if out.is_dir() {
203 return Ok((out.to_path_buf(), None));
204 }
205 if let Some(parent) = out.parent() {
206 return Ok((parent.to_path_buf(), Some(out.to_path_buf())));
207 }
208 return Ok((PathBuf::from("."), Some(out.to_path_buf())));
209 }
210
211 if out.extension().is_some() {
212 let parent = out.parent().unwrap_or_else(|| std::path::Path::new("."));
213 return Ok((parent.to_path_buf(), Some(out.to_path_buf())));
214 }
215
216 Ok((out.to_path_buf(), None))
217}