1use std::{
2 fs::{self, File},
3 io::{self, Read},
4 path::{Path, PathBuf},
5 time::SystemTime,
6};
7
8use globset::{Glob, GlobSet, GlobSetBuilder};
9use rayon::prelude::*;
10
11use crate::error::{Result, SyncwebError};
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15#[non_exhaustive]
16pub enum FileType {
17 File,
18 Directory,
19 Symlink,
20}
21
22#[derive(Clone, Debug)]
24#[non_exhaustive]
25pub struct FileEntry {
26 pub path: PathBuf,
27 pub relative_path: PathBuf,
28 pub size: u64,
29 pub modified: SystemTime,
30 pub hash: blake3::Hash,
31 pub file_type: FileType,
32}
33
34impl FileEntry {
35 #[must_use]
36 pub fn builder() -> FileEntryBuilder {
37 FileEntryBuilder::default()
38 }
39}
40
41#[derive(Clone, Default)]
42pub struct FileEntryBuilder {
43 path: Option<PathBuf>,
44 relative_path: Option<PathBuf>,
45 size: Option<u64>,
46 modified: Option<SystemTime>,
47 hash: Option<blake3::Hash>,
48 file_type: Option<FileType>,
49}
50
51impl FileEntryBuilder {
52 #[must_use]
53 pub fn path(mut self, path: PathBuf) -> Self {
54 self.path = Some(path);
55 self
56 }
57 #[must_use]
58 pub fn relative_path(mut self, relative_path: PathBuf) -> Self {
59 self.relative_path = Some(relative_path);
60 self
61 }
62 #[must_use]
63 pub const fn size(mut self, size: u64) -> Self {
64 self.size = Some(size);
65 self
66 }
67 #[must_use]
68 pub const fn modified(mut self, modified: SystemTime) -> Self {
69 self.modified = Some(modified);
70 self
71 }
72 #[must_use]
73 pub const fn hash(mut self, hash: blake3::Hash) -> Self {
74 self.hash = Some(hash);
75 self
76 }
77 #[must_use]
78 pub const fn file_type(mut self, file_type: FileType) -> Self {
79 self.file_type = Some(file_type);
80 self
81 }
82 pub fn build(self) -> std::result::Result<FileEntry, &'static str> {
88 Ok(FileEntry {
89 path: self.path.ok_or("path missing")?,
90 relative_path: self.relative_path.ok_or("relative_path missing")?,
91 size: self.size.ok_or("size missing")?,
92 modified: self.modified.ok_or("modified missing")?,
93 hash: self.hash.ok_or("hash missing")?,
94 file_type: self.file_type.ok_or("file_type missing")?,
95 })
96 }
97}
98
99#[derive(Clone, Debug, Default, Eq, PartialEq)]
101pub struct IgnoreFilter {
102 patterns: Vec<String>,
103}
104
105impl IgnoreFilter {
106 #[must_use]
107 pub fn new<I, S>(patterns: I) -> Self
108 where
109 I: IntoIterator<Item = S>,
110 S: Into<String>,
111 {
112 Self {
113 patterns: patterns.into_iter().map(Into::into).collect(),
114 }
115 }
116
117 #[must_use]
118 pub fn patterns(&self) -> &[String] {
119 &self.patterns
120 }
121}
122
123impl From<Vec<String>> for IgnoreFilter {
124 fn from(patterns: Vec<String>) -> Self {
125 Self { patterns }
126 }
127}
128
129pub trait ThreadCount {
131 fn into_thread_count(self) -> Option<usize>;
132}
133
134impl ThreadCount for usize {
135 fn into_thread_count(self) -> Option<usize> {
136 (self > 0).then_some(self)
137 }
138}
139
140impl ThreadCount for Option<usize> {
141 fn into_thread_count(self) -> Option<usize> {
142 self.filter(|threads| *threads > 0)
143 }
144}
145
146impl FileEntry {
147 #[must_use]
148 pub fn name(&self) -> Option<&str> {
149 self.path.file_name().and_then(|name| name.to_str())
150 }
151}
152
153#[derive(Clone)]
154struct IgnoreSet {
155 set: GlobSet,
156}
157
158impl IgnoreSet {
159 fn new<I, S>(patterns: I) -> Self
160 where
161 I: IntoIterator<Item = S>,
162 S: AsRef<str>,
163 {
164 let mut builder = GlobSetBuilder::new();
165 for pattern in patterns {
166 if let Ok(glob) = Glob::new(pattern.as_ref()) {
167 builder.add(glob);
168 }
169 }
170 let set = builder
171 .build()
172 .unwrap_or_else(|_| GlobSetBuilder::new().build().unwrap_or_default());
173 Self { set }
174 }
175
176 fn matches(&self, relative: &Path) -> bool {
177 self.set.is_match(relative)
178 || relative
179 .components()
180 .any(|component| self.set.is_match(Path::new(component.as_os_str())))
181 }
182}
183
184#[derive(Clone)]
186pub struct Scanner {
187 root: PathBuf,
188 ignore: IgnoreSet,
189}
190
191impl Scanner {
192 #[must_use]
193 pub fn new(root: impl Into<PathBuf>, ignore_patterns_arg: impl Into<IgnoreFilter>) -> Self {
194 let ignore_patterns = ignore_patterns_arg.into();
195 Self {
196 root: root.into(),
197 ignore: IgnoreSet::new(ignore_patterns.patterns()),
198 }
199 }
200
201 #[must_use]
202 pub fn root(&self) -> &Path {
203 &self.root
204 }
205
206 pub fn scan(&self) -> Result<Vec<FileEntry>> {
211 if self.root.is_file() {
212 let relative = self.root.file_name().map_or_else(PathBuf::new, PathBuf::from);
213 return Ok(vec![hash_file(&self.root, &relative)?]);
214 }
215 let mut entries = Vec::new();
216 self.scan_dir(&self.root, Path::new(""), &mut entries)?;
217 entries.sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
218 Ok(entries)
219 }
220
221 pub fn scan_all(&self) -> Result<Vec<FileEntry>> {
230 if self.root.is_file() {
231 let relative = self.root.file_name().map_or_else(PathBuf::new, PathBuf::from);
232 return Ok(vec![metadata_entry(&self.root, &relative)?]);
233 }
234 let mut entries = Vec::new();
235 self.scan_dir_all(&self.root, Path::new(""), &mut entries)?;
236 entries.sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
237 Ok(entries)
238 }
239
240 fn scan_dir(&self, directory: &Path, relative: &Path, output: &mut Vec<FileEntry>) -> Result<()> {
241 let mut children = fs::read_dir(directory)?.collect::<std::result::Result<Vec<_>, io::Error>>()?;
242 children.sort_by_key(std::fs::DirEntry::file_name);
243 for child in children {
244 let path = child.path();
245 let child_relative = relative.join(child.file_name());
246 if self.ignore.matches(&child_relative) {
247 continue;
248 }
249 let file_type = child.file_type()?;
250 if file_type.is_dir() {
251 self.scan_dir(&path, &child_relative, output)?;
252 } else if file_type.is_file() {
253 output.push(hash_file(&path, &child_relative)?);
254 } else {
255 }
257 }
258 Ok(())
259 }
260
261 fn scan_dir_all(&self, directory: &Path, relative: &Path, output: &mut Vec<FileEntry>) -> Result<()> {
262 let mut children = fs::read_dir(directory)?.collect::<std::result::Result<Vec<_>, io::Error>>()?;
263 children.sort_by_key(std::fs::DirEntry::file_name);
264 for child in children {
265 let path = child.path();
266 let child_relative = relative.join(child.file_name());
267 if self.ignore.matches(&child_relative) {
268 continue;
269 }
270 let file_type = child.file_type()?;
271 output.push(metadata_entry(&path, &child_relative)?);
272 if file_type.is_dir() {
273 self.scan_dir_all(&path, &child_relative, output)?;
274 }
275 }
276 Ok(())
277 }
278}
279
280#[derive(Clone)]
282pub struct ParallelScanner {
283 scanner: Scanner,
284 threads: Option<usize>,
285}
286
287impl ParallelScanner {
288 #[must_use]
289 pub fn new<T: ThreadCount>(root: impl Into<PathBuf>, ignore_patterns: impl Into<IgnoreFilter>, threads: T) -> Self {
290 Self {
291 scanner: Scanner::new(root, ignore_patterns),
292 threads: threads.into_thread_count(),
293 }
294 }
295
296 #[must_use]
297 pub fn root(&self) -> &Path {
298 self.scanner.root()
299 }
300
301 pub fn scan(&self) -> Result<Vec<FileEntry>> {
306 if self.scanner.root.is_file() {
307 let relative = self.scanner.root.file_name().map_or_else(PathBuf::new, PathBuf::from);
308 return Ok(vec![hash_file(&self.scanner.root, &relative)?]);
309 }
310 let mut paths = Vec::new();
311 collect_paths(self.scanner.root(), Path::new(""), &self.scanner.ignore, &mut paths)?;
312 let run = || {
313 paths
314 .par_iter()
315 .map(|(path, relative)| hash_file(path, relative))
316 .collect::<Result<Vec<_>>>()
317 };
318 let mut entries = if let Some(threads) = self.threads {
319 rayon::ThreadPoolBuilder::new()
320 .num_threads(threads)
321 .build()
322 .map_err(|error| SyncwebError::operation("failed to create scanner thread pool", error))?
323 .install(run)?
324 } else {
325 run()?
326 };
327 entries.sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
328 Ok(entries)
329 }
330
331 pub fn scan_parallel(&self) -> Result<Vec<FileEntry>> {
336 self.scan()
337 }
338
339 pub fn scan_all(&self) -> Result<Vec<FileEntry>> {
344 if self.scanner.root.is_file() {
345 let relative = self.scanner.root.file_name().map_or_else(PathBuf::new, PathBuf::from);
346 return Ok(vec![metadata_entry(&self.scanner.root, &relative)?]);
347 }
348 let mut paths = Vec::new();
349 collect_all_paths(self.scanner.root(), Path::new(""), &self.scanner.ignore, &mut paths)?;
350 let run = || {
351 paths
352 .par_iter()
353 .map(|(path, relative)| metadata_entry(path, relative))
354 .collect::<Result<Vec<_>>>()
355 };
356 let mut entries = if let Some(threads) = self.threads {
357 rayon::ThreadPoolBuilder::new()
358 .num_threads(threads)
359 .build()
360 .map_err(|error| SyncwebError::operation("failed to create scanner thread pool", error))?
361 .install(run)?
362 } else {
363 run()?
364 };
365 entries.sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
366 Ok(entries)
367 }
368}
369
370fn collect_paths(
371 directory: &Path,
372 relative: &Path,
373 ignore: &IgnoreSet,
374 paths: &mut Vec<(PathBuf, PathBuf)>,
375) -> Result<()> {
376 let mut children = fs::read_dir(directory)?.collect::<std::result::Result<Vec<_>, io::Error>>()?;
377 children.sort_by_key(std::fs::DirEntry::file_name);
378 for child in children {
379 let path = child.path();
380 let child_relative = relative.join(child.file_name());
381 if ignore.matches(&child_relative) {
382 continue;
383 }
384 let file_type = child.file_type()?;
385 if file_type.is_dir() {
386 collect_paths(&path, &child_relative, ignore, paths)?;
387 } else if file_type.is_file() {
388 paths.push((path, child_relative));
389 } else {
390 }
392 }
393 Ok(())
394}
395
396fn collect_all_paths(
397 directory: &Path,
398 relative: &Path,
399 ignore: &IgnoreSet,
400 paths: &mut Vec<(PathBuf, PathBuf)>,
401) -> Result<()> {
402 let mut children = fs::read_dir(directory)?.collect::<std::result::Result<Vec<_>, io::Error>>()?;
403 children.sort_by_key(std::fs::DirEntry::file_name);
404 for child in children {
405 let path = child.path();
406 let child_relative = relative.join(child.file_name());
407 if ignore.matches(&child_relative) {
408 continue;
409 }
410 let file_type = child.file_type()?;
411 paths.push((path.clone(), child_relative.clone()));
412 if file_type.is_dir() {
413 collect_all_paths(&path, &child_relative, ignore, paths)?;
414 }
415 }
416 Ok(())
417}
418
419fn hash_file(path: &Path, relative: &Path) -> Result<FileEntry> {
420 let mut file = File::open(path)?;
421 let mut hasher = blake3::Hasher::new();
422 let mut buffer = vec![0_u8; 128 * 1024];
423 loop {
424 let read = file.read(&mut buffer)?;
425 if read == 0 {
426 break;
427 }
428 if let Some(chunk) = buffer.get(..read) {
429 hasher.update(chunk);
430 }
431 }
432 let metadata = fs::metadata(path)?;
433 Ok(FileEntry {
434 path: path.to_path_buf(),
435 relative_path: relative.to_path_buf(),
436 size: metadata.len(),
437 modified: metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
438 hash: hasher.finalize(),
439 file_type: if path.is_symlink() {
440 FileType::Symlink
441 } else if path.is_dir() {
442 FileType::Directory
443 } else {
444 FileType::File
445 },
446 })
447}
448
449fn metadata_entry(path: &Path, relative: &Path) -> Result<FileEntry> {
450 let metadata = fs::symlink_metadata(path)?;
451 let file_type = if metadata.file_type().is_symlink() {
452 FileType::Symlink
453 } else if metadata.is_dir() {
454 FileType::Directory
455 } else {
456 FileType::File
457 };
458 let hash = if file_type == FileType::File {
459 hash_file(path, relative)?.hash
460 } else {
461 blake3::hash(&[])
462 };
463 Ok(FileEntry {
464 path: path.to_path_buf(),
465 relative_path: relative.to_path_buf(),
466 size: metadata.len(),
467 modified: metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
468 hash,
469 file_type,
470 })
471}