Skip to main content

malbox_plugin_sdk/runtime/guest/
collector.rs

1//! Auto-collection of artifact and external log files after task execution.
2//!
3//! After `GuestPlugin::on_stop` returns, the runtime walks the artifact and
4//! external-log directories and sends any files that were not already
5//! explicitly sent (or marked as collected) by the plugin.
6
7use crate::context::Context;
8use crate::result::PluginResult;
9use globset::{Glob, GlobSetBuilder};
10use std::collections::HashSet;
11use std::path::{Path, PathBuf};
12use tracing::{debug, warn};
13
14/// Resolved auto-collection settings for a single directory.
15#[derive(Debug, Clone)]
16pub(crate) struct AutoCollectSection {
17    pub enabled: bool,
18    pub include: Vec<String>,
19    pub exclude: Vec<String>,
20    pub max_file_size: u64,
21}
22
23/// Collect files from `dir` and send them as results via `ctx.results().push()`.
24///
25/// When `claimed_paths` is `Some`, files whose canonical path appears in the
26/// set are skipped (artifact dedup). When `None`, all matching files are sent
27/// (external log collection - no dedup).
28pub(crate) fn auto_collect(
29    ctx: &Context,
30    dir: &Path,
31    config: &AutoCollectSection,
32    claimed_paths: Option<&HashSet<PathBuf>>,
33    result_prefix: &str,
34) {
35    if !config.enabled {
36        return;
37    }
38
39    if !dir.exists() {
40        debug!(dir = %dir.display(), "auto-collect directory does not exist, skipping");
41        return;
42    }
43
44    let include_set = match build_globset(&config.include) {
45        Ok(s) => s,
46        Err(e) => {
47            warn!(error = %e, "failed to compile auto-collect include patterns, skipping");
48            return;
49        }
50    };
51
52    let exclude_set = match build_globset(&config.exclude) {
53        Ok(s) => s,
54        Err(e) => {
55            warn!(error = %e, "failed to compile auto-collect exclude patterns, skipping");
56            return;
57        }
58    };
59
60    let entries = match collect_files(dir) {
61        Ok(e) => e,
62        Err(e) => {
63            warn!(error = %e, dir = %dir.display(), "failed to walk auto-collect directory");
64            return;
65        }
66    };
67
68    for file_path in entries {
69        let relative = match file_path.strip_prefix(dir) {
70            Ok(r) => r,
71            Err(_) => continue,
72        };
73
74        let rel_str = relative.to_string_lossy().into_owned();
75
76        if !include_set.is_match(relative) {
77            continue;
78        }
79        if exclude_set.is_match(relative) {
80            continue;
81        }
82
83        if let Some(claimed) = claimed_paths
84            && let Ok(canonical) = std::fs::canonicalize(&file_path)
85            && claimed.contains(&canonical)
86        {
87            debug!(path = %rel_str, "skipping already-claimed artifact");
88            continue;
89        }
90
91        let size = match std::fs::metadata(&file_path) {
92            Ok(m) => m.len(),
93            Err(e) => {
94                warn!(path = %rel_str, error = %e, "failed to stat file, skipping");
95                continue;
96            }
97        };
98
99        if size > config.max_file_size {
100            debug!(
101                path = %rel_str,
102                size,
103                max = config.max_file_size,
104                "file exceeds max_file_size, skipping"
105            );
106            continue;
107        }
108
109        let result_name = if result_prefix.is_empty() {
110            rel_str.to_string()
111        } else {
112            format!("{result_prefix}/{rel_str}")
113        };
114
115        if let Err(e) = ctx
116            .results()
117            .push(PluginResult::file(result_name.clone(), &file_path))
118        {
119            warn!(
120                path = %file_path.display(),
121                error = %e,
122                "failed to auto-collect file"
123            );
124        } else {
125            debug!(result_name, "auto-collected file");
126        }
127    }
128}
129
130fn build_globset(patterns: &[String]) -> std::result::Result<globset::GlobSet, globset::Error> {
131    let mut builder = GlobSetBuilder::new();
132    for pattern in patterns {
133        builder.add(Glob::new(pattern)?);
134    }
135    builder.build()
136}
137
138fn collect_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
139    let mut files = Vec::new();
140    collect_files_recursive(dir, &mut files)?;
141    files.sort();
142    Ok(files)
143}
144
145fn collect_files_recursive(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
146    for entry in std::fs::read_dir(dir)? {
147        let entry = entry?;
148        let path = entry.path();
149        if path.is_dir() {
150            collect_files_recursive(&path, files)?;
151        } else if path.is_file() {
152            files.push(path);
153        }
154    }
155    Ok(())
156}