1use super::Error;
2use crate::preprocessor::preprocessor;
3use crate::settings::PrintOutput;
4use flate2::read::GzDecoder;
5use log::debug;
6use phlow_sdk::{prelude::*, valu3};
7use reqwest::Client;
8use reqwest::header::AUTHORIZATION;
9use std::fs;
10use std::io::Cursor;
11use std::path::{Path, PathBuf};
12use tar::Archive;
13use zip::ZipArchive;
14
15pub struct ScriptLoaded {
16 pub script: Value,
17 pub script_file_path: String,
18}
19
20use crate::analyzer::Analyzer;
21
22pub async fn load_script(
23 script_target: &str,
24 print_yaml: bool,
25 print_output: PrintOutput,
26 analyzer: Option<&Analyzer>,
27) -> Result<ScriptLoaded, Error> {
28 let script_file_path = match resolve_script_path(script_target).await {
29 Ok(path) => path,
30 Err(err) => return Err(err),
31 };
32
33 let file: String = match std::fs::read_to_string(&script_file_path) {
34 Ok(file) => file,
35 Err(_) => return Err(Error::ModuleNotFound(script_file_path.to_string())),
36 };
37
38 let script = resolve_script(&file, script_file_path.clone(), print_yaml, print_output)
39 .map_err(|err| {
40 Error::ModuleLoaderError(format!(
41 "Failed to resolve script: {}. Error: {}",
42 script_file_path, err
43 ))
44 })?;
45
46 if let Some(a) = analyzer {
48 if a.enabled {
49 match a.run().await {
51 Ok(result) => {
52 a.display(&result);
53 }
54 Err(err) => {
55 eprintln!("Analyzer error: {:?}", err);
56 }
57 }
58 }
59 }
60
61 Ok(ScriptLoaded {
62 script,
63 script_file_path,
64 })
65}
66
67fn get_remote_path() -> Result<PathBuf, Error> {
68 let remote_path = PathBuf::from("phlow_remote");
69
70 if remote_path.exists() {
71 fs::remove_dir_all(&remote_path).map_err(|e| {
73 Error::ModuleLoaderError(format!("Failed to remove remote path: {}", e))
74 })?;
75 }
76
77 fs::create_dir_all(&remote_path)
78 .map_err(|e| Error::ModuleLoaderError(format!("Failed to create remote dir: {}", e)))?;
79
80 Ok(remote_path)
81}
82
83fn clone_git_repo(url: &str, branch: Option<&str>) -> Result<String, Error> {
84 use git2::{FetchOptions, RemoteCallbacks, build::RepoBuilder};
85
86 let remote_path = get_remote_path()?;
87
88 let mut callbacks = RemoteCallbacks::new();
89
90 callbacks.certificate_check(|_cert, _valid| {
92 Ok(git2::CertificateCheckStatus::CertificateOk)
95 });
96
97 if url.contains("@") {
98 debug!("Using SSH authentication for Git: {}", url);
99 if let Some(ssh_user) = url.split('@').next() {
100 let id_rsa_path: String = match std::env::var("PHLOW_REMOTE_ID_RSA_PATH") {
101 Ok(path) => path,
102 Err(_) => {
103 let home = std::env::var("HOME").map_err(|_| {
104 Error::ModuleLoaderError(
105 "HOME not set and PHLOW_REMOTE_ID_RSA_PATH not set".to_string(),
106 )
107 })?;
108 format!("{}/.ssh/id_rsa", home)
109 }
110 };
111
112 debug!("Using SSH user: {}", ssh_user);
113 debug!("Using SSH key path: {}", id_rsa_path);
114
115 if !Path::new(&id_rsa_path).exists() {
116 return Err(Error::ModuleLoaderError(format!(
117 "SSH key not found at path: {}",
118 id_rsa_path
119 )));
120 }
121
122 let id_rsa_path = id_rsa_path.clone();
123
124 callbacks.credentials(move |_url, username_from_url, _allowed_types| {
125 git2::Cred::ssh_key(
126 username_from_url.unwrap_or(ssh_user),
127 None,
128 std::path::Path::new(&id_rsa_path),
129 None,
130 )
131 });
132 }
133 }
134
135 let mut fetch_options = FetchOptions::new();
136 fetch_options.remote_callbacks(callbacks);
137
138 let mut builder = RepoBuilder::new();
139 builder.fetch_options(fetch_options);
140
141 if let Some(branch_name) = branch {
142 builder.branch(branch_name);
143 }
144
145 let repo = builder
146 .clone(url, &remote_path)
147 .map_err(|e| Error::ModuleLoaderError(format!("Git clone failed: {}", e)))?;
148
149 if let Some(branch_name) = branch {
150 let (object, reference) = repo.revparse_ext(branch_name).map_err(|e| {
151 Error::ModuleLoaderError(format!("Branch `{}` not found: {}", branch_name, e))
152 })?;
153
154 repo.set_head(
155 reference
156 .and_then(|r| r.name().map(|s| s.to_string()))
157 .ok_or_else(|| Error::ModuleLoaderError("Invalid branch ref".to_string()))?
158 .as_str(),
159 )
160 .map_err(|e| Error::ModuleLoaderError(format!("Failed to set HEAD: {}", e)))?;
161
162 repo.checkout_tree(&object, None)
163 .map_err(|e| Error::ModuleLoaderError(format!("Checkout failed: {}", e)))?;
164 }
165
166 let file_path = if let Ok(main_file) = std::env::var("PHLOW_MAIN_FILE") {
168 let specific_file_path = remote_path.join(&main_file);
169 if specific_file_path.exists() {
170 specific_file_path.to_str().unwrap_or_default().to_string()
171 } else {
172 return Err(Error::MainNotFound(format!(
173 "Specified file '{}' not found in repository '{}'",
174 main_file, url
175 )));
176 }
177 } else {
178 find_default_file(&remote_path).ok_or_else(|| Error::MainNotFound(url.to_string()))?
179 };
180
181 Ok(file_path)
182}
183
184async fn download_file(url: &str, inner_folder: Option<&str>) -> Result<String, Error> {
185 let client = Client::new();
186
187 let mut request = client.get(url);
188
189 if let Ok(auth_header) = std::env::var("PHLOW_REMOTE_HEADER_AUTHORIZATION") {
190 request = request.header(AUTHORIZATION, auth_header);
191 }
192
193 let response = request.send().await.map_err(Error::GetFileError)?;
194 let bytes = response.bytes().await.map_err(Error::BufferError)?;
195
196 let remote_path = get_remote_path()?;
197
198 if Archive::new(GzDecoder::new(Cursor::new(bytes.clone())))
199 .unpack(&remote_path)
200 .is_err()
201 {
202 if let Ok(mut archive) = ZipArchive::new(Cursor::new(bytes.clone())) {
203 archive
204 .extract(&remote_path)
205 .map_err(Error::ZipErrorError)?;
206 }
207 };
208
209 let effective_path = if let Some(inner_folder) = inner_folder {
210 remote_path.join(inner_folder)
211 } else {
212 let entries: Vec<_> = fs::read_dir(&remote_path)
213 .map_err(|e| Error::ModuleLoaderError(format!("Failed to read remote dir: {}", e)))?
214 .filter_map(Result::ok)
215 .collect();
216
217 if entries.len() == 1 && entries[0].path().is_dir() {
218 entries[0].path()
219 } else {
220 remote_path
221 }
222 };
223
224 let main_path = if let Ok(main_file) = std::env::var("PHLOW_MAIN_FILE") {
226 println!("Using specified main file: {}", main_file);
227 let specific_file_path = effective_path.join(&main_file);
228 if specific_file_path.exists() {
229 specific_file_path.to_str().unwrap_or_default().to_string()
230 } else {
231 return Err(Error::MainNotFound(format!(
232 "Specified file '{}' not found in downloaded archive '{}'",
233 main_file, url
234 )));
235 }
236 } else {
237 find_default_file(&effective_path).ok_or_else(|| Error::MainNotFound(url.to_string()))?
238 };
239
240 Ok(main_path)
241}
242
243async fn resolve_script_path(script_path: &str) -> Result<String, Error> {
244 let (target, branch) = if script_path.contains('#') {
245 let parts: Vec<&str> = script_path.split('#').collect();
246 (parts[0], Some(parts[1]))
247 } else {
248 (script_path, None)
249 };
250
251 if target.trim_end().ends_with(".git") {
252 return clone_git_repo(target, branch);
253 }
254
255 if target.starts_with("http://") || target.starts_with("https://") {
256 return download_file(target, branch).await;
257 }
258
259 let target_path = PathBuf::from(target);
260 if target_path.is_dir() {
261 return find_default_file(&target_path)
262 .ok_or_else(|| Error::MainNotFound(script_path.to_string()));
263 } else if target_path.exists() {
264 return Ok(target.to_string());
265 }
266
267 Err(Error::MainNotFound(script_path.to_string()))
268}
269
270fn resolve_script(
271 file: &str,
272 main_file_path: String,
273 print_yaml: bool,
274 print_output: PrintOutput,
275) -> Result<Value, Error> {
276 let mut value: Value = {
277 let script_path = Path::new(&main_file_path)
278 .parent()
279 .unwrap_or_else(|| Path::new("."));
280
281 let extension = Path::new(&main_file_path)
283 .extension()
284 .and_then(|s| s.to_str())
285 .unwrap_or("")
286 .to_lowercase();
287
288 let script: String = if extension == "yaml" || extension == "yml" || extension == "json" {
289 file.to_string()
291 } else {
292 preprocessor(&file, script_path, print_yaml, print_output).map_err(|errors| {
293 eprintln!("❌ Failed to transform YAML file: {}", main_file_path);
294 Error::ModuleLoaderError(format!(
295 "YAML transformation failed with {} error(s)",
296 errors.len()
297 ))
298 })?
299 };
300
301 if let Ok(yaml_show) = std::env::var("PHLOW_SCRIPT_SHOW") {
302 if yaml_show == "true" {
303 println!("YAML: {}", script);
304 }
305 }
306
307 if extension == "json" {
308 println!("Parsing JSON script");
309 valu3::value::Value::json_to_value(&script).map_err(Error::LoaderErrorJsonValu3)?
310 } else {
311 serde_yaml::from_str::<Value>(&script).map_err(Error::LoaderErrorScript)?
312 }
313 };
314
315 if value.get("steps").is_none() {
316 return Err(Error::StepsNotDefined);
317 }
318
319 if let Some(modules) = value.get("modules") {
320 if !modules.is_array() {
321 return Err(Error::ModuleLoaderError("Modules not an array".to_string()));
322 }
323
324 value.insert("modules", modules.clone());
325 } else {
326 value.insert("modules", Value::Array(phlow_sdk::prelude::Array::new()));
328 }
329
330 Ok(value)
331}
332
333pub fn load_external_module_info(module: &str) -> Value {
334 let module_path = format!("phlow_packages/{}/phlow.yaml", module);
335
336 if !Path::new(&module_path).exists() {
337 return Value::Null;
338 }
339
340 let file = match std::fs::read_to_string(&module_path) {
341 Ok(file) => file,
342 Err(_) => return Value::Null,
343 };
344
345 let mut input_order = Vec::new();
346
347 {
348 let value: serde_yaml::Value = match serde_yaml::from_str::<serde_yaml::Value>(&file) {
349 Ok(value) => value,
350 Err(err) => {
351 debug!(
352 "Failed to parse module metadata {}: {}",
353 module_path, err
354 );
355 return Value::Null;
356 }
357 };
358
359 if let Some(input) = value.get("input") {
360 if let serde_yaml::Value::Mapping(input) = input {
361 if let Some(serde_yaml::Value::String(input_type)) = input.get("type") {
362 if input_type == "object" {
363 if let Some(serde_yaml::Value::Mapping(properties)) =
364 input.get(&serde_yaml::Value::String("properties".to_string()))
365 {
366 for (key, _) in properties {
367 if let serde_yaml::Value::String(key) = key {
368 input_order.push(key.clone());
369 }
370 }
371 }
372 }
373 }
374 }
375 }
376
377 drop(value)
378 }
379
380 let mut value: Value = match serde_yaml::from_str::<Value>(&file) {
381 Ok(value) => value,
382 Err(err) => {
383 debug!(
384 "Failed to parse module metadata {}: {}",
385 module_path, err
386 );
387 return Value::Null;
388 }
389 };
390
391 value.insert("input_order".to_string(), input_order.to_value());
392
393 value
394}
395
396pub fn load_local_module_info(local_path: &str) -> Value {
397 debug!("load_local_module_info");
398 let module_path = format!("{}/phlow.yaml", local_path);
399
400 if !Path::new(&module_path).exists() {
401 debug!("phlow.yaml not exists");
402 return Value::Null;
403 }
404
405 let file = match std::fs::read_to_string(&module_path) {
406 Ok(file) => file,
407 Err(_) => return Value::Null,
408 };
409
410 let mut input_order = Vec::new();
411
412 {
413 let value: serde_yaml::Value = match serde_yaml::from_str::<serde_yaml::Value>(&file) {
414 Ok(value) => value,
415 Err(err) => {
416 debug!(
417 "Failed to parse module metadata {}: {}",
418 module_path, err
419 );
420 return Value::Null;
421 }
422 };
423
424 if let Some(input) = value.get("input") {
425 if let serde_yaml::Value::Mapping(input) = input {
426 if let Some(serde_yaml::Value::String(input_type)) = input.get("type") {
427 if input_type == "object" {
428 if let Some(serde_yaml::Value::Mapping(properties)) =
429 input.get(&serde_yaml::Value::String("properties".to_string()))
430 {
431 for (key, _) in properties {
432 if let serde_yaml::Value::String(key) = key {
433 input_order.push(key.clone());
434 }
435 }
436 }
437 }
438 }
439 }
440 }
441
442 drop(value)
443 }
444
445 let mut value: Value = match serde_yaml::from_str::<Value>(&file) {
446 Ok(value) => value,
447 Err(err) => {
448 debug!(
449 "Failed to parse module metadata {}: {}",
450 module_path, err
451 );
452 return Value::Null;
453 }
454 };
455
456 value.insert("input_order".to_string(), input_order.to_value());
457
458 value
459}
460
461fn find_default_file(base: &PathBuf) -> Option<String> {
462 if base.is_file() {
463 return Some(base.to_str().unwrap_or_default().to_string());
464 }
465
466 if base.is_dir() {
467 {
468 let mut base_path = base.clone();
469 base_path.set_extension("phlow");
470
471 if base_path.exists() {
472 return Some(base_path.to_str().unwrap_or_default().to_string());
473 }
474 }
475
476 let files = vec!["main.phlow", "mod.phlow", "module.phlow"];
477
478 for file in files {
479 let file_path = base.join(file);
480
481 if file_path.exists() {
482 return Some(file_path.to_str().unwrap_or_default().to_string());
483 }
484 }
485 }
486
487 None
488}