Skip to main content

phlow_runtime/loader/
loader.rs

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 analyzer was provided and is enabled, run it using the script target
47    if let Some(a) = analyzer {
48        if a.enabled {
49            // run analyzer but ignore errors (we don't want to fail loading because of analyzer)
50            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        // remove
72        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    // Add certificate check callback to handle SSH host key verification
91    callbacks.certificate_check(|_cert, _valid| {
92        // Accept all certificates - you might want to implement proper host key checking
93        // in a production environment by verifying against known_hosts
94        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    // Check if a specific file is requested via environment variable
167    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    // Check if a specific file is requested via environment variable
225    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        // Se a extensão do arquivo for yaml ou yml, não executar o preprocessor
282        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            // Usar o conteúdo original do arquivo quando for YAML
290            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        // Se modules não foi definido, criar uma lista vazia
327        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}