capsula_file_context/
lib.rs

1mod config;
2mod hash;
3
4use crate::config::FileContextFactory;
5use crate::hash::file_digest_sha256;
6use capsula_core::captured::Captured;
7use capsula_core::context::{Context, ContextFactory, RuntimeParams};
8use capsula_core::error::{CapsulaError, CoreResult};
9use globwalk::GlobWalkerBuilder;
10use serde::{Deserialize, Serialize};
11use std::path::{Path, PathBuf};
12
13pub const KEY: &str = "file";
14
15#[derive(Debug, Clone, Deserialize, Serialize)]
16#[serde(rename_all = "lowercase")]
17pub enum CaptureMode {
18    Copy,
19    Move,
20    None,
21}
22impl Default for CaptureMode {
23    fn default() -> Self {
24        CaptureMode::Copy
25    }
26}
27
28#[derive(Debug, Clone, Deserialize, Serialize)]
29#[serde(rename_all = "lowercase")]
30pub enum HashAlgorithm {
31    Sha256,
32    // Md5,
33    None,
34}
35
36impl Default for HashAlgorithm {
37    fn default() -> Self {
38        HashAlgorithm::Sha256
39    }
40}
41
42#[derive(Debug)]
43pub struct FileContext {
44    pub glob: String,
45    pub mode: CaptureMode,
46    pub hash: HashAlgorithm,
47}
48
49#[derive(Debug)]
50pub struct FileCapturedPerFile {
51    pub path: PathBuf,
52    pub copied_path: Option<PathBuf>,
53    pub hash: Option<String>,
54}
55
56#[derive(Debug)]
57pub struct FileCaptured {
58    pub files: Vec<FileCapturedPerFile>,
59}
60
61impl Captured for FileCaptured {
62    fn to_json(&self) -> serde_json::Value {
63        serde_json::json!({
64            "type": KEY.to_string(),
65            "files": self.files.iter().map(|f| {
66                serde_json::json!({
67                    "path": f.path.to_string_lossy(),
68                    "copied_path": f.copied_path.as_ref().map(|p| p.to_string_lossy()),
69                    "hash": f.hash,
70                })
71            }).collect::<Vec<_>>(),
72        })
73    }
74}
75
76impl Context for FileContext {
77    type Output = FileCaptured;
78
79    fn run(&self, params: &RuntimeParams) -> CoreResult<Self::Output> {
80        GlobWalkerBuilder::from_patterns(&params.project_root, &[&self.glob])
81            .max_depth(1)
82            .build()
83            .map_err(|e| CapsulaError::from(std::io::Error::new(std::io::ErrorKind::Other, e)))?
84            .filter_map(Result::ok)
85            .map(|entry| self.capture_file(entry.path(), &params))
86            .collect::<Result<Vec<_>, CapsulaError>>()
87            .map(|files| FileCaptured { files })
88    }
89}
90
91impl FileContext {
92    fn capture_file(
93        &self,
94        path: &Path,
95        runtime_params: &RuntimeParams,
96    ) -> CoreResult<FileCapturedPerFile> {
97        // Compute hash if needed
98        let hash = match self.hash {
99            HashAlgorithm::Sha256 => Some(format!("sha256:{}", file_digest_sha256(path)?)),
100            HashAlgorithm::None => None,
101        };
102
103        // Copy or move file if needed
104        let copied_path = match self.mode {
105            CaptureMode::Copy | CaptureMode::Move => {
106                let run_dir = runtime_params.run_dir.as_ref().ok_or_else(|| {
107                    CapsulaError::from(std::io::Error::new(
108                        std::io::ErrorKind::InvalidInput,
109                        "run_dir is required for Copy or Move mode",
110                    ))
111                })?;
112                let file_name = path
113                    .file_name()
114                    .ok_or_else(|| {
115                        CapsulaError::from(std::io::Error::new(
116                            std::io::ErrorKind::InvalidInput,
117                            "Invalid file name",
118                        ))
119                    })?
120                    .to_os_string();
121                let dest_path = run_dir.join(file_name);
122                match self.mode {
123                    CaptureMode::Copy => {
124                        std::fs::copy(path, &dest_path)?;
125                    }
126                    CaptureMode::Move => {
127                        std::fs::rename(path, &dest_path)?;
128                    }
129                    _ => unreachable!(),
130                }
131                Some(dest_path)
132            }
133            CaptureMode::None => None,
134        };
135
136        Ok(FileCapturedPerFile {
137            path: path.to_path_buf(),
138            copied_path,
139            hash,
140        })
141    }
142}
143
144pub fn create_factory() -> Box<dyn ContextFactory> {
145    Box::new(FileContextFactory)
146}