swiftide_docker_executor/
context_builder.rs1use std::{
2 os::unix::fs::MetadataExt as _,
3 path::{Path, PathBuf},
4};
5
6use ignore::gitignore::{Gitignore, GitignoreBuilder};
7use tokio::io::AsyncReadExt as _;
9use tokio_tar::{Builder, EntryType, Header};
10use walkdir::{DirEntry, WalkDir};
11
12use crate::ContextError;
13
14type ContextArchive = Vec<u8>;
15
16#[derive(Debug)]
17pub struct ContextBuilder {
18 context_path: PathBuf,
19 ignore: Gitignore,
20 global: Option<Gitignore>,
21}
22
23impl ContextBuilder {
24 pub fn from_path(context_path: impl Into<PathBuf>) -> Result<Self, ContextError> {
25 let path = context_path.into();
26 let mut gitignore = GitignoreBuilder::new(&path);
27
28 if let Some(err) = gitignore.add(path.join(".gitignore")) {
29 tracing::warn!(?err, "Error adding .gitignore");
30 }
31 if let Some(err) = gitignore.add(path.join(".dockerignore")) {
32 tracing::warn!(?err, "Error adding .dockerignore");
33 }
34
35 let gitignore = gitignore.build()?;
36
37 let (global_gitignore, maybe_error) = Gitignore::global();
38 let maybe_global = if let Some(err) = maybe_error {
39 tracing::warn!(?err, "Error adding global gitignore");
40 None
41 } else {
42 Some(global_gitignore)
43 };
44
45 Ok(Self {
46 context_path: path,
47 ignore: gitignore,
48 global: maybe_global,
49 })
50 }
51
52 fn is_ignored(&self, path: impl AsRef<Path>) -> bool {
53 let Ok(relative_path) = path.as_ref().strip_prefix(&self.context_path) else {
54 tracing::debug!(
55 "not ignoring {path} as it seems to be not prefixed by {prefix}",
56 path = path.as_ref().display(),
57 prefix = self.context_path.to_string_lossy()
58 );
59 return false;
60 };
61
62 if relative_path.starts_with(".git") {
63 tracing::debug!(
64 "not ignoring {path} as it seems to be a git file",
65 path = path.as_ref().display()
66 );
67 return false;
68 }
69
70 if let Some(global) = &self.global {
71 if global
72 .matched_path_or_any_parents(relative_path, false)
73 .is_ignore()
74 {
75 tracing::debug!(
76 "ignoring {path} as it is ignored by global gitignore",
77 path = path.as_ref().display()
78 );
79 return true;
80 }
81 }
82
83 self.ignore
84 .matched_path_or_any_parents(relative_path, false)
85 .is_ignore()
86 }
87
88 fn iter(&self) -> impl Iterator<Item = Result<DirEntry, walkdir::Error>> {
89 WalkDir::new(&self.context_path).into_iter()
90 }
91
92 pub async fn build_tar(&self) -> Result<ContextArchive, ContextError> {
93 let buffer = Vec::new();
94
95 let mut tar = Builder::new(buffer);
96
97 for entry in self.iter() {
98 let Ok(entry) = entry else {
99 tracing::warn!(?entry, "Failed to read entry");
100 continue;
101 };
102 let path = entry.path();
103
104 let Ok(relative_path) = path.strip_prefix(&self.context_path) else {
105 tracing::warn!(?path, "Failed to strip prefix on path");
106 continue;
107 };
108
109 if path.is_dir() && !path.is_symlink() {
110 tracing::debug!(path = ?path, relative_path = ?relative_path, "Adding directory to tar");
111 if let Err(err) = tar.append_path(relative_path).await {
112 tracing::warn!(?err, "Failed to append path to tar");
113 }
114 continue;
115 }
116
117 if self.is_ignored(path) {
118 tracing::debug!(path = ?path, "Ignored file");
119 continue;
120 }
121
122 if path.is_symlink() {
123 tracing::debug!(path = ?path, "Adding symlink to tar");
124 let Ok(link_target) = tokio::fs::read_link(path).await else {
125 continue;
126 }; let Ok(metadata) = entry.metadata() else {
128 continue;
129 };
130 tracing::debug!(link_target = ?link_target, "Symlink target");
131 let mut header = Header::new_gnu();
132
133 header.set_entry_type(EntryType::Symlink);
135 header.set_link_name(link_target)?;
137
138 header.set_uid(metadata.uid() as u64);
140 header.set_gid(metadata.gid() as u64);
141 header.set_mode(metadata.mode());
144 header.set_mtime(metadata.mtime() as u64);
146 header.set_size(0);
148
149 tar.append_data(&mut header, path, tokio::io::empty())
150 .await?;
151 continue;
152 }
153
154 tracing::debug!(path = ?path, "Adding file to tar");
155 let mut file = tokio::fs::File::open(path).await?;
156 let mut buffer_content = Vec::new();
157 file.read_to_end(&mut buffer_content).await?;
158
159 let mut header = Header::new_gnu();
160 header.set_size(buffer_content.len() as u64);
161 header.set_mode(0o644);
162 header.set_cksum();
163
164 let relative_path = path.strip_prefix(&self.context_path)?;
165 tar.append_data(&mut header, relative_path, &*buffer_content)
166 .await?;
167 }
168
169 let result = tar.into_inner().await?;
170
171 Ok(result)
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use std::fs;
179 use std::io::Write;
180 use tempfile::tempdir;
181
182 #[test_log::test(tokio::test)]
183 async fn test_is_ignored() {
184 let dir = tempdir().unwrap();
185 let context_path = dir.path().to_path_buf();
186
187 let mut gitignore_file = fs::File::create(context_path.join(".gitignore")).unwrap();
189 writeln!(gitignore_file, "*.log").unwrap();
190
191 let mut dockerignore_file = fs::File::create(context_path.join(".dockerignore")).unwrap();
193 writeln!(dockerignore_file, "*.tmp").unwrap();
194
195 dbg!(&std::fs::read_to_string(context_path.join(".gitignore")).unwrap());
196
197 let context_builder = ContextBuilder::from_path(&context_path).unwrap();
198
199 let log_file = context_path.join("test.log");
201 let tmp_file = context_path.join("test.tmp");
202 let txt_file = context_path.join("test.txt");
203
204 fs::File::create(&log_file).unwrap();
205 fs::File::create(&tmp_file).unwrap();
206 fs::File::create(&txt_file).unwrap();
207
208 assert!(context_builder.is_ignored(&log_file));
209 assert!(context_builder.is_ignored(&tmp_file));
210 assert!(!context_builder.is_ignored(&txt_file));
211 }
212
213 #[test_log::test(tokio::test)]
214 async fn test_adds_git_even_if_in_ignore() {
215 let dir = tempdir().unwrap();
216 let context_path = dir.path().to_path_buf();
217
218 let mut gitignore_file = fs::File::create(context_path.join(".gitignore")).unwrap();
220 writeln!(gitignore_file, ".git").unwrap();
221
222 let context_builder = ContextBuilder::from_path(&context_path).unwrap();
223
224 assert!(!context_builder.is_ignored(".git"));
225 }
226
227 #[test_log::test(tokio::test)]
228 async fn test_works_without_gitignore() {
229 let dir = tempdir().unwrap();
230 let context_path = dir.path().to_path_buf();
231
232 let context_builder = ContextBuilder::from_path(&context_path).unwrap();
235
236 assert!(!context_builder.is_ignored(".git"));
237 assert!(!context_builder.is_ignored("Dockerfile"));
238
239 fs::File::create(context_path.join("Dockerfile")).unwrap();
240
241 assert!(!context_builder.is_ignored("Dockerfile"));
242 }
243}