Support creating multi-file torrents
type: added
This commit is contained in:
parent
551617de4f
commit
3739a92857
|
@ -12,7 +12,7 @@ pub(crate) use std::{
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
num::ParseFloatError,
|
num::ParseFloatError,
|
||||||
ops::{Div, DivAssign, Mul, MulAssign},
|
ops::{Div, DivAssign, Mul, MulAssign},
|
||||||
path::{Path, PathBuf},
|
path::{self, Path, PathBuf},
|
||||||
process::{self, Command, ExitStatus},
|
process::{self, Command, ExitStatus},
|
||||||
str::{self, FromStr},
|
str::{self, FromStr},
|
||||||
time::{SystemTime, SystemTimeError},
|
time::{SystemTime, SystemTimeError},
|
||||||
|
@ -46,10 +46,10 @@ pub(crate) use crate::{
|
||||||
|
|
||||||
// structs and enums
|
// structs and enums
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
bytes::Bytes, env::Env, error::Error, file_info::FileInfo, files::Files, hasher::Hasher,
|
bytes::Bytes, env::Env, error::Error, file_info::FileInfo, file_path::FilePath, files::Files,
|
||||||
info::Info, lint::Lint, linter::Linter, metainfo::Metainfo, mode::Mode, opt::Opt,
|
hasher::Hasher, info::Info, lint::Lint, linter::Linter, metainfo::Metainfo, mode::Mode, opt::Opt,
|
||||||
piece_length_picker::PieceLengthPicker, platform::Platform, style::Style, table::Table,
|
piece_length_picker::PieceLengthPicker, platform::Platform, style::Style, table::Table,
|
||||||
target::Target, torrent_summary::TorrentSummary, use_color::UseColor,
|
target::Target, torrent_summary::TorrentSummary, use_color::UseColor, walker::Walker,
|
||||||
};
|
};
|
||||||
|
|
||||||
// test stdlib types
|
// test stdlib types
|
||||||
|
|
29
src/error.rs
29
src/error.rs
|
@ -37,6 +37,35 @@ pub(crate) enum Error {
|
||||||
Filesystem { source: io::Error, path: PathBuf },
|
Filesystem { source: io::Error, path: PathBuf },
|
||||||
#[snafu(display("Failed to find opener utility, please install one of {}", tried.join(",")))]
|
#[snafu(display("Failed to find opener utility, please install one of {}", tried.join(",")))]
|
||||||
OpenerMissing { tried: &'static [&'static str] },
|
OpenerMissing { tried: &'static [&'static str] },
|
||||||
|
#[snafu(display(
|
||||||
|
"Path `{}` contains non-normal component: {}",
|
||||||
|
path.display(),
|
||||||
|
component.display(),
|
||||||
|
))]
|
||||||
|
PathComponent { component: PathBuf, path: PathBuf },
|
||||||
|
#[snafu(display(
|
||||||
|
"Path `{}` contains non-unicode component: {}",
|
||||||
|
path.display(),
|
||||||
|
component.display(),
|
||||||
|
))]
|
||||||
|
PathDecode { path: PathBuf, component: PathBuf },
|
||||||
|
#[snafu(display(
|
||||||
|
"Path `{}` empty after stripping prefix `{}`",
|
||||||
|
path.display(),
|
||||||
|
prefix.display(),
|
||||||
|
))]
|
||||||
|
PathStripEmpty { path: PathBuf, prefix: PathBuf },
|
||||||
|
#[snafu(display(
|
||||||
|
"Failed to strip prefix `{}` from path `{}`: {}",
|
||||||
|
prefix.display(),
|
||||||
|
path.display(),
|
||||||
|
source
|
||||||
|
))]
|
||||||
|
PathStripPrefix {
|
||||||
|
path: PathBuf,
|
||||||
|
prefix: PathBuf,
|
||||||
|
source: path::StripPrefixError,
|
||||||
|
},
|
||||||
#[snafu(display(
|
#[snafu(display(
|
||||||
"Piece length `{}` too large. The maximum supported piece length is {}.",
|
"Piece length `{}` too large. The maximum supported piece length is {}.",
|
||||||
bytes,
|
bytes,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||||
pub struct FileInfo {
|
pub(crate) struct FileInfo {
|
||||||
pub length: u64,
|
pub(crate) length: u64,
|
||||||
pub md5sum: Option<String>,
|
pub(crate) md5sum: Option<String>,
|
||||||
pub path: Vec<String>,
|
pub(crate) path: FilePath,
|
||||||
}
|
}
|
||||||
|
|
58
src/file_path.rs
Normal file
58
src/file_path.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[serde(transparent)]
|
||||||
|
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||||
|
pub(crate) struct FilePath {
|
||||||
|
components: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilePath {
|
||||||
|
pub(crate) fn from_prefix_and_path(prefix: &Path, path: &Path) -> Result<FilePath, Error> {
|
||||||
|
let relative = path
|
||||||
|
.strip_prefix(prefix)
|
||||||
|
.context(error::PathStripPrefix { prefix, path })?;
|
||||||
|
|
||||||
|
let mut components = Vec::new();
|
||||||
|
|
||||||
|
for component in relative.components() {
|
||||||
|
match component {
|
||||||
|
path::Component::Normal(os) => {
|
||||||
|
if let Some(unicode) = os.to_str() {
|
||||||
|
components.push(unicode.to_owned());
|
||||||
|
} else {
|
||||||
|
return Err(Error::PathDecode {
|
||||||
|
path: relative.to_owned(),
|
||||||
|
component: PathBuf::from(component.as_os_str()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(Error::PathComponent {
|
||||||
|
path: relative.to_owned(),
|
||||||
|
component: PathBuf::from(component.as_os_str()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if components.is_empty() {
|
||||||
|
return Err(Error::PathStripEmpty {
|
||||||
|
prefix: prefix.to_owned(),
|
||||||
|
path: path.to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(FilePath { components })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn from_components(components: &[&str]) -> FilePath {
|
||||||
|
let components: Vec<String> = components
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|component| component.to_owned())
|
||||||
|
.collect();
|
||||||
|
assert!(!components.is_empty());
|
||||||
|
FilePath { components }
|
||||||
|
}
|
||||||
|
}
|
21
src/files.rs
21
src/files.rs
|
@ -1,26 +1,17 @@
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
pub(crate) struct Files {
|
pub(crate) struct Files {
|
||||||
|
root: PathBuf,
|
||||||
total_size: Bytes,
|
total_size: Bytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Files {
|
impl Files {
|
||||||
pub(crate) fn from_root(root: &Path) -> Result<Files, Error> {
|
pub(crate) fn new(root: PathBuf, total_size: Bytes) -> Files {
|
||||||
let mut total_size = 0;
|
Files { root, total_size }
|
||||||
|
}
|
||||||
|
|
||||||
for result in WalkDir::new(root).sort_by(|a, b| a.file_name().cmp(b.file_name())) {
|
pub(crate) fn root(&self) -> &Path {
|
||||||
let entry = result?;
|
&self.root
|
||||||
|
|
||||||
let metadata = entry.metadata()?;
|
|
||||||
|
|
||||||
if metadata.is_file() {
|
|
||||||
total_size += metadata.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Files {
|
|
||||||
total_size: Bytes::from(total_size),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn total_size(&self) -> Bytes {
|
pub(crate) fn total_size(&self) -> Bytes {
|
||||||
|
|
|
@ -12,11 +12,11 @@ pub(crate) struct Hasher {
|
||||||
|
|
||||||
impl Hasher {
|
impl Hasher {
|
||||||
pub(crate) fn hash(
|
pub(crate) fn hash(
|
||||||
root: &Path,
|
files: &Files,
|
||||||
md5sum: bool,
|
md5sum: bool,
|
||||||
piece_length: u32,
|
piece_length: u32,
|
||||||
) -> Result<(Mode, Vec<u8>), Error> {
|
) -> Result<(Mode, Vec<u8>), Error> {
|
||||||
Self::new(md5sum, piece_length).hash_root(root)
|
Self::new(md5sum, piece_length).hash_root(files.root())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(md5sum: bool, piece_length: u32) -> Self {
|
fn new(md5sum: bool, piece_length: u32) -> Self {
|
||||||
|
@ -64,17 +64,28 @@ impl Hasher {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hash_dir(&mut self, dir: &Path) -> Result<Vec<FileInfo>, Error> {
|
fn hash_dir(&mut self, dir: &Path) -> Result<Vec<FileInfo>, Error> {
|
||||||
|
let mut files = Vec::new();
|
||||||
for result in WalkDir::new(dir).sort_by(|a, b| a.file_name().cmp(b.file_name())) {
|
for result in WalkDir::new(dir).sort_by(|a, b| a.file_name().cmp(b.file_name())) {
|
||||||
let entry = result?;
|
let entry = result?;
|
||||||
|
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
if entry.metadata()?.is_file() {
|
if !entry.metadata()?.is_file() {
|
||||||
let (_md5sum, _length) = self.hash_file(path)?;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (md5sum, length) = self.hash_file(path)?;
|
||||||
|
|
||||||
|
let file_path = FilePath::from_prefix_and_path(dir, path)?;
|
||||||
|
|
||||||
|
files.push(FileInfo {
|
||||||
|
md5sum: md5sum.map(|md5sum| format!("{:x}", md5sum)),
|
||||||
|
path: file_path,
|
||||||
|
length,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Vec::new())
|
Ok(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hash_file(&mut self, file: &Path) -> Result<(Option<md5::Digest>, u64), Error> {
|
fn hash_file(&mut self, file: &Path) -> Result<(Option<md5::Digest>, u64), Error> {
|
||||||
|
|
|
@ -60,6 +60,7 @@ mod consts;
|
||||||
mod env;
|
mod env;
|
||||||
mod error;
|
mod error;
|
||||||
mod file_info;
|
mod file_info;
|
||||||
|
mod file_path;
|
||||||
mod files;
|
mod files;
|
||||||
mod hasher;
|
mod hasher;
|
||||||
mod info;
|
mod info;
|
||||||
|
@ -80,6 +81,7 @@ mod table;
|
||||||
mod target;
|
mod target;
|
||||||
mod torrent_summary;
|
mod torrent_summary;
|
||||||
mod use_color;
|
mod use_color;
|
||||||
|
mod walker;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if let Err(code) = Env::main().status() {
|
if let Err(code) = Env::main().status() {
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::common::*;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum Mode {
|
pub(crate) enum Mode {
|
||||||
Single { length: u64, md5sum: Option<String> },
|
Single { length: u64, md5sum: Option<String> },
|
||||||
Multiple { files: Vec<FileInfo> },
|
Multiple { files: Vec<FileInfo> },
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,7 +123,7 @@ impl Create {
|
||||||
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
|
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
|
||||||
let input = env.resolve(&self.input);
|
let input = env.resolve(&self.input);
|
||||||
|
|
||||||
let files = Files::from_root(&input)?;
|
let files = Walker::new(&input).files()?;
|
||||||
|
|
||||||
let piece_length = self
|
let piece_length = self
|
||||||
.piece_length
|
.piece_length
|
||||||
|
@ -209,7 +209,7 @@ impl Create {
|
||||||
Some(String::from(consts::CREATED_BY_DEFAULT))
|
Some(String::from(consts::CREATED_BY_DEFAULT))
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mode, pieces) = Hasher::hash(&input, self.md5sum, piece_length)?;
|
let (mode, pieces) = Hasher::hash(&files, self.md5sum, piece_length)?;
|
||||||
|
|
||||||
let info = Info {
|
let info = Info {
|
||||||
source: self.source,
|
source: self.source,
|
||||||
|
@ -710,7 +710,35 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multiple_one_file() {
|
fn multiple_one_file_md5() {
|
||||||
|
let mut env = environment(&["--input", "foo", "--announce", "http://bar", "--md5sum"]);
|
||||||
|
let dir = env.resolve("foo");
|
||||||
|
fs::create_dir(&dir).unwrap();
|
||||||
|
let file = dir.join("bar");
|
||||||
|
let contents = "bar";
|
||||||
|
fs::write(file, contents).unwrap();
|
||||||
|
env.run().unwrap();
|
||||||
|
let torrent = env.resolve("foo.torrent");
|
||||||
|
let bytes = fs::read(torrent).unwrap();
|
||||||
|
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
||||||
|
assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes());
|
||||||
|
match metainfo.info.mode {
|
||||||
|
Mode::Multiple { files } => {
|
||||||
|
assert_eq!(
|
||||||
|
files,
|
||||||
|
&[FileInfo {
|
||||||
|
length: 3,
|
||||||
|
md5sum: Some("37b51d194a7513e45b56f6524f2d51f2".to_owned()),
|
||||||
|
path: FilePath::from_components(&["bar"]),
|
||||||
|
},]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected multi-file torrent"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_one_file_md5_off() {
|
||||||
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]);
|
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]);
|
||||||
let dir = env.resolve("foo");
|
let dir = env.resolve("foo");
|
||||||
fs::create_dir(&dir).unwrap();
|
fs::create_dir(&dir).unwrap();
|
||||||
|
@ -722,12 +750,24 @@ mod tests {
|
||||||
let bytes = fs::read(torrent).unwrap();
|
let bytes = fs::read(torrent).unwrap();
|
||||||
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
||||||
assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes());
|
assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes());
|
||||||
assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() })
|
match metainfo.info.mode {
|
||||||
|
Mode::Multiple { files } => {
|
||||||
|
assert_eq!(
|
||||||
|
files,
|
||||||
|
&[FileInfo {
|
||||||
|
length: 3,
|
||||||
|
md5sum: None,
|
||||||
|
path: FilePath::from_components(&["bar"]),
|
||||||
|
},]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected multi-file torrent"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multiple_three_files() {
|
fn multiple_three_files() {
|
||||||
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]);
|
let mut env = environment(&["--input", "foo", "--announce", "http://bar", "--md5sum"]);
|
||||||
let dir = env.resolve("foo");
|
let dir = env.resolve("foo");
|
||||||
fs::create_dir(&dir).unwrap();
|
fs::create_dir(&dir).unwrap();
|
||||||
fs::write(dir.join("a"), "abc").unwrap();
|
fs::write(dir.join("a"), "abc").unwrap();
|
||||||
|
@ -741,7 +781,31 @@ mod tests {
|
||||||
metainfo.info.pieces,
|
metainfo.info.pieces,
|
||||||
Sha1::from("abchijxyz").digest().bytes()
|
Sha1::from("abchijxyz").digest().bytes()
|
||||||
);
|
);
|
||||||
assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() })
|
match metainfo.info.mode {
|
||||||
|
Mode::Multiple { files } => {
|
||||||
|
assert_eq!(
|
||||||
|
files,
|
||||||
|
&[
|
||||||
|
FileInfo {
|
||||||
|
length: 3,
|
||||||
|
md5sum: Some("900150983cd24fb0d6963f7d28e17f72".to_owned()),
|
||||||
|
path: FilePath::from_components(&["a"]),
|
||||||
|
},
|
||||||
|
FileInfo {
|
||||||
|
length: 3,
|
||||||
|
md5sum: Some("857c4402ad934005eae4638a93812bf7".to_owned()),
|
||||||
|
path: FilePath::from_components(&["h"]),
|
||||||
|
},
|
||||||
|
FileInfo {
|
||||||
|
length: 3,
|
||||||
|
md5sum: Some("d16fb36f0911f878998c136191af705e".to_owned()),
|
||||||
|
path: FilePath::from_components(&["x"]),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected multi-file torrent"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -902,14 +966,14 @@ mod tests {
|
||||||
env.run().unwrap();
|
env.run().unwrap();
|
||||||
let have = env.out();
|
let have = env.out();
|
||||||
let want = " Name foo
|
let want = " Name foo
|
||||||
Info Hash 2637812436658f855e99f07c40fe7da5832a7b6d
|
Info Hash d3432a4b9d18baa413095a70f1e417021ceaca5b
|
||||||
Torrent Size 165 bytes
|
Torrent Size 237 bytes
|
||||||
Content Size 0 bytes
|
Content Size 9 bytes
|
||||||
Private no
|
Private no
|
||||||
Tracker http://bar/
|
Tracker http://bar/
|
||||||
Piece Size 16 KiB
|
Piece Size 16 KiB
|
||||||
Piece Count 1
|
Piece Count 1
|
||||||
File Count 0
|
File Count 3
|
||||||
";
|
";
|
||||||
assert_eq!(have, want);
|
assert_eq!(have, want);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,15 @@ impl PlatformInterface for Platform {
|
||||||
OsString::from("start"),
|
OsString::from("start"),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hidden(path: &Path) -> Result<bool, Error> {
|
||||||
|
use std::os::windows::fs::MetadataExt;
|
||||||
|
|
||||||
|
const HIDDEN_MASK_WIN: u32 = 0x0000_0002;
|
||||||
|
|
||||||
|
let metadata = path.metadata().context(error::Filesystem { path })?;
|
||||||
|
Ok((metadata.file_attributes() & HIDDEN_MASK_WIN) != 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
@ -19,6 +28,28 @@ impl PlatformInterface for Platform {
|
||||||
fn opener() -> Result<Vec<OsString>, Error> {
|
fn opener() -> Result<Vec<OsString>, Error> {
|
||||||
Ok(vec![OsString::from("open")])
|
Ok(vec![OsString::from("open")])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hidden(path: &Path) -> Result<bool, Error> {
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
use std::{ffi::CString, mem};
|
||||||
|
|
||||||
|
const HIDDEN_MASK_MAC: u32 = 0x0000_8000;
|
||||||
|
|
||||||
|
let mut stat: libc::stat = unsafe { mem::zeroed() };
|
||||||
|
|
||||||
|
let cpath = CString::new(path.as_os_str().as_bytes()).expect("Path contained null character.");
|
||||||
|
|
||||||
|
let error_code = unsafe { libc::stat(cpath.as_ptr(), &mut stat) };
|
||||||
|
|
||||||
|
if error_code != 0 {
|
||||||
|
return Err(Error::Filesystem {
|
||||||
|
source: io::Error::from_raw_os_error(error_code),
|
||||||
|
path: path.to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(stat.st_flags & HIDDEN_MASK_MAC != 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||||
|
@ -36,4 +67,8 @@ impl PlatformInterface for Platform {
|
||||||
|
|
||||||
Err(Error::OpenerMissing { tried: OPENERS })
|
Err(Error::OpenerMissing { tried: OPENERS })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hidden(path: &Path) -> Result<bool, Error> {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,4 +32,6 @@ pub(crate) trait PlatformInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn opener() -> Result<Vec<OsString>, Error>;
|
fn opener() -> Result<Vec<OsString>, Error>;
|
||||||
|
|
||||||
|
fn hidden(path: &Path) -> Result<bool, Error>;
|
||||||
}
|
}
|
||||||
|
|
69
src/walker.rs
Normal file
69
src/walker.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
pub(crate) struct Walker {
|
||||||
|
include_junk: bool,
|
||||||
|
include_hidden: bool,
|
||||||
|
root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Walker {
|
||||||
|
pub(crate) fn new(root: &Path) -> Walker {
|
||||||
|
Walker {
|
||||||
|
include_junk: false,
|
||||||
|
include_hidden: false,
|
||||||
|
root: root.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn _include_junk(self) -> Self {
|
||||||
|
Walker {
|
||||||
|
include_junk: true,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn _include_hidden(self) -> Self {
|
||||||
|
Walker {
|
||||||
|
include_hidden: true,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn files(self) -> Result<Files, Error> {
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
let mut total_size = 0;
|
||||||
|
|
||||||
|
let junk: &[&OsStr] = &[OsStr::new("Thumbs.db"), OsStr::new("Desktop.ini")];
|
||||||
|
|
||||||
|
for result in WalkDir::new(&self.root).sort_by(|a, b| a.file_name().cmp(b.file_name())) {
|
||||||
|
let entry = result?;
|
||||||
|
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
let file_name = entry.file_name();
|
||||||
|
|
||||||
|
let metadata = entry.metadata()?;
|
||||||
|
|
||||||
|
if !metadata.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.include_hidden && file_name.to_string_lossy().starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.include_hidden && Platform::hidden(path)? {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.include_junk && junk.contains(&file_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
total_size += metadata.len();
|
||||||
|
paths.push(entry.path().to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Files::new(self.root, Bytes::from(total_size)))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user