1 use std::collections::BTreeSet;
2 use std::fs;
3 use std::io::Read;
4 use std::path::PathBuf;
5 
6 use console::style;
7 use failure::Error;
8 use ignore::overrides::OverrideBuilder;
9 use ignore::types::TypesBuilder;
10 use ignore::WalkBuilder;
11 use log::info;
12 
13 use crate::utils::progress::{ProgressBar, ProgressStyle};
14 
15 pub struct ReleaseFileSearch {
16     path: PathBuf,
17     extensions: BTreeSet<String>,
18     ignores: BTreeSet<String>,
19     ignore_file: Option<String>,
20 }
21 
22 #[derive(Eq, PartialEq, Hash)]
23 pub struct ReleaseFileMatch {
24     pub base_path: PathBuf,
25     pub path: PathBuf,
26     pub contents: Vec<u8>,
27 }
28 
29 impl ReleaseFileSearch {
new(path: PathBuf) -> Self30     pub fn new(path: PathBuf) -> Self {
31         ReleaseFileSearch {
32             path,
33             extensions: BTreeSet::new(),
34             ignore_file: None,
35             ignores: BTreeSet::new(),
36         }
37     }
38 
extension<E>(&mut self, extension: E) -> &mut Self where E: Into<String>,39     pub fn extension<E>(&mut self, extension: E) -> &mut Self
40     where
41         E: Into<String>,
42     {
43         self.extensions.insert(extension.into());
44         self
45     }
46 
extensions<E>(&mut self, extensions: E) -> &mut Self where E: IntoIterator, E::Item: Into<String>,47     pub fn extensions<E>(&mut self, extensions: E) -> &mut Self
48     where
49         E: IntoIterator,
50         E::Item: Into<String>,
51     {
52         for extension in extensions {
53             self.extensions.insert(extension.into());
54         }
55         self
56     }
57 
ignore<I>(&mut self, ignore: I) -> &mut Self where I: Into<String>,58     pub fn ignore<I>(&mut self, ignore: I) -> &mut Self
59     where
60         I: Into<String>,
61     {
62         self.ignores.insert(ignore.into());
63         self
64     }
65 
ignores<I>(&mut self, ignores: I) -> &mut Self where I: IntoIterator, I::Item: Into<String>,66     pub fn ignores<I>(&mut self, ignores: I) -> &mut Self
67     where
68         I: IntoIterator,
69         I::Item: Into<String>,
70     {
71         for ignore in ignores {
72             self.ignores.insert(ignore.into());
73         }
74         self
75     }
76 
ignore_file<P>(&mut self, path: P) -> &mut Self where P: Into<String>,77     pub fn ignore_file<P>(&mut self, path: P) -> &mut Self
78     where
79         P: Into<String>,
80     {
81         let path = path.into();
82         if !path.is_empty() {
83             self.ignore_file = Some(path);
84         }
85         self
86     }
87 
collect_file(path: PathBuf) -> Result<ReleaseFileMatch, Error>88     pub fn collect_file(path: PathBuf) -> Result<ReleaseFileMatch, Error> {
89         let mut f = fs::File::open(path.clone())?;
90         let mut contents = Vec::new();
91         f.read_to_end(&mut contents)?;
92         Ok(ReleaseFileMatch {
93             base_path: path.clone(),
94             path,
95             contents,
96         })
97     }
98 
collect_files(&self) -> Result<Vec<ReleaseFileMatch>, Error>99     pub fn collect_files(&self) -> Result<Vec<ReleaseFileMatch>, Error> {
100         let progress_style = ProgressStyle::default_spinner().template(
101             "{spinner} Searching for release files...\
102         \n  found {prefix:.yellow} {msg:.dim}",
103         );
104 
105         let progress = ProgressBar::new_spinner();
106         progress.enable_steady_tick(100);
107         progress.set_style(progress_style);
108 
109         let mut collected = Vec::new();
110 
111         let mut builder = WalkBuilder::new(&self.path);
112         builder
113             .follow_links(true)
114             .git_exclude(false)
115             .git_ignore(false)
116             .ignore(false);
117 
118         if !&self.extensions.is_empty() {
119             let mut types_builder = TypesBuilder::new();
120             for ext in &self.extensions {
121                 let ext_name = ext.replace('.', "__");
122                 types_builder.add(&ext_name, &format!("*.{}", ext))?;
123             }
124             builder.types(types_builder.select("all").build()?);
125         }
126 
127         if let Some(ignore_file) = &self.ignore_file {
128             // This could yield an optional partial error
129             // We ignore this error to match behavior of git
130             builder.add_ignore(ignore_file);
131         }
132 
133         if !&self.ignores.is_empty() {
134             let mut override_builder = OverrideBuilder::new(&self.path);
135             for ignore in &self.ignores {
136                 override_builder.add(ignore)?;
137             }
138             builder.overrides(override_builder.build()?);
139         }
140 
141         for result in builder.build() {
142             let file = result?;
143             if file.file_type().map_or(false, |t| t.is_dir()) {
144                 continue;
145             }
146             progress.set_message(&format!("{}", file.path().display()));
147 
148             info!(
149                 "found: {} ({} bytes)",
150                 file.path().display(),
151                 file.metadata().unwrap().len()
152             );
153 
154             let mut f = fs::File::open(file.path())?;
155             let mut contents = Vec::new();
156             f.read_to_end(&mut contents)?;
157 
158             let file_match = ReleaseFileMatch {
159                 base_path: self.path.clone(),
160                 path: file.path().to_path_buf(),
161                 contents,
162             };
163             collected.push(file_match);
164 
165             progress.set_prefix(&collected.len().to_string());
166         }
167 
168         progress.finish_and_clear();
169         println!(
170             "{} Found {} release {}",
171             style(">").dim(),
172             style(collected.len()).yellow(),
173             match collected.len() {
174                 1 => "file",
175                 _ => "files",
176             }
177         );
178 
179         Ok(collected)
180     }
181 }
182