intermodal/src/walker.rs
Casey Rodarmor 97018031c1
Introduce "sort specs" to allow fine-grained sorting of files in torrents
Sort specs are of the form `KEY:ORDER`, and allow sorting files in a
torrent by multiple criteria. Multiple sort specs can be passed with
`--sort-by` upon torrent creation.

type: added
2020-04-07 19:01:33 -07:00

214 lines
4.6 KiB
Rust

use crate::common::*;
const JUNK: &[&str] = &["Thumbs.db", "Desktop.ini"];
#[derive(Debug)]
struct Pattern {
glob: GlobMatcher,
include: bool,
}
pub(crate) struct Walker {
follow_symlinks: bool,
include_hidden: bool,
include_junk: bool,
sort_by: Vec<SortSpec>,
patterns: Vec<Pattern>,
root: PathBuf,
spinner: Option<ProgressBar>,
}
impl Walker {
pub(crate) fn new(root: &Path) -> Self {
Self {
follow_symlinks: false,
include_hidden: false,
include_junk: false,
sort_by: Vec::new(),
patterns: Vec::new(),
root: root.to_owned(),
spinner: None,
}
}
pub(crate) fn include_junk(self, include_junk: bool) -> Self {
Self {
include_junk,
..self
}
}
pub(crate) fn include_hidden(self, include_hidden: bool) -> Self {
Self {
include_hidden,
..self
}
}
pub(crate) fn sort_by(self, sort_by: Vec<SortSpec>) -> Self {
Self { sort_by, ..self }
}
pub(crate) fn globs(mut self, globs: &[String]) -> Result<Self, Error> {
for glob in globs {
let exclude = glob.starts_with('!');
let glob = Glob::new(if exclude { &glob[1..] } else { glob })?.compile_matcher();
self.patterns.push(Pattern {
glob,
include: !exclude,
});
}
Ok(self)
}
pub(crate) fn follow_symlinks(self, follow_symlinks: bool) -> Self {
Self {
follow_symlinks,
..self
}
}
pub(crate) fn spinner(self, spinner: Option<ProgressBar>) -> Self {
Self { spinner, ..self }
}
pub(crate) fn files(self) -> Result<Files, Error> {
if !self.follow_symlinks
&& self
.root
.symlink_metadata()
.context(error::Filesystem { path: &self.root })?
.file_type()
.is_symlink()
{
return Err(Error::SymlinkRoot { root: self.root });
}
let root_metadata = self
.root
.metadata()
.context(error::Filesystem { path: &self.root })?;
if root_metadata.is_file() {
return Ok(Files::file(self.root, Bytes::from(root_metadata.len())));
}
let filter = |entry: &walkdir::DirEntry| {
let path = entry.path();
if let Some(s) = &self.spinner {
let display_path = path.strip_prefix(&self.root).unwrap_or(&path);
s.set_message(&display_path.display().to_string());
s.tick();
}
let file_name = entry.file_name();
if !self.include_hidden && file_name.to_string_lossy().starts_with('.') {
return false;
}
let hidden = Platform::hidden(path).unwrap_or(true);
if !self.include_hidden && hidden {
return false;
}
true
};
let mut file_infos = Vec::new();
let mut total_size = 0;
for result in WalkDir::new(&self.root)
.follow_links(self.follow_symlinks)
.into_iter()
.filter_entry(filter)
{
let entry = result?;
let path = entry.path();
let metadata = entry.metadata()?;
if !metadata.is_file() {
continue;
}
let relative = path
.strip_prefix(&self.root)
.context(error::PathStripPrefix {
path,
prefix: &self.root,
})?;
if relative.components().count() == 0 {
return Err(Error::PathStripEmpty {
prefix: self.root.clone(),
path: path.to_owned(),
});
}
if !self.pattern_filter(&relative) {
continue;
}
let file_path = FilePath::from_relative_path(relative)?;
if !self.include_junk && JUNK.contains(&file_path.name()) {
continue;
}
let len = metadata.len();
total_size += len;
file_infos.push(FileInfo {
path: file_path,
length: Bytes(len),
md5sum: None,
});
}
file_infos.sort_by(|a, b| SortSpec::compare(&self.sort_by, a, b));
Ok(Files::dir(
self.root,
Bytes::from(total_size),
file_infos
.into_iter()
.map(|file_info| file_info.path)
.collect(),
))
}
fn pattern_filter(&self, relative: &Path) -> bool {
for Pattern { glob, include } in self.patterns.iter().rev() {
if glob.is_match(relative) {
return *include;
}
}
if let Some(Pattern { include, .. }) = self.patterns.first() {
return !include;
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn glob() {
let walker = Walker::new(Path::new("foo"))
.globs(&["[bc]".into()])
.unwrap();
assert!(!walker.pattern_filter(Path::new("a")));
assert!(walker.pattern_filter(Path::new("b")));
assert!(walker.pattern_filter(Path::new("c")));
}
}