capsula_file_context/
lib.rs1mod 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 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(¶ms.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(), ¶ms))
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 let hash = match self.hash {
99 HashAlgorithm::Sha256 => Some(format!("sha256:{}", file_digest_sha256(path)?)),
100 HashAlgorithm::None => None,
101 };
102
103 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}