1use crate::{
2 common::{util, AbsolutePath, Item},
3 config::Config,
4 verbose_println,
5};
6use derive_more::From;
7use failure::Fail;
8use std::{
9 collections::HashSet,
10 ffi::OsString,
11 io, iter,
12 path::{Path, PathBuf},
13};
14use walkdir::WalkDir;
15
16fn make_hidden(path: &Path) -> PathBuf {
18 let path_str = OsString::from(path.as_os_str());
19 let hidden_path = {
20 let mut hidden_path = OsString::from(".");
21 hidden_path.push(path_str);
22
23 hidden_path
24 };
25
26 PathBuf::from(hidden_path)
27}
28
29fn link_dir_contents(
32 dir: &AbsolutePath,
33 excludes: &HashSet<&AbsolutePath>,
34) -> Result<Vec<Item>, Error> {
35 let mut res = vec![];
36 for entry in WalkDir::new(dir)
37 .into_iter()
38 .filter_entry(|entry| !util::is_hidden(entry.file_name()))
39 {
40 let entry = entry?;
41
42 let path = AbsolutePath::from(entry.path());
43
44 if excludes.contains(&path) {
45 verbose_println!("Excluded {}", path);
46 }
47
48 if !util::is_hidden(entry.file_name())
49 && entry.file_type().is_file()
50 && !excludes.contains(&path)
51 {
52 let dest = {
53 let dest_tail = match dir.parent() {
54 None => path.as_path(),
55 Some(parent) => path
56 .strip_prefix(parent)
57 .expect("dir must be a prefix of entry"),
58 };
59
60 AbsolutePath::from(util::home_dir().join(make_hidden(dest_tail)))
61 };
62 let source = path;
63
64 res.push(Item::new(source, dest));
65 }
66 }
67
68 Ok(res)
69}
70
71fn find_items(
74 root: AbsolutePath,
75 is_prefixed: &impl Fn(&Path) -> bool,
76 active_prefixed_dirs: &HashSet<&Path>,
77 excludes: &HashSet<&AbsolutePath>,
78 res: &mut Vec<Item>,
79) -> Result<(), Error> {
80 for entry in root.read_dir()? {
81 let entry = entry?;
82 let path = AbsolutePath::from(entry.path());
83
84 let entry_name = entry.file_name();
85 let entry_name = Path::new(&entry_name);
86
87 let excluded = excludes.contains(&path);
88 if util::is_hidden(entry_name.as_os_str()) || excluded {
89 if excluded {
90 verbose_println!("Excluded {}", path);
91 }
92 continue;
93 }
94
95 if is_prefixed(&entry_name) {
96 if active_prefixed_dirs.contains(entry_name) {
97 find_items(path, is_prefixed, active_prefixed_dirs, excludes, res)?;
98 }
99 } else {
100 let contents = link_dir_contents(&AbsolutePath::from(entry.path()), excludes)?;
101 res.extend(contents);
102 }
103 }
104
105 Ok(())
106}
107
108pub fn get(config: &Config) -> Result<Vec<Item>, Error> {
109 let hostname_prefix = "host-";
110 let tag_prefix = "tag-";
111 let platform_prefix = "platform-";
112 let prefixes = [hostname_prefix, tag_prefix, platform_prefix];
113
114 let is_prefixed = |filename: &Path| -> bool {
117 for prefix in &prefixes {
118 match filename.to_str() {
119 Some(s) if s.starts_with(prefix) => return true,
120 _ => continue,
121 }
122 }
123
124 false
125 };
126
127 let hostname_dir = PathBuf::from([hostname_prefix, config.hostname()].concat());
128
129 let platform_dirs: Vec<PathBuf> = config
130 .platform()
131 .strs()
132 .iter()
133 .map(|platform| PathBuf::from([platform_prefix, platform].concat()))
134 .collect();
135
136 let tag_dirs: Vec<PathBuf> = config
137 .tags()
138 .iter()
139 .map(|tag| PathBuf::from([tag_prefix, tag].concat()))
140 .collect();
141
142 let active_prefixed_dirs: HashSet<&Path> = iter::once(&hostname_dir)
143 .chain(tag_dirs.iter())
144 .chain(platform_dirs.iter())
145 .map(|p| p.as_path())
146 .collect();
147
148 let excludes = config.excludes().iter().collect();
149
150 let mut res = vec![];
151
152 find_items(
153 config.dotfiles_path().clone(),
154 &is_prefixed,
155 &active_prefixed_dirs,
156 &excludes,
157 &mut res,
158 )?;
159
160 let mut seen = HashSet::new();
162 for item in &res {
163 let dest = item.dest();
164 if seen.contains(dest) {
165 return Err(DuplicateFiles { dest: dest.clone() });
166 } else {
167 seen.insert(dest);
168 }
169 }
170
171 Ok(res)
172}
173
174#[derive(Debug, From, Fail)]
175pub enum Error {
176 #[fail(display = "multiple source files for destination {}", dest)]
179 DuplicateFiles { dest: AbsolutePath },
180
181 #[fail(display = "error reading from dotfiles directory ({})", _0)]
182 IoError(#[fail(cause)] io::Error),
183
184 #[fail(display = "error reading from dotfiles directory ({})", _0)]
185 WalkdirError(#[fail(cause)] walkdir::Error),
186}
187use Error::*;