dir.rs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. // SPDX-FileCopyrightText: 2024 Christina Sørensen
  2. // SPDX-License-Identifier: EUPL-1.2
  3. //
  4. // SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors
  5. // SPDX-FileCopyrightText: 2014 Benjamin Sago
  6. // SPDX-License-Identifier: MIT
  7. use crate::fs::feature::git::GitCache;
  8. use crate::fs::fields::GitStatus;
  9. use std::fs;
  10. use std::fs::DirEntry;
  11. use std::io;
  12. use std::path::{Path, PathBuf};
  13. use std::slice::Iter as SliceIter;
  14. use log::info;
  15. use crate::fs::File;
  16. /// A **Dir** provides a cached list of the file paths in a directory that’s
  17. /// being listed.
  18. ///
  19. /// This object gets passed to the Files themselves, in order for them to
  20. /// check the existence of surrounding files, then highlight themselves
  21. /// accordingly. (See `File#get_source_files`)
  22. pub struct Dir {
  23. /// A vector of the files that have been read from this directory.
  24. contents: Vec<DirEntry>,
  25. /// The path that was read.
  26. pub path: PathBuf,
  27. }
  28. impl Dir {
  29. /// Create a new, empty `Dir` object representing the directory at the given path.
  30. ///
  31. /// This function does not attempt to read the contents of the directory; it merely
  32. /// initializes an instance of `Dir` with an empty `DirEntry` list and the specified path.
  33. /// To populate the `Dir` object with actual directory contents, use the `read` function.
  34. pub fn new(path: PathBuf) -> Self {
  35. Self {
  36. contents: vec![],
  37. path,
  38. }
  39. }
  40. /// Reads the contents of the directory into `DirEntry`.
  41. ///
  42. /// It is recommended to use this method in conjunction with `new` in recursive
  43. /// calls, rather than `read_dir`, to avoid holding multiple open file descriptors
  44. /// simultaneously, which can lead to "too many open files" errors.
  45. pub fn read(&mut self) -> io::Result<&Self> {
  46. info!("Reading directory {:?}", &self.path);
  47. self.contents = fs::read_dir(&self.path)?.collect::<Result<Vec<_>, _>>()?;
  48. info!("Read directory success {:?}", &self.path);
  49. Ok(self)
  50. }
  51. /// Create a new Dir object filled with all the files in the directory
  52. /// pointed to by the given path. Fails if the directory can’t be read, or
  53. /// isn’t actually a directory, or if there’s an IO error that occurs at
  54. /// any point.
  55. ///
  56. /// The `read_dir` iterator doesn’t actually yield the `.` and `..`
  57. /// entries, so if the user wants to see them, we’ll have to add them
  58. /// ourselves after the files have been read.
  59. pub fn read_dir(path: PathBuf) -> io::Result<Self> {
  60. info!("Reading directory {:?}", &path);
  61. let contents = fs::read_dir(&path)?.collect::<Result<Vec<_>, _>>()?;
  62. info!("Read directory success {:?}", &path);
  63. Ok(Self { contents, path })
  64. }
  65. /// Produce an iterator of IO results of trying to read all the files in
  66. /// this directory.
  67. #[must_use]
  68. pub fn files<'dir, 'ig>(
  69. &'dir self,
  70. dots: DotFilter,
  71. git: Option<&'ig GitCache>,
  72. git_ignoring: bool,
  73. deref_links: bool,
  74. total_size: bool,
  75. ) -> Files<'dir, 'ig> {
  76. Files {
  77. inner: self.contents.iter(),
  78. dir: self,
  79. dotfiles: dots.shows_dotfiles(),
  80. dots: dots.dots(),
  81. git,
  82. git_ignoring,
  83. deref_links,
  84. total_size,
  85. }
  86. }
  87. /// Whether this directory contains a file with the given path.
  88. #[must_use]
  89. pub fn contains(&self, path: &Path) -> bool {
  90. self.contents.iter().any(|p| p.path().as_path() == path)
  91. }
  92. /// Append a path onto the path specified by this directory.
  93. #[must_use]
  94. pub fn join(&self, child: &Path) -> PathBuf {
  95. self.path.join(child)
  96. }
  97. }
  98. /// Iterator over reading the contents of a directory as `File` objects.
  99. #[allow(clippy::struct_excessive_bools)]
  100. pub struct Files<'dir, 'ig> {
  101. /// The internal iterator over the paths that have been read already.
  102. inner: SliceIter<'dir, DirEntry>,
  103. /// The directory that begat those paths.
  104. dir: &'dir Dir,
  105. /// Whether to include dotfiles in the list.
  106. dotfiles: bool,
  107. /// Whether the `.` or `..` directories should be produced first, before
  108. /// any files have been listed.
  109. dots: DotsNext,
  110. git: Option<&'ig GitCache>,
  111. git_ignoring: bool,
  112. /// Whether symbolic links should be dereferenced when querying information.
  113. deref_links: bool,
  114. /// Whether to calculate the directory size recursively
  115. total_size: bool,
  116. }
  117. impl<'dir> Files<'dir, '_> {
  118. fn parent(&self) -> PathBuf {
  119. // We can’t use `Path#parent` here because all it does is remove the
  120. // last path component, which is no good for us if the path is
  121. // relative. For example, while the parent of `/testcases/files` is
  122. // `/testcases`, the parent of `.` is an empty path. Adding `..` on
  123. // the end is the only way to get to the *actual* parent directory.
  124. self.dir.path.join("..")
  125. }
  126. /// Go through the directory until we encounter a file we can list (which
  127. /// varies depending on the dotfile visibility flag)
  128. fn next_visible_file(&mut self) -> Option<File<'dir>> {
  129. loop {
  130. if let Some(entry) = self.inner.next() {
  131. let path = entry.path();
  132. let filename = File::filename(&path);
  133. if !self.dotfiles && filename.starts_with('.') {
  134. continue;
  135. }
  136. // Also hide _prefix files on Windows because it's used by old applications
  137. // as an alternative to dot-prefix files.
  138. #[cfg(windows)]
  139. if !self.dotfiles && filename.starts_with('_') {
  140. continue;
  141. }
  142. if self.git_ignoring {
  143. let git_status = self.git.map(|g| g.get(&path, false)).unwrap_or_default();
  144. if git_status.unstaged == GitStatus::Ignored {
  145. continue;
  146. }
  147. }
  148. let file = File::from_args(
  149. path,
  150. self.dir,
  151. filename,
  152. self.deref_links,
  153. self.total_size,
  154. entry.file_type().ok(),
  155. );
  156. // Windows has its own concept of hidden files, when dotfiles are
  157. // hidden Windows hidden files should also be filtered out
  158. #[cfg(windows)]
  159. if !self.dotfiles && file.attributes().map_or(false, |a| a.hidden) {
  160. continue;
  161. }
  162. return Some(file);
  163. }
  164. return None;
  165. }
  166. }
  167. }
  168. /// The dot directories that need to be listed before actual files, if any.
  169. /// If these aren’t being printed, then `FilesNext` is used to skip them.
  170. enum DotsNext {
  171. /// List the `.` directory next.
  172. Dot,
  173. /// List the `..` directory next.
  174. DotDot,
  175. /// Forget about the dot directories and just list files.
  176. Files,
  177. }
  178. impl<'dir> Iterator for Files<'dir, '_> {
  179. type Item = File<'dir>;
  180. fn next(&mut self) -> Option<Self::Item> {
  181. match self.dots {
  182. DotsNext::Dot => {
  183. self.dots = DotsNext::DotDot;
  184. Some(File::new_aa_current(self.dir, self.total_size))
  185. }
  186. DotsNext::DotDot => {
  187. self.dots = DotsNext::Files;
  188. Some(File::new_aa_parent(
  189. self.parent(),
  190. self.dir,
  191. self.total_size,
  192. ))
  193. }
  194. DotsNext::Files => self.next_visible_file(),
  195. }
  196. }
  197. }
  198. /// Usually files in Unix use a leading dot to be hidden or visible, but two
  199. /// entries in particular are “extra-hidden”: `.` and `..`, which only become
  200. /// visible after an extra `-a` option.
  201. #[derive(PartialEq, Eq, Debug, Default, Copy, Clone)]
  202. pub enum DotFilter {
  203. /// Shows files, dotfiles, and `.` and `..`.
  204. DotfilesAndDots,
  205. /// Show files and dotfiles, but hide `.` and `..`.
  206. Dotfiles,
  207. /// Just show files, hiding anything beginning with a dot.
  208. #[default]
  209. JustFiles,
  210. }
  211. impl DotFilter {
  212. /// Whether this filter should show dotfiles in a listing.
  213. fn shows_dotfiles(self) -> bool {
  214. match self {
  215. Self::JustFiles => false,
  216. Self::Dotfiles => true,
  217. Self::DotfilesAndDots => true,
  218. }
  219. }
  220. /// Whether this filter should add dot directories to a listing.
  221. fn dots(self) -> DotsNext {
  222. match self {
  223. Self::JustFiles => DotsNext::Files,
  224. Self::Dotfiles => DotsNext::Files,
  225. Self::DotfilesAndDots => DotsNext::Dot,
  226. }
  227. }
  228. }