zip_extensions/deflate/
zip_ignore_entry_handler.rs

1use crate::default_entry_handler::DefaultEntryHandler;
2use crate::entry_handler::EntryHandler;
3use ignore::gitignore::{Gitignore, GitignoreBuilder};
4use std::collections::HashMap;
5use std::io;
6use std::io::Write;
7use std::path::{Path, PathBuf};
8use std::sync::Mutex;
9use zip::ZipWriter;
10use zip::result::ZipResult;
11use zip::write::{FileOptionExtension, FileOptions};
12
13/// An EntryHandler wrapper that honors `.zipignore` files similar to how `.gitignore` works.
14/// Patterns from `.zipignore` files are merged from the root directory down to deeper levels.
15/// If a path is not ignored, delegation continues to the wrapped `inner` handler.
16pub struct ZipIgnoreEntryHandler<H = DefaultEntryHandler> {
17    per_directory_matcher_cache: Mutex<HashMap<PathBuf, Gitignore>>,
18    ignore_filename: &'static str,
19    inner: H,
20}
21
22pub(crate) const IGNORE_FILENAME: &str = ".zipignore";
23
24impl ZipIgnoreEntryHandler<DefaultEntryHandler> {
25    pub fn new() -> Self {
26        Self {
27            per_directory_matcher_cache: Mutex::new(HashMap::new()),
28            ignore_filename: IGNORE_FILENAME,
29            inner: DefaultEntryHandler,
30        }
31    }
32}
33
34impl<H> ZipIgnoreEntryHandler<H> {
35    pub fn with_inner(inner: H) -> Self {
36        Self {
37            per_directory_matcher_cache: Mutex::new(HashMap::new()),
38            ignore_filename: IGNORE_FILENAME,
39            inner,
40        }
41    }
42
43    fn parent_dir(path: &Path) -> &Path {
44        path.parent().unwrap_or(path)
45    }
46
47    fn build_matcher(&self, root: &Path, dir: &Path) -> io::Result<Gitignore> {
48        // Build by adding all .zipignore files encountered from root to current dir
49        let mut ignore_builder = GitignoreBuilder::new(root);
50
51        // Collect directories from root to dir
52        let mut stack: Vec<PathBuf> = vec![];
53        let mut current_dir = dir;
54        loop {
55            stack.push(current_dir.to_path_buf());
56            if current_dir == root {
57                break;
58            }
59            match current_dir.parent() {
60                Some(p) => current_dir = p,
61                None => break,
62            }
63        }
64        stack.reverse();
65
66        for d in stack {
67            let ignore_file = d.join(self.ignore_filename);
68            if ignore_file.exists() {
69                let _ = ignore_builder.add(ignore_file);
70            }
71        }
72        let built = ignore_builder
73            .build()
74            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
75        Ok(built)
76    }
77
78    fn matcher_for_dir(&self, root: &Path, dir: &Path) -> io::Result<Gitignore> {
79        let mut cache = self.per_directory_matcher_cache.lock().unwrap();
80        if let Some(existing_matcher) = cache.get(dir) {
81            return Ok(existing_matcher.clone());
82        }
83        let new_matcher = self.build_matcher(root, dir)?;
84        cache.insert(dir.to_path_buf(), new_matcher.clone());
85        Ok(new_matcher)
86    }
87
88    fn is_ignored(&self, root: &Path, path: &Path, is_dir: bool) -> bool {
89        let dir = if path.is_dir() {
90            path
91        } else {
92            Self::parent_dir(path)
93        };
94        match self.matcher_for_dir(root, dir) {
95            Ok(matcher) => matcher
96                .matched_path_or_any_parents(path, is_dir)
97                .is_ignore(),
98            Err(_) => false,
99        }
100    }
101}
102
103impl<T: FileOptionExtension, H> EntryHandler<T> for ZipIgnoreEntryHandler<H>
104where
105    H: EntryHandler<T>,
106{
107    fn handle_entry<W: Write + io::Seek>(
108        &self,
109        writer: &mut ZipWriter<W>,
110        root: &PathBuf,
111        entry_path: &PathBuf,
112        file_options: FileOptions<T>,
113        buffer: &mut Vec<u8>,
114    ) -> ZipResult<()> {
115        let metadata = std::fs::metadata(entry_path)?;
116        let is_dir = metadata.is_dir();
117        if self.is_ignored(root.as_path(), entry_path.as_path(), is_dir) {
118            return Ok(());
119        }
120        self.inner
121            .handle_entry(writer, root, entry_path, file_options, buffer)
122    }
123}