1use std::path::{Path, PathBuf};
2
3use ignore::{WalkBuilder, overrides::OverrideBuilder};
4
5use crate::error::{Error, Result};
6
7#[derive(Debug, Clone)]
9pub struct FileEntry {
10 pub path: PathBuf,
12 pub is_dir: bool,
13 pub size: u64,
14}
15
16#[derive(Debug, Default)]
19pub struct FileIndex {
20 pub entries: Vec<FileEntry>,
21}
22
23impl FileIndex {
24 pub fn files(&self) -> impl Iterator<Item = &FileEntry> {
25 self.entries.iter().filter(|e| !e.is_dir)
26 }
27
28 pub fn dirs(&self) -> impl Iterator<Item = &FileEntry> {
29 self.entries.iter().filter(|e| e.is_dir)
30 }
31
32 pub fn total_size(&self) -> u64 {
33 self.files().map(|f| f.size).sum()
34 }
35
36 pub fn find_file(&self, rel: &Path) -> Option<&FileEntry> {
40 self.files().find(|e| e.path == rel)
41 }
42}
43
44#[derive(Debug, Clone)]
45pub struct WalkOptions {
46 pub respect_gitignore: bool,
47 pub extra_ignores: Vec<String>,
48}
49
50impl Default for WalkOptions {
51 fn default() -> Self {
52 Self {
53 respect_gitignore: true,
54 extra_ignores: Vec::new(),
55 }
56 }
57}
58
59pub fn walk(root: &Path, opts: &WalkOptions) -> Result<FileIndex> {
60 let mut builder = WalkBuilder::new(root);
61 builder
62 .standard_filters(opts.respect_gitignore)
63 .hidden(false)
64 .follow_links(true)
65 .require_git(false);
66
67 if !opts.extra_ignores.is_empty() {
68 let mut overrides = OverrideBuilder::new(root);
69 for pattern in &opts.extra_ignores {
70 let pattern = if pattern.starts_with('!') {
71 pattern.clone()
72 } else {
73 format!("!{pattern}")
74 };
75 overrides
76 .add(&pattern)
77 .map_err(|e| Error::Other(format!("ignore pattern {pattern:?}: {e}")))?;
78 }
79 let overrides = overrides
80 .build()
81 .map_err(|e| Error::Other(format!("failed to build overrides: {e}")))?;
82 builder.overrides(overrides);
83 }
84
85 let mut entries = Vec::new();
86 for result in builder.build() {
87 let entry = result?;
88 let abs = entry.path();
89 let Ok(rel) = abs.strip_prefix(root) else {
90 continue;
91 };
92 if rel.as_os_str().is_empty() {
93 continue;
94 }
95 let metadata = entry.metadata().map_err(|e| Error::Io {
96 path: abs.to_path_buf(),
97 source: std::io::Error::other(e.to_string()),
98 })?;
99 entries.push(FileEntry {
100 path: rel.to_path_buf(),
101 is_dir: metadata.is_dir(),
102 size: if metadata.is_file() {
103 metadata.len()
104 } else {
105 0
106 },
107 });
108 }
109 Ok(FileIndex { entries })
110}