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:
parent
687a863b45
commit
1cd6c276fd
|
@ -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
|
||||
|
|
|
@ -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
110
src/file_order.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user