use crate::default_entry_handler::DefaultEntryHandler;
use crate::entry_handler::EntryHandler;
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use std::collections::HashMap;
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use zip::ZipWriter;
use zip::result::ZipResult;
use zip::write::{FileOptionExtension, FileOptions};
pub struct ZipIgnoreEntryHandler<H = DefaultEntryHandler> {
per_directory_matcher_cache: Mutex<HashMap<PathBuf, Gitignore>>,
ignore_filename: &'static str,
inner: H,
}
pub(crate) const IGNORE_FILENAME: &str = ".zipignore";
impl ZipIgnoreEntryHandler<DefaultEntryHandler> {
pub fn new() -> Self {
Self {
per_directory_matcher_cache: Mutex::new(HashMap::new()),
ignore_filename: IGNORE_FILENAME,
inner: DefaultEntryHandler,
}
}
}
impl<H> ZipIgnoreEntryHandler<H> {
pub fn with_inner(inner: H) -> Self {
Self {
per_directory_matcher_cache: Mutex::new(HashMap::new()),
ignore_filename: IGNORE_FILENAME,
inner,
}
}
fn parent_dir(path: &Path) -> &Path {
path.parent().unwrap_or(path)
}
fn build_matcher(&self, root: &Path, dir: &Path) -> io::Result<Gitignore> {
let mut ignore_builder = GitignoreBuilder::new(root);
let mut stack: Vec<PathBuf> = vec![];
let mut current_dir = dir;
loop {
stack.push(current_dir.to_path_buf());
if current_dir == root {
break;
}
match current_dir.parent() {
Some(p) => current_dir = p,
None => break,
}
}
stack.reverse();
for d in stack {
let ignore_file = d.join(self.ignore_filename);
if ignore_file.exists() {
let _ = ignore_builder.add(ignore_file);
}
}
let built = ignore_builder
.build()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
Ok(built)
}
fn matcher_for_dir(&self, root: &Path, dir: &Path) -> io::Result<Gitignore> {
let mut cache = self.per_directory_matcher_cache.lock().unwrap();
if let Some(existing_matcher) = cache.get(dir) {
return Ok(existing_matcher.clone());
}
let new_matcher = self.build_matcher(root, dir)?;
cache.insert(dir.to_path_buf(), new_matcher.clone());
Ok(new_matcher)
}
fn is_ignored(&self, root: &Path, path: &Path, is_dir: bool) -> bool {
let dir = if path.is_dir() {
path
} else {
Self::parent_dir(path)
};
match self.matcher_for_dir(root, dir) {
Ok(matcher) => matcher
.matched_path_or_any_parents(path, is_dir)
.is_ignore(),
Err(_) => false,
}
}
}
impl<T: FileOptionExtension, H> EntryHandler<T> for ZipIgnoreEntryHandler<H>
where
H: EntryHandler<T>,
{
fn handle_entry<W: Write + io::Seek>(
&self,
writer: &mut ZipWriter<W>,
root: &PathBuf,
entry_path: &PathBuf,
file_options: FileOptions<T>,
buffer: &mut Vec<u8>,
) -> ZipResult<()> {
let metadata = std::fs::metadata(entry_path)?;
let is_dir = metadata.is_dir();
if self.is_ignored(root.as_path(), entry_path.as_path(), is_dir) {
return Ok(());
}
self.inner
.handle_entry(writer, root, entry_path, file_options, buffer)
}
}