aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/package/archiver/archivable_files_finder.rs
blob: 72ffabf32edb8e4d3d30327893ac53f0d6af837e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! ref: composer/src/Composer/Package/Archiver/ArchivableFilesFinder.php

use crate::package::archiver::composer_exclude_filter::ComposerExcludeFilter;
use crate::package::archiver::git_exclude_filter::GitExcludeFilter;
use crate::util::filesystem::Filesystem;
use shirabe_external_packages::composer::pcre::preg::Preg;
use shirabe_external_packages::symfony::component::finder::finder::Finder;
use shirabe_external_packages::symfony::component::finder::spl_file_info::SplFileInfo;
use shirabe_php_shim::{RuntimeException, preg_quote, realpath};

pub struct ArchivableFilesFinder {
    pub(crate) finder: Finder,
    inner_iter: Box<dyn Iterator<Item = SplFileInfo>>,
}

impl std::fmt::Debug for ArchivableFilesFinder {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ArchivableFilesFinder")
            .field("finder", &self.finder)
            .finish()
    }
}

impl ArchivableFilesFinder {
    pub fn new(sources: &str, excludes: Vec<String>, ignore_filters: bool) -> anyhow::Result<Self> {
        let fs = Filesystem::new(None);

        let sources_real_path = realpath(sources);
        if sources_real_path.is_none() {
            return Err(RuntimeException {
                message: format!("Could not realpath() the source directory \"{}\"", sources),
                code: 0,
            }
            .into());
        }
        let sources = fs.normalize_path(&sources_real_path.unwrap());

        let filters: Vec<Box<dyn ArchivableFilesFilter>> = if ignore_filters {
            vec![]
        } else {
            vec![
                Box::new(GitExcludeFilter::new(sources.clone())),
                Box::new(ComposerExcludeFilter::new(sources.clone(), excludes)),
            ]
        };

        let mut finder = Finder::new();

        let sources_clone = sources.clone();
        let filter = move |file: &SplFileInfo| -> bool {
            let realpath = file.get_real_path();
            if realpath.is_none() {
                return false;
            }
            let realpath = realpath.unwrap();
            if file.is_link() && !realpath.starts_with(sources_clone.as_str()) {
                return false;
            }

            let relative_path = Preg::replace(
                &format!("^{}", preg_quote(&sources_clone, Some('#'))),
                "",
                &fs.normalize_path(&realpath),
            )
            .unwrap_or_default();

            let mut exclude = false;
            for f in &filters {
                exclude = f.filter(&relative_path, exclude);
            }

            !exclude
        };

        finder
            .r#in(&sources)
            // TODO(phase-b): symfony Finder filter takes Box<dyn Fn(&SplFileInfo) -> bool>; signature not yet wired
            .ignore_vcs(true)
            .ignore_dot_files(false)
            .sort_by_name();
        let _ = filter;

        let inner_iter: Box<dyn Iterator<Item = SplFileInfo>> = Box::new(finder.get_iterator());

        Ok(Self { finder, inner_iter })
    }

    pub fn accept(&self, current: &SplFileInfo) -> bool {
        if !current.is_dir() {
            return true;
        }

        let path = current.get_pathname();
        match std::fs::read_dir(&path) {
            Ok(mut iter) => iter.next().is_none(),
            Err(_) => false,
        }
    }
}

trait ArchivableFilesFilter {
    fn filter(&self, relative_path: &str, exclude: bool) -> bool;
}

impl ArchivableFilesFilter for GitExcludeFilter {
    fn filter(&self, relative_path: &str, exclude: bool) -> bool {
        self.filter(relative_path, exclude)
    }
}

impl ArchivableFilesFilter for ComposerExcludeFilter {
    fn filter(&self, relative_path: &str, exclude: bool) -> bool {
        self.filter(relative_path, exclude)
    }
}

impl Iterator for ArchivableFilesFinder {
    type Item = SplFileInfo;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            let item = self.inner_iter.next()?;
            if self.accept(&item) {
                return Some(item);
            }
        }
    }
}