Skip to main content

greentic_component/cmd/
store.rs

1use 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 a component from a source and write the wasm bytes to disk
15    Fetch(StoreFetchArgs),
16}
17
18#[derive(Args, Debug, Clone)]
19pub struct StoreFetchArgs {
20    /// Destination directory for the fetched component bytes
21    #[arg(long, value_name = "DIR")]
22    pub out: PathBuf,
23    /// Optional cache directory for fetched components
24    #[arg(long, value_name = "DIR")]
25    pub cache_dir: Option<PathBuf>,
26    /// Source reference to resolve (file://, oci://, repo://, store://, etc.)
27    #[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}