mdbook/utils/
fs.rs

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    path.chars()
12        .map(|ch| 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 = build_dir.join(filename);
19
20    create_file(&path)?.write_all(content).map_err(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    debug!("path_to_root");
42    // Remove filename and add "../" for every directory
43
44    path.into()
45        .parent()
46        .expect("")
47        .components()
48        .fold(String::new(), |mut s, c| {
49            match c {
50                Component::Normal(_) => s.push_str("../"),
51                _ => {
52                    debug!("Other path component... {:?}", c);
53                }
54            }
55            s
56        })
57}
58
59/// This function creates a file and returns it. But before creating the file
60/// it checks every directory in the path to see if it exists,
61/// and if it does not it will be created.
62pub fn create_file(path: &Path) -> Result<File> {
63    debug!("Creating {}", path.display());
64
65    // Construct path
66    if let Some(p) = path.parent() {
67        trace!("Parent directory is: {:?}", p);
68
69        fs::create_dir_all(p)?;
70    }
71
72    File::create(path).map_err(Into::into)
73}
74
75/// Removes all the content of a directory but not the directory itself
76pub fn remove_dir_content(dir: &Path) -> Result<()> {
77    for item in fs::read_dir(dir)? {
78        if let Ok(item) = item {
79            let item = item.path();
80            if item.is_dir() {
81                fs::remove_dir_all(item)?;
82            } else {
83                fs::remove_file(item)?;
84            }
85        }
86    }
87    Ok(())
88}
89
90/// Copies all files of a directory to another one except the files
91/// with the extensions given in the `ext_blacklist` array
92pub fn copy_files_except_ext(
93    from: &Path,
94    to: &Path,
95    recursive: bool,
96    avoid_dir: Option<&PathBuf>,
97    ext_blacklist: &[&str],
98) -> Result<()> {
99    debug!(
100        "Copying all files from {} to {} (blacklist: {:?}), avoiding {:?}",
101        from.display(),
102        to.display(),
103        ext_blacklist,
104        avoid_dir
105    );
106
107    // Check that from and to are different
108    if from == to {
109        return Ok(());
110    }
111
112    for entry in fs::read_dir(from)? {
113        let entry = entry?;
114        let metadata = entry
115            .path()
116            .metadata()
117            .with_context(|| format!("Failed to read {:?}", entry.path()))?;
118
119        // If the entry is a dir and the recursive option is enabled, call itself
120        if metadata.is_dir() && recursive {
121            if entry.path() == to.to_path_buf() {
122                continue;
123            }
124
125            if let Some(avoid) = avoid_dir {
126                if entry.path() == *avoid {
127                    continue;
128                }
129            }
130
131            // check if output dir already exists
132            if !to.join(entry.file_name()).exists() {
133                fs::create_dir(&to.join(entry.file_name()))?;
134            }
135
136            copy_files_except_ext(
137                &from.join(entry.file_name()),
138                &to.join(entry.file_name()),
139                true,
140                avoid_dir,
141                ext_blacklist,
142            )?;
143        } else if metadata.is_file() {
144            // Check if it is in the blacklist
145            if let Some(ext) = entry.path().extension() {
146                if ext_blacklist.contains(&ext.to_str().unwrap()) {
147                    continue;
148                }
149            }
150            debug!(
151                "creating path for file: {:?}",
152                &to.join(
153                    entry
154                        .path()
155                        .file_name()
156                        .expect("a file should have a file name...")
157                )
158            );
159
160            debug!(
161                "Copying {:?} to {:?}",
162                entry.path(),
163                &to.join(
164                    entry
165                        .path()
166                        .file_name()
167                        .expect("a file should have a file name...")
168                )
169            );
170            fs::copy(
171                entry.path(),
172                &to.join(
173                    entry
174                        .path()
175                        .file_name()
176                        .expect("a file should have a file name..."),
177                ),
178            )?;
179        }
180    }
181    Ok(())
182}
183
184pub fn get_404_output_file(input_404: &Option<String>) -> String {
185    input_404
186        .as_ref()
187        .unwrap_or(&"404.md".to_string())
188        .replace(".md", ".html")
189}
190
191#[cfg(test)]
192mod tests {
193    use super::copy_files_except_ext;
194    use std::{fs, io::Result, path::Path};
195
196    #[cfg(target_os = "windows")]
197    fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
198        std::os::windows::fs::symlink_file(src, dst)
199    }
200
201    #[cfg(not(target_os = "windows"))]
202    fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
203        std::os::unix::fs::symlink(src, dst)
204    }
205
206    #[test]
207    fn copy_files_except_ext_test() {
208        let tmp = match tempfile::TempDir::new() {
209            Ok(t) => t,
210            Err(e) => panic!("Could not create a temp dir: {}", e),
211        };
212
213        // Create a couple of files
214        if let Err(err) = fs::File::create(&tmp.path().join("file.txt")) {
215            panic!("Could not create file.txt: {}", err);
216        }
217        if let Err(err) = fs::File::create(&tmp.path().join("file.md")) {
218            panic!("Could not create file.md: {}", err);
219        }
220        if let Err(err) = fs::File::create(&tmp.path().join("file.png")) {
221            panic!("Could not create file.png: {}", err);
222        }
223        if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir")) {
224            panic!("Could not create sub_dir: {}", err);
225        }
226        if let Err(err) = fs::File::create(&tmp.path().join("sub_dir/file.png")) {
227            panic!("Could not create sub_dir/file.png: {}", err);
228        }
229        if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir_exists")) {
230            panic!("Could not create sub_dir_exists: {}", err);
231        }
232        if let Err(err) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) {
233            panic!("Could not create sub_dir_exists/file.txt: {}", err);
234        }
235        if let Err(err) = symlink(
236            &tmp.path().join("file.png"),
237            &tmp.path().join("symlink.png"),
238        ) {
239            panic!("Could not symlink file.png: {}", err);
240        }
241
242        // Create output dir
243        if let Err(err) = fs::create_dir(&tmp.path().join("output")) {
244            panic!("Could not create output: {}", err);
245        }
246        if let Err(err) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) {
247            panic!("Could not create output/sub_dir_exists: {}", err);
248        }
249
250        if let Err(e) =
251            copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"])
252        {
253            panic!("Error while executing the function:\n{:?}", e);
254        }
255
256        // Check if the correct files where created
257        if !(&tmp.path().join("output/file.txt")).exists() {
258            panic!("output/file.txt should exist")
259        }
260        if (&tmp.path().join("output/file.md")).exists() {
261            panic!("output/file.md should not exist")
262        }
263        if !(&tmp.path().join("output/file.png")).exists() {
264            panic!("output/file.png should exist")
265        }
266        if !(&tmp.path().join("output/sub_dir/file.png")).exists() {
267            panic!("output/sub_dir/file.png should exist")
268        }
269        if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() {
270            panic!("output/sub_dir/file.png should exist")
271        }
272        if !(&tmp.path().join("output/symlink.png")).exists() {
273            panic!("output/symlink.png should exist")
274        }
275    }
276}