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 = ureq::get(url)
127 .call()
128 .map_err(|err| anyhow::anyhow!("failed to fetch {url}: {err}"))?;
129 let bytes = response
130 .into_body()
131 .read_to_vec()
132 .map_err(|err| anyhow::anyhow!("failed to read {url}: {err}"))?;
133
134 let nonce = SystemTime::now()
135 .duration_since(UNIX_EPOCH)
136 .unwrap_or_default()
137 .as_nanos();
138 let base = std::env::temp_dir().join(format!("greentic-setup-remote-{nonce}"));
139 fs::create_dir_all(&base)?;
140
141 let file_name = url
142 .rsplit('/')
143 .next()
144 .filter(|value| !value.is_empty())
145 .unwrap_or("bundle.gtbundle");
146 let archive_path = base.join(file_name);
147 fs::write(&archive_path, bytes)?;
148
149 if !gtbundle::is_gtbundle_file(&archive_path) {
150 bail!("remote bundle URL must point to a .gtbundle archive: {url}");
151 }
152
153 gtbundle::extract_gtbundle_to_temp(&archive_path)
154}
155
156pub fn resolve_bundle_dir(bundle: Option<PathBuf>) -> Result<PathBuf> {
158 match bundle {
159 Some(path) => Ok(path),
160 None => std::env::current_dir().context("failed to get current directory"),
161 }
162}
163
164pub fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf, _only_used: bool) -> Result<()> {
166 if !src.is_dir() {
167 bail!("source is not a directory: {}", src.display());
168 }
169
170 std::fs::create_dir_all(dst)?;
171
172 for entry in std::fs::read_dir(src)? {
173 let entry = entry?;
174 let src_path = entry.path();
175 let dst_path = dst.join(entry.file_name());
176
177 if src_path.is_dir() {
178 copy_dir_recursive(&src_path, &dst_path, _only_used)?;
179 } else {
180 std::fs::copy(&src_path, &dst_path)?;
181 }
182 }
183
184 Ok(())
185}
186
187pub fn detect_domain_from_filename(filename: &str) -> &'static str {
192 let stem = filename.strip_suffix(".gtpack").unwrap_or(filename);
193 if stem.starts_with("messaging-")
194 || stem.starts_with("state-")
195 || stem.starts_with("telemetry-")
196 {
197 "messaging"
198 } else if stem.starts_with("events-") || stem.starts_with("event-") {
199 "events"
200 } else if stem.starts_with("oauth-") {
201 "oauth"
202 } else if stem.starts_with("secrets-") {
203 "secrets"
204 } else {
205 "messaging"
206 }
207}
208
209pub fn resolve_pack_source(source: &str) -> Result<PathBuf> {
211 use crate::bundle_source::BundleSource;
212
213 let parsed = BundleSource::parse(source)?;
214
215 if parsed.is_local() {
216 let path = parsed.resolve()?;
217 if path.extension().and_then(|e| e.to_str()) != Some("gtpack") {
218 anyhow::bail!("Not a .gtpack file: {source}");
219 }
220 Ok(path)
221 } else {
222 println!(" Fetching from registry...");
223 let path = parsed.resolve()?;
224 println!(" Downloaded to cache: {}", path.display());
225 Ok(path)
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::{SetupOutputTarget, setup_output_target};
232 use std::env;
233 use std::path::Path;
234 use tempfile::tempdir;
235
236 #[test]
237 fn setup_output_target_uses_cwd_file_name_directory_for_remote_bundle_urls() {
238 let cwd = env::current_dir().expect("cwd");
239 let dir = tempdir().expect("tempdir");
240 env::set_current_dir(dir.path()).expect("set cwd");
241
242 let output = setup_output_target(Path::new(
243 "https://github.com/greenticai/greentic-demo/releases/download/v0.1.9/cloud-deploy-demo.gtbundle",
244 ))
245 .expect("output target")
246 .expect("some output target");
247
248 match output {
249 SetupOutputTarget::Directory(path) => {
250 let expected = env::current_dir()
251 .expect("cwd after set")
252 .join("cloud-deploy-demo.gtbundle");
253 assert_eq!(path, expected);
254 }
255 SetupOutputTarget::Archive(path) => {
256 panic!("expected directory output, got archive {}", path.display());
257 }
258 }
259
260 env::set_current_dir(cwd).expect("restore cwd");
261 }
262
263 #[test]
264 fn setup_output_target_updates_local_archives_in_place() {
265 let archive = Path::new("/tmp/demo-bundle.gtbundle");
266 let output = setup_output_target(archive)
267 .expect("output target")
268 .expect("some output target");
269 match output {
270 SetupOutputTarget::Archive(path) => assert_eq!(path, archive),
271 SetupOutputTarget::Directory(path) => {
272 panic!("expected archive output, got directory {}", path.display());
273 }
274 }
275 }
276
277 #[test]
278 fn setup_output_target_skips_local_bundle_directories() {
279 let output = setup_output_target(Path::new("./demo-bundle")).expect("output target");
280 assert!(output.is_none());
281 }
282}