at main 113 lines 3.7 kB view raw
1/// Credit to https://github.com/zoni 2/// 3/// Modified from https://github.com/zoni/obsidian-export/blob/main/src/walker.rs on 2025-05-21 4/// 5use std::fmt; 6use std::path::{Path, PathBuf}; 7 8use ignore::{DirEntry, Walk, WalkBuilder}; 9 10use crate::RenderError; 11 12type FilterFn = dyn Fn(&DirEntry) -> bool + Send + Sync + 'static; 13 14/// `WalkOptions` specifies how an Obsidian vault directory is scanned for eligible files to export. 15#[derive(Clone)] 16#[allow(clippy::exhaustive_structs)] 17pub struct WalkOptions<'a> { 18 /// The filename for ignore files, following the 19 /// [gitignore](https://git-scm.com/docs/gitignore) syntax. 20 /// 21 /// By default `.export-ignore` is used. 22 pub ignore_filename: &'a str, 23 /// Whether to ignore hidden files. 24 /// 25 /// This is enabled by default. 26 pub ignore_hidden: bool, 27 /// Whether to honor git's ignore rules (`.gitignore` files, `.git/config/exclude`, etc) if 28 /// the target is within a git repository. 29 /// 30 /// This is enabled by default. 31 pub honor_gitignore: bool, 32 /// An optional custom filter function which is called for each directory entry to determine if 33 /// it should be included or not. 34 /// 35 /// This is passed to [`ignore::WalkBuilder::filter_entry`]. 36 pub filter_fn: Option<&'static FilterFn>, 37} 38 39impl<'a> fmt::Debug for WalkOptions<'a> { 40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 let filter_fn_fmt = match self.filter_fn { 42 Some(_) => "<function set>", 43 None => "<not set>", 44 }; 45 f.debug_struct("WalkOptions") 46 .field("ignore_filename", &self.ignore_filename) 47 .field("ignore_hidden", &self.ignore_hidden) 48 .field("honor_gitignore", &self.honor_gitignore) 49 .field("filter_fn", &filter_fn_fmt) 50 .finish() 51 } 52} 53 54impl<'a> WalkOptions<'a> { 55 /// Create a new set of options using default values. 56 #[must_use] 57 pub fn new() -> Self { 58 WalkOptions { 59 ignore_filename: ".export-ignore", 60 ignore_hidden: true, 61 honor_gitignore: true, 62 filter_fn: None, 63 } 64 } 65 66 fn build_walker(self, path: &Path) -> Walk { 67 let mut walker = WalkBuilder::new(path); 68 walker 69 .standard_filters(false) 70 .parents(true) 71 .hidden(self.ignore_hidden) 72 .add_custom_ignore_filename(self.ignore_filename) 73 .require_git(true) 74 .git_ignore(self.honor_gitignore) 75 .git_global(self.honor_gitignore) 76 .git_exclude(self.honor_gitignore); 77 78 if let Some(filter) = self.filter_fn { 79 walker.filter_entry(filter); 80 } 81 walker.build() 82 } 83} 84 85impl<'a> Default for WalkOptions<'a> { 86 fn default() -> Self { 87 Self::new() 88 } 89} 90 91/// `vault_contents` returns all of the files in an Obsidian vault located at `path` which would be 92/// exported when using the given [`WalkOptions`]. 93pub fn vault_contents(root: &Path, opts: WalkOptions<'_>) -> Result<Vec<PathBuf>, RenderError> { 94 let mut contents = Vec::new(); 95 let walker = opts.build_walker(root); 96 for entry in walker { 97 let entry = entry.map_err(|e| RenderError::WalkDirError { 98 path: root.to_path_buf(), 99 msg: e.to_string(), 100 })?; 101 let path = entry.path(); 102 let metadata = entry.metadata().map_err(|e| RenderError::WalkDirError { 103 path: root.to_path_buf(), 104 msg: e.to_string(), 105 })?; 106 107 if metadata.is_dir() { 108 continue; 109 } 110 contents.push(path.to_path_buf()); 111 } 112 Ok(contents) 113}