Allow sorting files in torrents

The order in which files appear in torrents can now be controlled
with the `--order` flag:

    imdl torrent create --input foo --order alphabetical-asc

See `--help` documentation for possible values.

type: added
This commit is contained in:
Eric 2020-03-25 18:08:55 -07:00 committed by Casey Rodarmor
parent 687a863b45
commit 1cd6c276fd
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
7 changed files with 312 additions and 14 deletions

View File

@ -2,7 +2,7 @@
pub(crate) use std::{
borrow::Cow,
char,
cmp::Reverse,
cmp::{Ordering, Reverse},
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
convert::{Infallible, TryInto},
env,
@ -59,14 +59,15 @@ pub(crate) use crate::{
// structs and enums
pub(crate) use crate::{
arguments::Arguments, bytes::Bytes, env::Env, error::Error, file_error::FileError,
file_info::FileInfo, file_path::FilePath, file_status::FileStatus, files::Files, hasher::Hasher,
host_port::HostPort, host_port_parse_error::HostPortParseError, info::Info, infohash::Infohash,
input::Input, input_target::InputTarget, lint::Lint, linter::Linter, magnet_link::MagnetLink,
md5_digest::Md5Digest, metainfo::Metainfo, metainfo_error::MetainfoError, mode::Mode,
options::Options, output_stream::OutputStream, output_target::OutputTarget,
piece_length_picker::PieceLengthPicker, piece_list::PieceList, platform::Platform,
sha1_digest::Sha1Digest, status::Status, style::Style, subcommand::Subcommand, table::Table,
torrent_summary::TorrentSummary, use_color::UseColor, verifier::Verifier, walker::Walker,
file_info::FileInfo, file_order::FileOrder, file_path::FilePath, file_status::FileStatus,
files::Files, hasher::Hasher, host_port::HostPort, host_port_parse_error::HostPortParseError,
info::Info, infohash::Infohash, input::Input, input_target::InputTarget, lint::Lint,
linter::Linter, magnet_link::MagnetLink, md5_digest::Md5Digest, metainfo::Metainfo,
metainfo_error::MetainfoError, mode::Mode, options::Options, output_stream::OutputStream,
output_target::OutputTarget, piece_length_picker::PieceLengthPicker, piece_list::PieceList,
platform::Platform, sha1_digest::Sha1Digest, status::Status, style::Style,
subcommand::Subcommand, table::Table, torrent_summary::TorrentSummary, use_color::UseColor,
verifier::Verifier, walker::Walker,
};
// type aliases

View File

@ -24,6 +24,8 @@ pub(crate) enum Error {
FilenameDecode { filename: PathBuf },
#[snafu(display("Path had no file name: {}", path.display()))]
FilenameExtract { path: PathBuf },
#[snafu(display("Unknown file ordering: {}", text))]
FileOrderUnknown { text: String },
#[snafu(display("I/O error at `{}`: {}", path.display(), source))]
Filesystem { source: io::Error, path: PathBuf },
#[snafu(display("Invalid glob: {}", source))]

110
src/file_order.rs Normal file
View File

@ -0,0 +1,110 @@
use crate::common::*;
#[derive(Eq, PartialEq, Debug, Copy, Clone, Ord, PartialOrd)]
pub(crate) enum FileOrder {
AlphabeticalDesc,
AlphabeticalAsc,
SizeDesc,
SizeAsc,
}
impl FileOrder {
pub(crate) const ALPHABETICAL_ASC: &'static str = "alphabetical-asc";
pub(crate) const ALPHABETICAL_DESC: &'static str = "alphabetical-desc";
pub(crate) const SIZE_ASC: &'static str = "size-asc";
pub(crate) const SIZE_DESC: &'static str = "size-desc";
pub(crate) const VALUES: &'static [&'static str] = &[
Self::ALPHABETICAL_DESC,
Self::ALPHABETICAL_ASC,
Self::SIZE_DESC,
Self::SIZE_ASC,
];
pub(crate) fn name(self) -> &'static str {
match self {
Self::AlphabeticalDesc => Self::ALPHABETICAL_DESC,
Self::AlphabeticalAsc => Self::ALPHABETICAL_ASC,
Self::SizeDesc => Self::SIZE_DESC,
Self::SizeAsc => Self::SIZE_ASC,
}
}
pub(crate) fn compare_file_info(self, a: &FileInfo, b: &FileInfo) -> Ordering {
match self {
Self::AlphabeticalAsc => a.path.cmp(&b.path),
Self::AlphabeticalDesc => a.path.cmp(&b.path).reverse(),
Self::SizeAsc => a.length.cmp(&b.length).then_with(|| a.path.cmp(&b.path)),
Self::SizeDesc => a
.length
.cmp(&b.length)
.reverse()
.then_with(|| a.path.cmp(&b.path)),
}
}
}
impl FromStr for FileOrder {
type Err = Error;
fn from_str(text: &str) -> Result<Self, Self::Err> {
match text.replace('_', "-").to_lowercase().as_str() {
Self::ALPHABETICAL_DESC => Ok(Self::AlphabeticalDesc),
Self::ALPHABETICAL_ASC => Ok(Self::AlphabeticalAsc),
Self::SIZE_DESC => Ok(Self::SizeDesc),
Self::SIZE_ASC => Ok(Self::SizeAsc),
_ => Err(Error::FileOrderUnknown {
text: text.to_string(),
}),
}
}
}
impl Display for FileOrder {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_str_ok() {
assert_eq!(
FileOrder::AlphabeticalDesc,
"alphabetical_desc".parse().unwrap()
);
assert_eq!(
FileOrder::AlphabeticalDesc,
"alphabetical-desc".parse().unwrap()
);
assert_eq!(
FileOrder::AlphabeticalDesc,
"ALPHABETICAL-desc".parse().unwrap()
);
}
#[test]
fn convert() {
fn case(text: &str, value: FileOrder) {
assert_eq!(value, text.parse().unwrap());
assert_eq!(value.name(), text);
}
case("alphabetical-desc", FileOrder::AlphabeticalDesc);
case("alphabetical-asc", FileOrder::AlphabeticalAsc);
case("size-desc", FileOrder::SizeDesc);
case("size-asc", FileOrder::SizeAsc);
}
#[test]
fn from_str_err() {
assert_matches!(
"foo".parse::<FileOrder>(),
Err(Error::FileOrderUnknown { text }) if text == "foo"
);
}
}

View File

@ -56,6 +56,7 @@ mod env;
mod error;
mod file_error;
mod file_info;
mod file_order;
mod file_path;
mod file_status;
mod files;

View File

@ -79,6 +79,18 @@ impl Metainfo {
Self::deserialize(&InputTarget::File("<TEST>".into()), bytes).unwrap()
}
#[cfg(test)]
pub(crate) fn file_paths(&self) -> Vec<String> {
let files = match &self.info.mode {
Mode::Single { .. } => panic!(),
Mode::Multiple { files } => files,
};
let paths: Vec<String> = files.iter().map(|f| f.path.to_string()).collect();
paths
}
pub(crate) fn verify(&self, base: &Path, progress_bar: Option<ProgressBar>) -> Result<Status> {
Verifier::verify(self, base, progress_bar)
}

View File

@ -153,6 +153,15 @@ pub(crate) struct Create {
Linux; `open` on macOS; and `cmd /C start` on Windows"
)]
open: bool,
#[structopt(
long = "order",
value_name = "ORDER",
possible_values = FileOrder::VALUES,
set(ArgSettings::CaseInsensitive),
help = "Specify the file order within the torrent. \
Defaults to ascending alphabetical order."
)]
order: Option<FileOrder>,
#[structopt(
long = "output",
short = "o",
@ -246,6 +255,7 @@ impl Create {
.include_junk(self.include_junk)
.include_hidden(self.include_hidden)
.follow_symlinks(self.follow_symlinks)
.file_order(self.order.unwrap_or(FileOrder::AlphabeticalAsc))
.globs(&self.globs)?
.spinner(spinner)
.files()?;
@ -2385,4 +2395,147 @@ Content Size 9 bytes
let err = fs::read(torrent).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn file_ordering_by_default() {
let mut env = test_env! {
args: [
"torrent",
"create",
"--input",
"foo",
],
tree: {
foo: {
a: "aa",
b: "b",
c: "ccc",
d: {
e: "eeee",
},
},
}
};
assert_matches!(env.run(), Ok(()));
let torrent = env.load_metainfo("foo.torrent");
assert_eq!(torrent.file_paths(), &["a", "b", "c", "d/e"]);
}
#[test]
fn file_ordering_by_alpha_asc() {
let mut env = test_env! {
args: [
"torrent",
"create",
"--input",
"foo",
"--order",
"alphabetical-asc",
],
tree: {
foo: {
a: "aa",
b: "b",
c: "ccc",
d: {
e: "eeee",
},
},
}
};
assert_matches!(env.run(), Ok(()));
let torrent = env.load_metainfo("foo.torrent");
assert_eq!(torrent.file_paths(), &["a", "b", "c", "d/e"]);
}
#[test]
fn file_ordering_by_alpha_desc() {
let mut env = test_env! {
args: [
"torrent",
"create",
"--input",
"foo",
"--order",
"alphabetical-desc",
],
tree: {
foo: {
a: "aa",
b: "b",
c: "ccc",
d: {
a: "aaaa",
},
},
}
};
assert_matches!(env.run(), Ok(()));
let torrent = env.load_metainfo("foo.torrent");
assert_eq!(torrent.file_paths(), &["d/a", "c", "b", "a"]);
}
#[test]
fn file_ordering_by_size_asc() {
let mut env = test_env! {
args: [
"torrent",
"create",
"--input",
"foo",
"--order",
"size-asc",
],
tree: {
foo: {
a: "aa",
b: "b",
c: "ccc",
d: {
e: "e",
},
},
}
};
assert_matches!(env.run(), Ok(()));
let torrent = env.load_metainfo("foo.torrent");
assert_eq!(torrent.file_paths(), &["b", "d/e", "a", "c"]);
}
#[test]
fn file_ordering_by_size_desc() {
let mut env = test_env! {
args: [
"torrent",
"create",
"--input",
"foo",
"--order",
"size-desc",
],
tree: {
foo: {
a: "aa",
b: "b",
c: "ccc",
d: {
e: "e",
},
},
}
};
assert_matches!(env.run(), Ok(()));
let torrent = env.load_metainfo("foo.torrent");
assert_eq!(torrent.file_paths(), &["c", "a", "b", "d/e"]);
}
}

