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