greentic_setup/cli_helpers/
bundle.rs1use std::fs;
7use std::path::{Path, PathBuf};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use anyhow::{Context, Result, bail};
11use url::Url;
12
13use crate::cli_i18n::CliI18n;
14
15pub fn resolve_bundle_source(path: &std::path::Path, i18n: &CliI18n) -> Result<PathBuf> {
17 use crate::gtbundle;
18
19 let path_str = path.to_string_lossy();
20 if path_str.starts_with("https://") || path_str.starts_with("http://") {
21 println!("{}", i18n.t("cli.simple.extracting"));
22 let temp_dir = download_and_extract_remote_bundle(&path_str)
23 .context("failed to fetch and extract remote bundle archive")?;
24 println!(
25 "{}",
26 i18n.tf(
27 "cli.simple.extracted_to",
28 &[&temp_dir.display().to_string()]
29 )
30 );
31 return Ok(temp_dir);
32 }
33
34 if gtbundle::is_gtbundle_file(path) {
35 println!("{}", i18n.t("cli.simple.extracting"));
36 let temp_dir = gtbundle::extract_gtbundle_to_temp(path)
37 .context("failed to extract .gtbundle archive")?;
38 println!(
39 "{}",
40 i18n.tf(
41 "cli.simple.extracted_to",
42 &[&temp_dir.display().to_string()]
43 )
44 );
45 return Ok(temp_dir);
46 }
47
48 if gtbundle::is_gtbundle_dir(path) {
49 return Ok(path.to_path_buf());
50 }
51 if path_str.ends_with(".gtbundle") && !path.exists() {
52 bail!(
53 "{}",
54 i18n.tf(
55 "setup.error.bundle_not_found",
56 &[&path.display().to_string()]
57 )
58 );
59 }
60
61 if path.is_dir() {
62 Ok(path.to_path_buf())
63 } else if path.exists() {
64 bail!(
65 "{}",
66 i18n.tf(
67 "cli.simple.expected_bundle_format",
68 &[&path.display().to_string()]
69 )
70 );
71 } else {
72 bail!(
73 "{}",
74 i18n.tf(
75 "setup.error.bundle_not_found",
76 &[&path.display().to_string()]
77 )
78 );
79 }
80}
81
82#[derive(Clone)]
84pub enum SetupOutputTarget {
85 Directory(PathBuf),
86 Archive(PathBuf),
87}
88
89pub fn setup_output_target(source: &Path) -> Result<Option<SetupOutputTarget>> {
98 let source_str = source.to_string_lossy();
99
100 if source_str.starts_with("https://") || source_str.starts_with("http://") {
101 let parsed =
102 Url::parse(&source_str).with_context(|| format!("invalid bundle URL: {source_str}"))?;
103 let file_name = parsed
104 .path_segments()
105 .and_then(|mut segments| segments.rfind(|segment| !segment.is_empty()))
106 .filter(|segment| segment.ends_with(".gtbundle"))
107 .ok_or_else(|| {
108 anyhow::anyhow!("remote bundle URL must point to a .gtbundle archive")
109 })?;
110 return Ok(Some(SetupOutputTarget::Directory(
111 std::env::current_dir()?.join(file_name),
112 )));
113 }
114
115 if source_str.ends_with(".gtbundle") {
116 return Ok(Some(SetupOutputTarget::Archive(source.to_path_buf())));
117 }
118
119 Ok(None)
120}
121
122fn download_and_extract_remote_bundle(url: &str) -> Result<PathBuf> {
124 use crate::gtbundle;
125
126 let response = crate::http_client::download_agent()
127 .get(url)
128 .call()
129 .map_err(|err| anyhow::anyhow!("failed to fetch {url}: {err}"))?;
130 let bytes = response
131 .into_body()
132 .read_to_vec()
133 .map_err(|err| anyhow::anyhow!("failed to read {url}: {err}"))?;
134
135 let nonce = SystemTime::now()
136 .duration_since(UNIX_EPOCH)
137 .unwrap_or_default()
138 .as_nanos();
139 let base = std::env::temp_dir().join(format!("greentic-setup-remote-{nonce}"));
140 fs::create_dir_all(&base)?;
141
142 let file_name = url
143 .rsplit('/')
144 .next()
145 .filter(|value| !value.is_empty())
146 .unwrap_or("bundle.gtbundle");
147 let archive_path = base.join(file_name);
148 fs::write(&archive_path, bytes)?;
149
150 if !gtbundle::is_gtbundle_file(&archive_path) {
151 bail!("remote bundle URL must point to a .gtbundle archive: {url}");
152 }
153
154 gtbundle::extract_gtbundle_to_temp(&archive_path)
155}
156
157pub fn resolve_bundle_dir(bundle: Option<PathBuf>) -> Result<PathBuf> {
159 match bundle {
160 Some(path) => Ok(path),
161 None => std::env::current_dir().context("failed to get current directory"),
162 }
163}
164
165pub fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf, _only_used: bool) -> Result<()> {
167 if !src.is_dir() {
168 bail!("source is not a directory: {}", src.display());
169 }
170
171 std::fs::create_dir_all(dst)?;
172
173 for entry in std::fs::read_dir(src)? {
174 let entry = entry?;
175 let src_path = entry.path();
176 let dst_path = dst.join(entry.file_name());
177
178 if src_path.is_dir() {
179 copy_dir_recursive(&src_path, &dst_path, _only_used)?;
180 } else {
181 std::fs::copy(&src_path, &dst_path)?;
182 }
183 }
184
185 Ok(())
186}
187
188pub fn detect_domain_from_filename(filename: &str) -> &'static str {
193 let stem = filename.strip_suffix(".gtpack").unwrap_or(filename);
194 if stem.starts_with("messaging-")
195 || stem.starts_with("state-")
196 || stem.starts_with("telemetry-")
197 {
198 "messaging"
199 } else if stem.starts_with("events-") || stem.starts_with("event-") {
200 "events"
201 } else if stem.starts_with("oauth-") {
202 "oauth"
203 } else if stem.starts_with("secrets-") {
204 "secrets"
205 } else {
206 "messaging"
207 }
208}
209
210pub fn resolve_pack_source(source: &str) -> Result<PathBuf> {
212 use crate::bundle_source::BundleSource;
213
214 let parsed = BundleSource::parse(source)?;
215
216 if parsed.is_local() {
217 let path = parsed.resolve()?;
218 if path.extension().and_then(|e| e.to_str()) != Some("gtpack") {
219 anyhow::bail!("Not a .gtpack file: {source}");
220 }
221 Ok(path)
222 } else {
223 println!(" Fetching from registry...");
224 let path = parsed.resolve()?;
225 println!(" Downloaded to cache: {}", path.display());
226 Ok(path)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::{SetupOutputTarget, setup_output_target};
233 use std::env;
234 use std::path::Path;
235 use tempfile::tempdir;
236
237 #[test]
238 fn setup_output_target_uses_cwd_file_name_directory_for_remote_bundle_urls() {
239 let cwd = env::current_dir().expect("cwd");
240 let dir = tempdir().expect("tempdir");
241 env::set_current_dir(dir.path()).expect("set cwd");
242
243 let output = setup_output_target(Path::new(
244 "https://github.com/greenticai/greentic-demo/releases/download/v0.1.9/cloud-deploy-demo.gtbundle",
245 ))
246 .expect("output target")
247 .expect("some output target");
248
249 match output {
250 SetupOutputTarget::Directory(path) => {
251 let expected = env::current_dir()
252 .expect("cwd after set")
253 .join("cloud-deploy-demo.gtbundle");
254 assert_eq!(path, expected);
255 }
256 SetupOutputTarget::Archive(path) => {
257 panic!("expected directory output, got archive {}", path.display());
258 }
259 }
260
261 env::set_current_dir(cwd).expect("restore cwd");
262 }
263
264 #[test]
265 fn setup_output_target_updates_local_archives_in_place() {
266 let archive = Path::new("/tmp/demo-bundle.gtbundle");
267 let output = setup_output_target(archive)
268 .expect("output target")
269 .expect("some output target");
270 match output {
271 SetupOutputTarget::Archive(path) => assert_eq!(path, archive),
272 SetupOutputTarget::Directory(path) => {
273 panic!("expected archive output, got directory {}", path.display());
274 }
275 }
276 }
277
278 #[test]
279 fn setup_output_target_skips_local_bundle_directories() {
280 let output = setup_output_target(Path::new("./demo-bundle")).expect("output target");
281 assert!(output.is_none());
282 }
283}