swiftide_docker_executor/
context_builder.rsuse std::{
os::unix::fs::MetadataExt as _,
path::{Path, PathBuf},
};
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use tokio::io::AsyncReadExt as _;
use tokio_tar::{Builder, EntryType, Header};
use walkdir::{DirEntry, WalkDir};
use crate::ContextError;
type ContextArchive = Vec<u8>;
#[derive(Debug)]
pub struct ContextBuilder {
context_path: PathBuf,
ignore: Gitignore,
global: Option<Gitignore>,
}
impl ContextBuilder {
pub fn from_path(context_path: impl Into<PathBuf>) -> Result<Self, ContextError> {
let path = context_path.into();
let mut gitignore = GitignoreBuilder::new(&path);
if let Some(err) = gitignore.add(path.join(".gitignore")) {
tracing::warn!(?err, "Error adding .gitignore");
}
if let Some(err) = gitignore.add(path.join(".dockerignore")) {
tracing::warn!(?err, "Error adding .dockerignore");
}
let gitignore = gitignore.build()?;
let (global_gitignore, maybe_error) = Gitignore::global();
let maybe_global = if let Some(err) = maybe_error {
tracing::warn!(?err, "Error adding global gitignore");
None
} else {
Some(global_gitignore)
};
Ok(Self {
context_path: path,
ignore: gitignore,
global: maybe_global,
})
}
fn is_ignored(&self, path: impl AsRef<Path>) -> bool {
let Ok(relative_path) = path.as_ref().strip_prefix(&self.context_path) else {
tracing::debug!(
"not ignoring {path} as it seems to be not prefixed by {prefix}",
path = path.as_ref().display(),
prefix = self.context_path.to_string_lossy()
);
return false;
};
if relative_path.starts_with(".git") {
tracing::debug!(
"not ignoring {path} as it seems to be a git file",
path = path.as_ref().display()
);
return false;
}
if let Some(global) = &self.global {
if global
.matched_path_or_any_parents(relative_path, false)
.is_ignore()
{
tracing::debug!(
"ignoring {path} as it is ignored by global gitignore",
path = path.as_ref().display()
);
return true;
}
}
self.ignore
.matched_path_or_any_parents(relative_path, false)
.is_ignore()
}
fn iter(&self) -> impl Iterator<Item = Result<DirEntry, walkdir::Error>> {
WalkDir::new(&self.context_path).into_iter()
}
pub async fn build_tar(&self) -> Result<ContextArchive, ContextError> {
let buffer = Vec::new();
let mut tar = Builder::new(buffer);
for entry in self.iter() {
let Ok(entry) = entry else { continue };
let path = entry.path();
let Ok(relative_path) = path.strip_prefix(&self.context_path) else {
continue;
};
if path.is_dir() {
let _ = tar.append_path(relative_path).await;
continue;
}
if self.is_ignored(path) {
tracing::debug!(path = ?path, "Ignored file");
continue;
}
if path.is_symlink() {
let Ok(link_target) = tokio::fs::read_link(path).await else {
continue;
}; let Ok(metadata) = entry.metadata() else {
continue;
};
let mut header = Header::new_gnu();
header.set_entry_type(EntryType::Symlink);
header.set_link_name(link_target)?;
header.set_uid(metadata.uid() as u64);
header.set_gid(metadata.gid() as u64);
header.set_mode(metadata.mode());
header.set_mtime(metadata.mtime() as u64);
header.set_size(0);
tar.append_data(&mut header, path, tokio::io::empty())
.await?;
continue;
}
tracing::debug!(path = ?path, "Adding file to tar");
let mut file = tokio::fs::File::open(path).await?;
let mut buffer_content = Vec::new();
file.read_to_end(&mut buffer_content).await?;
let mut header = Header::new_gnu();
header.set_size(buffer_content.len() as u64);
header.set_mode(0o644);
header.set_cksum();
let relative_path = path.strip_prefix(&self.context_path)?;
tar.append_data(&mut header, relative_path, &*buffer_content)
.await?;
}
let result = tar.into_inner().await?;
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use tempfile::tempdir;
#[test_log::test(tokio::test)]
async fn test_is_ignored() {
let dir = tempdir().unwrap();
let context_path = dir.path().to_path_buf();
let mut gitignore_file = fs::File::create(context_path.join(".gitignore")).unwrap();
writeln!(gitignore_file, "*.log").unwrap();
let mut dockerignore_file = fs::File::create(context_path.join(".dockerignore")).unwrap();
writeln!(dockerignore_file, "*.tmp").unwrap();
dbg!(&std::fs::read_to_string(context_path.join(".gitignore")).unwrap());
let context_builder = ContextBuilder::from_path(&context_path).unwrap();
let log_file = context_path.join("test.log");
let tmp_file = context_path.join("test.tmp");
let txt_file = context_path.join("test.txt");
fs::File::create(&log_file).unwrap();
fs::File::create(&tmp_file).unwrap();
fs::File::create(&txt_file).unwrap();
assert!(context_builder.is_ignored(&log_file));
assert!(context_builder.is_ignored(&tmp_file));
assert!(!context_builder.is_ignored(&txt_file));
}
#[test_log::test(tokio::test)]
async fn test_adds_git_even_if_in_ignore() {
let dir = tempdir().unwrap();
let context_path = dir.path().to_path_buf();
let mut gitignore_file = fs::File::create(context_path.join(".gitignore")).unwrap();
writeln!(gitignore_file, ".git").unwrap();
let context_builder = ContextBuilder::from_path(&context_path).unwrap();
assert!(!context_builder.is_ignored(".git"));
}
#[test_log::test(tokio::test)]
async fn test_works_without_gitignore() {
let dir = tempdir().unwrap();
let context_path = dir.path().to_path_buf();
let context_builder = ContextBuilder::from_path(&context_path).unwrap();
assert!(!context_builder.is_ignored(".git"));
assert!(!context_builder.is_ignored("Dockerfile"));
fs::File::create(context_path.join("Dockerfile")).unwrap();
assert!(!context_builder.is_ignored("Dockerfile"));
}
}