1use crate::errors::*;
2use log::{debug, trace};
3use std::convert::Into;
4use std::fs::{self, File};
5use std::io::Write;
6use std::path::{Component, Path, PathBuf};
7
8/// Naively replaces any path separator with a forward-slash '/'
9pub 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
17pub 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.
40pub 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.
61pub 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
75pub 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
91pub 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.
184fn 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
239pub 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)]
247mod 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