View File

@ -12,6 +12,7 @@ pub(crate) struct Walker {
follow_symlinks: bool,
include_hidden: bool,
include_junk: bool,
file_order: FileOrder,
patterns: Vec<Pattern>,
root: PathBuf,
spinner: Option<ProgressBar>,
@ -23,6 +24,7 @@ impl Walker {
follow_symlinks: false,
include_hidden: false,
include_junk: false,
file_order: FileOrder::AlphabeticalAsc,
patterns: Vec::new(),
root: root.to_owned(),
spinner: None,
@ -43,6 +45,10 @@ impl Walker {
}
}
pub(crate) fn file_order(self, file_order: FileOrder) -> Self {
Self { file_order, ..self }
}
pub(crate) fn globs(mut self, globs: &[String]) -> Result<Self, Error> {
for glob in globs {
let exclude = glob.starts_with('!');
@ -112,11 +118,10 @@ impl Walker {
true
};
let mut paths = Vec::new();
let mut file_infos = Vec::new();
let mut total_size = 0;
for result in WalkDir::new(&self.root)
.follow_links(self.follow_symlinks)
.sort_by(|a, b| a.file_name().cmp(b.file_name()))
.into_iter()
.filter_entry(filter)
{
@ -154,12 +159,26 @@ impl Walker {
continue;
}
total_size += metadata.len();
let len = metadata.len();
total_size += len;
paths.push(file_path);
file_infos.push(FileInfo {
path: file_path,
length: Bytes(len),
md5sum: None,
});
}
Ok(Files::dir(self.root, Bytes::from(total_size), paths))
file_infos.sort_by(|a, b| self.file_order.compare_file_info(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 {