1 | use crate::errors::*; |
2 | use log::{debug, trace}; |
3 | use std::convert::Into; |
4 | use std::fs::{self, File}; |
5 | use std::io::Write; |
6 | use std::path::{Component, Path, PathBuf}; |
7 | |
8 | /// Naively replaces any path separator with a forward-slash '/' |
9 | pub fn normalize_path(path: &str) -> String { |
10 | use std::path::is_separator; |
11 | pathimpl Iterator .chars() |
12 | .map(|ch: char| if is_separator(ch) { '/' } else { ch }) |
13 | .collect::<String>() |
14 | } |
15 | |
16 | /// Write the given data to a file, creating it first if necessary |
17 | pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8]) -> Result<()> { |
18 | let path: PathBuf = build_dir.join(path:filename); |
19 | |
20 | create_file(&path)?.write_all(content).map_err(op:Into::into) |
21 | } |
22 | |
23 | /// Takes a path and returns a path containing just enough `../` to point to |
24 | /// the root of the given path. |
25 | /// |
26 | /// This is mostly interesting for a relative path to point back to the |
27 | /// directory from where the path starts. |
28 | /// |
29 | /// ```rust |
30 | /// # use std::path::Path; |
31 | /// # use mdbook::utils::fs::path_to_root; |
32 | /// let path = Path::new("some/relative/path" ); |
33 | /// assert_eq!(path_to_root(path), "../../" ); |
34 | /// ``` |
35 | /// |
36 | /// **note:** it's not very fool-proof, if you find a situation where |
37 | /// it doesn't return the correct path. |
38 | /// Consider [submitting a new issue](https://github.com/rust-lang/mdBook/issues) |
39 | /// or a [pull-request](https://github.com/rust-lang/mdBook/pulls) to improve it. |
40 | pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String { |
41 | // Remove filename and add "../" for every directory |
42 | |
43 | path.into() |
44 | .parent() |
45 | .expect("" ) |
46 | .components() |
47 | .fold(String::new(), |mut s, c: Component<'_>| { |
48 | match c { |
49 | Component::Normal(_) => s.push_str("../" ), |
50 | _ => { |
51 | debug!("Other path component... {:?}" , c); |
52 | } |
53 | } |
54 | s |
55 | }) |
56 | } |
57 | |
58 | /// This function creates a file and returns it. But before creating the file |
59 | /// it checks every directory in the path to see if it exists, |
60 | /// and if it does not it will be created. |
61 | pub fn create_file(path: &Path) -> Result<File> { |
62 | debug!("Creating {}" , path.display()); |
63 | |
64 | // Construct path |
65 | if let Some(p: &Path) = path.parent() { |
66 | trace!("Parent directory is: {:?}" , p); |
67 | |
68 | fs::create_dir_all(path:p)?; |
69 | } |
70 | |
71 | File::create(path).map_err(op:Into::into) |
72 | } |
73 | |
74 | /// Removes all the content of a directory but not the directory itself |
75 | pub fn remove_dir_content(dir: &Path) -> Result<()> { |
76 | for item: Result in fs::read_dir(path:dir)? { |
77 | if let Ok(item: DirEntry) = item { |
78 | let item: PathBuf = item.path(); |
79 | if item.is_dir() { |
80 | fs::remove_dir_all(path:item)?; |
81 | } else { |
82 | fs::remove_file(path:item)?; |
83 | } |
84 | } |
85 | } |
86 | Ok(()) |
87 | } |
88 | |
89 | /// Copies all files of a directory to another one except the files |
90 | /// with the extensions given in the `ext_blacklist` array |
91 | pub fn copy_files_except_ext( |
92 | from: &Path, |
93 | to: &Path, |
94 | recursive: bool, |
95 | avoid_dir: Option<&PathBuf>, |
96 | ext_blacklist: &[&str], |
97 | ) -> Result<()> { |
98 | debug!( |
99 | "Copying all files from {} to {} (blacklist: {:?}), avoiding {:?}" , |
100 | from.display(), |
101 | to.display(), |
102 | ext_blacklist, |
103 | avoid_dir |
104 | ); |
105 | |
106 | // Check that from and to are different |
107 | if from == to { |
108 | return Ok(()); |
109 | } |
110 | |
111 | for entry in fs::read_dir(from)? { |
112 | let entry = entry?; |
113 | let metadata = entry |
114 | .path() |
115 | .metadata() |
116 | .with_context(|| format!("Failed to read {:?}" , entry.path()))?; |
117 | |
118 | // If the entry is a dir and the recursive option is enabled, call itself |
119 | if metadata.is_dir() && recursive { |
120 | if entry.path() == to.to_path_buf() { |
121 | continue; |
122 | } |
123 | |
124 | if let Some(avoid) = avoid_dir { |
125 | if entry.path() == *avoid { |
126 | continue; |
127 | } |
128 | } |
129 | |
130 | // check if output dir already exists |
131 | if !to.join(entry.file_name()).exists() { |
132 | fs::create_dir(&to.join(entry.file_name()))?; |
133 | } |
134 | |
135 | copy_files_except_ext( |
136 | &from.join(entry.file_name()), |
137 | &to.join(entry.file_name()), |
138 | true, |
139 | avoid_dir, |
140 | ext_blacklist, |
141 | )?; |
142 | } else if metadata.is_file() { |
143 | // Check if it is in the blacklist |
144 | if let Some(ext) = entry.path().extension() { |
145 | if ext_blacklist.contains(&ext.to_str().unwrap()) { |
146 | continue; |
147 | } |
148 | } |
149 | debug!( |
150 | "creating path for file: {:?}" , |
151 | &to.join( |
152 | entry |
153 | .path() |
154 | .file_name() |
155 | .expect("a file should have a file name..." ) |
156 | ) |
157 | ); |
158 | |
159 | debug!( |
160 | "Copying {:?} to {:?}" , |
161 | entry.path(), |
162 | &to.join( |
163 | entry |
164 | .path() |
165 | .file_name() |
166 | .expect("a file should have a file name..." ) |
167 | ) |
168 | ); |
169 | copy( |
170 | entry.path(), |
171 | &to.join( |
172 | entry |
173 | .path() |
174 | .file_name() |
175 | .expect("a file should have a file name..." ), |
176 | ), |
177 | )?; |
178 | } |
179 | } |
180 | Ok(()) |
181 | } |
182 | |
183 | /// Copies a file. |
184 | fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> { |
185 | let from = from.as_ref(); |
186 | let to = to.as_ref(); |
187 | return copy_inner(from, to) |
188 | .with_context(|| format!("failed to copy ` {}` to ` {}`" , from.display(), to.display())); |
189 | |
190 | // This is a workaround for an issue with the macOS file watcher. |
191 | // Rust's `std::fs::copy` function uses `fclonefileat`, which creates |
192 | // clones on APFS. Unfortunately fs events seem to trigger on both |
193 | // sides of the clone, and there doesn't seem to be a way to differentiate |
194 | // which side it is. |
195 | // https://github.com/notify-rs/notify/issues/465#issuecomment-1657261035 |
196 | // contains more information. |
197 | // |
198 | // This is essentially a copy of the simple copy code path in Rust's |
199 | // standard library. |
200 | #[cfg (target_os = "macos" )] |
201 | fn copy_inner(from: &Path, to: &Path) -> Result<()> { |
202 | use std::fs::OpenOptions; |
203 | use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; |
204 | |
205 | let mut reader = File::open(from)?; |
206 | let metadata = reader.metadata()?; |
207 | if !metadata.is_file() { |
208 | anyhow::bail!( |
209 | "expected a file, ` {}` appears to be {:?}" , |
210 | from.display(), |
211 | metadata.file_type() |
212 | ); |
213 | } |
214 | let perm = metadata.permissions(); |
215 | let mut writer = OpenOptions::new() |
216 | .mode(perm.mode()) |
217 | .write(true) |
218 | .create(true) |
219 | .truncate(true) |
220 | .open(to)?; |
221 | let writer_metadata = writer.metadata()?; |
222 | if writer_metadata.is_file() { |
223 | // Set the correct file permissions, in case the file already existed. |
224 | // Don't set the permissions on already existing non-files like |
225 | // pipes/FIFOs or device nodes. |
226 | writer.set_permissions(perm)?; |
227 | } |
228 | std::io::copy(&mut reader, &mut writer)?; |
229 | Ok(()) |
230 | } |
231 | |
232 | #[cfg (not(target_os = "macos" ))] |
233 | fn copy_inner(from: &Path, to: &Path) -> Result<()> { |
234 | fs::copy(from, to)?; |
235 | Ok(()) |
236 | } |
237 | } |
238 | |
239 | pub fn get_404_output_file(input_404: &Option<String>) -> String { |
240 | input_404 |
241 | .as_ref() |
242 | .unwrap_or(&"404.md" .to_string()) |
243 | .replace(from:".md" , to:".html" ) |
244 | } |
245 | |
246 | #[cfg (test)] |
247 | mod tests { |
248 | use super::copy_files_except_ext; |
249 | use std::{fs, io::Result, path::Path}; |
250 | |
251 | #[cfg (target_os = "windows" )] |
252 | fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> { |
253 | std::os::windows::fs::symlink_file(src, dst) |
254 | } |
255 | |
256 | #[cfg (not(target_os = "windows" ))] |
257 | fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> { |
258 | std::os::unix::fs::symlink(src, dst) |
259 | } |
260 | |
261 | #[test ] |
262 | fn copy_files_except_ext_test() { |
263 | let tmp = match tempfile::TempDir::new() { |
264 | Ok(t) => t, |
265 | Err(e) => panic!("Could not create a temp dir: {}" , e), |
266 | }; |
267 | |
268 | // Create a couple of files |
269 | if let Err(err) = fs::File::create(tmp.path().join("file.txt" )) { |
270 | panic!("Could not create file.txt: {}" , err); |
271 | } |
272 | if let Err(err) = fs::File::create(tmp.path().join("file.md" )) { |
273 | panic!("Could not create file.md: {}" , err); |
274 | } |
275 | if let Err(err) = fs::File::create(tmp.path().join("file.png" )) { |
276 | panic!("Could not create file.png: {}" , err); |
277 | } |
278 | if let Err(err) = fs::create_dir(tmp.path().join("sub_dir" )) { |
279 | panic!("Could not create sub_dir: {}" , err); |
280 | } |
281 | if let Err(err) = fs::File::create(tmp.path().join("sub_dir/file.png" )) { |
282 | panic!("Could not create sub_dir/file.png: {}" , err); |
283 | } |
284 | if let Err(err) = fs::create_dir(tmp.path().join("sub_dir_exists" )) { |
285 | panic!("Could not create sub_dir_exists: {}" , err); |
286 | } |
287 | if let Err(err) = fs::File::create(tmp.path().join("sub_dir_exists/file.txt" )) { |
288 | panic!("Could not create sub_dir_exists/file.txt: {}" , err); |
289 | } |
290 | if let Err(err) = symlink(tmp.path().join("file.png" ), tmp.path().join("symlink.png" )) { |
291 | panic!("Could not symlink file.png: {}" , err); |
292 | } |
293 | |
294 | // Create output dir |
295 | if let Err(err) = fs::create_dir(tmp.path().join("output" )) { |
296 | panic!("Could not create output: {}" , err); |
297 | } |
298 | if let Err(err) = fs::create_dir(tmp.path().join("output/sub_dir_exists" )) { |
299 | panic!("Could not create output/sub_dir_exists: {}" , err); |
300 | } |
301 | |
302 | if let Err(e) = |
303 | copy_files_except_ext(tmp.path(), &tmp.path().join("output" ), true, None, &["md" ]) |
304 | { |
305 | panic!("Error while executing the function: \n{:?}" , e); |
306 | } |
307 | |
308 | // Check if the correct files where created |
309 | if !tmp.path().join("output/file.txt" ).exists() { |
310 | panic!("output/file.txt should exist" ) |
311 | } |
312 | if tmp.path().join("output/file.md" ).exists() { |
313 | panic!("output/file.md should not exist" ) |
314 | } |
315 | if !tmp.path().join("output/file.png" ).exists() { |
316 | panic!("output/file.png should exist" ) |
317 | } |
318 | if !tmp.path().join("output/sub_dir/file.png" ).exists() { |
319 | panic!("output/sub_dir/file.png should exist" ) |
320 | } |
321 | if !tmp.path().join("output/sub_dir_exists/file.txt" ).exists() { |
322 | panic!("output/sub_dir/file.png should exist" ) |
323 | } |
324 | if !tmp.path().join("output/symlink.png" ).exists() { |
325 | panic!("output/symlink.png should exist" ) |
326 | } |
327 | } |
328 | } |
329 | |