atproto blogging
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}