You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

166 lines
5.2KB

  1. use std::fs::{copy, create_dir_all, read_dir, File};
  2. use std::io::prelude::*;
  3. use std::path::{Path, PathBuf};
  4. use std::time::SystemTime;
  5. use walkdir::WalkDir;
  6. use errors::{Result, ResultExt};
  7. pub fn is_path_in_directory(parent: &Path, path: &Path) -> Result<bool> {
  8. let canonical_path = path
  9. .canonicalize()
  10. .map_err(|e| format!("Failed to canonicalize {}: {}", path.display(), e))?;
  11. let canonical_parent = parent
  12. .canonicalize()
  13. .map_err(|e| format!("Failed to canonicalize {}: {}", parent.display(), e))?;
  14. Ok(canonical_path.starts_with(canonical_parent))
  15. }
  16. /// Create a file with the content given
  17. pub fn create_file(path: &Path, content: &str) -> Result<()> {
  18. let mut file = File::create(&path).chain_err(|| format!("Failed to create {:?}", path))?;
  19. file.write_all(content.as_bytes())?;
  20. Ok(())
  21. }
  22. /// Create a directory at the given path if it doesn't exist already
  23. pub fn ensure_directory_exists(path: &Path) -> Result<()> {
  24. if !path.exists() {
  25. create_directory(path)?;
  26. }
  27. Ok(())
  28. }
  29. /// Very similar to `create_dir` from the std except it checks if the folder
  30. /// exists before creating it
  31. pub fn create_directory(path: &Path) -> Result<()> {
  32. if !path.exists() {
  33. create_dir_all(path)
  34. .chain_err(|| format!("Was not able to create folder {}", path.display()))?;
  35. }
  36. Ok(())
  37. }
  38. /// Return the content of a file, with error handling added
  39. pub fn read_file(path: &Path) -> Result<String> {
  40. let mut content = String::new();
  41. File::open(path)
  42. .chain_err(|| format!("Failed to open '{:?}'", path.display()))?
  43. .read_to_string(&mut content)?;
  44. // Remove utf-8 BOM if any.
  45. if content.starts_with("\u{feff}") {
  46. content.drain(..3);
  47. }
  48. Ok(content)
  49. }
  50. /// Looks into the current folder for the path and see if there's anything that is not a .md
  51. /// file. Those will be copied next to the rendered .html file
  52. pub fn find_related_assets(path: &Path) -> Vec<PathBuf> {
  53. let mut assets = vec![];
  54. for entry in read_dir(path).unwrap().filter_map(|e| e.ok()) {
  55. let entry_path = entry.path();
  56. if entry_path.is_file() {
  57. match entry_path.extension() {
  58. Some(e) => match e.to_str() {
  59. Some("md") => continue,
  60. _ => assets.push(entry_path.to_path_buf()),
  61. },
  62. None => continue,
  63. }
  64. }
  65. }
  66. assets
  67. }
  68. /// Copy a file but takes into account where to start the copy as
  69. /// there might be folders we need to create on the way
  70. pub fn copy_file(src: &Path, dest: &PathBuf, base_path: &PathBuf) -> Result<()> {
  71. let relative_path = src.strip_prefix(base_path).unwrap();
  72. let target_path = dest.join(relative_path);
  73. if let Some(parent_directory) = target_path.parent() {
  74. create_dir_all(parent_directory)?;
  75. }
  76. copy(src, target_path)?;
  77. Ok(())
  78. }
  79. pub fn copy_directory(src: &PathBuf, dest: &PathBuf) -> Result<()> {
  80. for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) {
  81. let relative_path = entry.path().strip_prefix(src).unwrap();
  82. let target_path = dest.join(relative_path);
  83. if entry.path().is_dir() {
  84. if !target_path.exists() {
  85. create_directory(&target_path)?;
  86. }
  87. } else {
  88. copy_file(entry.path(), dest, src)?;
  89. }
  90. }
  91. Ok(())
  92. }
  93. pub fn get_file_time(path: &Path) -> Option<SystemTime> {
  94. path.metadata().ok().and_then(|meta| {
  95. Some(match (meta.created().ok(), meta.modified().ok()) {
  96. (Some(tc), Some(tm)) => tc.max(tm),
  97. (Some(tc), None) => tc,
  98. (None, Some(tm)) => tm,
  99. (None, None) => return None,
  100. })
  101. })
  102. }
  103. /// Compares source and target files' timestamps and returns true if the source file
  104. /// has been created _or_ updated after the target file has
  105. pub fn file_stale<PS, PT>(p_source: PS, p_target: PT) -> bool
  106. where
  107. PS: AsRef<Path>,
  108. PT: AsRef<Path>,
  109. {
  110. let p_source = p_source.as_ref();
  111. let p_target = p_target.as_ref();
  112. if !p_target.exists() {
  113. return true;
  114. }
  115. let time_source = get_file_time(p_source);
  116. let time_target = get_file_time(p_target);
  117. time_source.and_then(|ts| time_target.map(|tt| ts > tt)).unwrap_or(true)
  118. }
  119. #[cfg(test)]
  120. mod tests {
  121. use std::fs::File;
  122. use tempfile::tempdir;
  123. use super::find_related_assets;
  124. #[test]
  125. fn can_find_related_assets() {
  126. let tmp_dir = tempdir().expect("create temp dir");
  127. File::create(tmp_dir.path().join("index.md")).unwrap();
  128. File::create(tmp_dir.path().join("example.js")).unwrap();
  129. File::create(tmp_dir.path().join("graph.jpg")).unwrap();
  130. File::create(tmp_dir.path().join("fail.png")).unwrap();
  131. let assets = find_related_assets(tmp_dir.path());
  132. assert_eq!(assets.len(), 3);
  133. assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 3);
  134. assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "example.js").count(), 1);
  135. assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "graph.jpg").count(), 1);
  136. assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "fail.png").count(), 1);
  137. }
  138. }