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::{
|
pub(crate) use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
char,
|
char,
|
||||||
cmp::Reverse,
|
cmp::{Ordering, Reverse},
|
||||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||||
convert::{Infallible, TryInto},
|
convert::{Infallible, TryInto},
|
||||||
env,
|
env,
|
||||||
|
@ -59,14 +59,15 @@ pub(crate) use crate::{
|
||||||
// structs and enums
|
// structs and enums
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
arguments::Arguments, bytes::Bytes, env::Env, error::Error, file_error::FileError,
|
arguments::Arguments, bytes::Bytes, env::Env, error::Error, file_error::FileError,
|
||||||
file_info::FileInfo, file_path::FilePath, file_status::FileStatus, files::Files, hasher::Hasher,
|
file_info::FileInfo, file_order::FileOrder, file_path::FilePath, file_status::FileStatus,
|
||||||
host_port::HostPort, host_port_parse_error::HostPortParseError, info::Info, infohash::Infohash,
|
files::Files, hasher::Hasher, host_port::HostPort, host_port_parse_error::HostPortParseError,
|
||||||
input::Input, input_target::InputTarget, lint::Lint, linter::Linter, magnet_link::MagnetLink,
|
info::Info, infohash::Infohash, input::Input, input_target::InputTarget, lint::Lint,
|
||||||
md5_digest::Md5Digest, metainfo::Metainfo, metainfo_error::MetainfoError, mode::Mode,
|
linter::Linter, magnet_link::MagnetLink, md5_digest::Md5Digest, metainfo::Metainfo,
|
||||||
options::Options, output_stream::OutputStream, output_target::OutputTarget,
|
metainfo_error::MetainfoError, mode::Mode, options::Options, output_stream::OutputStream,
|
||||||
piece_length_picker::PieceLengthPicker, piece_list::PieceList, platform::Platform,
|
output_target::OutputTarget, piece_length_picker::PieceLengthPicker, piece_list::PieceList,
|
||||||
sha1_digest::Sha1Digest, status::Status, style::Style, subcommand::Subcommand, table::Table,
|
platform::Platform, sha1_digest::Sha1Digest, status::Status, style::Style,
|
||||||
torrent_summary::TorrentSummary, use_color::UseColor, verifier::Verifier, walker::Walker,
|
subcommand::Subcommand, table::Table, torrent_summary::TorrentSummary, use_color::UseColor,
|
||||||
|
verifier::Verifier, walker::Walker,
|
||||||
};
|
};
|
||||||
|
|
||||||
// type aliases
|
// type aliases
|
||||||
|
|
|
@ -24,6 +24,8 @@ pub(crate) enum Error {
|
||||||
FilenameDecode { filename: PathBuf },
|
FilenameDecode { filename: PathBuf },
|
||||||
#[snafu(display("Path had no file name: {}", path.display()))]
|
#[snafu(display("Path had no file name: {}", path.display()))]
|
||||||
FilenameExtract { path: PathBuf },
|
FilenameExtract { path: PathBuf },
|
||||||
|
#[snafu(display("Unknown file ordering: {}", text))]
|
||||||
|
FileOrderUnknown { text: String },
|
||||||
#[snafu(display("I/O error at `{}`: {}", path.display(), source))]
|
#[snafu(display("I/O error at `{}`: {}", path.display(), source))]
|
||||||
Filesystem { source: io::Error, path: PathBuf },
|
Filesystem { source: io::Error, path: PathBuf },
|
||||||
#[snafu(display("Invalid glob: {}", source))]
|
#[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 error;
|
||||||
mod file_error;
|
mod file_error;
|
||||||
mod file_info;
|
mod file_info;
|
||||||
|
mod file_order;
|
||||||
mod file_path;
|
mod file_path;
|
||||||
mod file_status;
|
mod file_status;
|
||||||
mod files;
|
mod files;
|
||||||
|
|
|
@ -79,6 +79,18 @@ impl Metainfo {
|
||||||
Self::deserialize(&InputTarget::File("<TEST>".into()), bytes).unwrap()
|
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> {
|
pub(crate) fn verify(&self, base: &Path, progress_bar: Option<ProgressBar>) -> Result<Status> {
|
||||||
Verifier::verify(self, base, progress_bar)
|
Verifier::verify(self, base, progress_bar)
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,6 +153,15 @@ pub(crate) struct Create {
|
||||||
Linux; `open` on macOS; and `cmd /C start` on Windows"
|
Linux; `open` on macOS; and `cmd /C start` on Windows"
|
||||||
)]
|
)]
|
||||||
open: bool,
|
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(
|
#[structopt(
|
||||||
long = "output",
|
long = "output",
|
||||||
short = "o",
|
short = "o",
|
||||||
|
@ -246,6 +255,7 @@ impl Create {
|
||||||
.include_junk(self.include_junk)
|
.include_junk(self.include_junk)
|
||||||
.include_hidden(self.include_hidden)
|
.include_hidden(self.include_hidden)
|
||||||
.follow_symlinks(self.follow_symlinks)
|
.follow_symlinks(self.follow_symlinks)
|
||||||
|
.file_order(self.order.unwrap_or(FileOrder::AlphabeticalAsc))
|
||||||
.globs(&self.globs)?
|
.globs(&self.globs)?
|
||||||
.spinner(spinner)
|
.spinner(spinner)
|
||||||
.files()?;
|
.files()?;
|
||||||
|
@ -2385,4 +2395,147 @@ Content Size 9 bytes
|
||||||
let err = fs::read(torrent).unwrap_err();
|
let err = fs::read(torrent).unwrap_err();
|
||||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
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,
|
follow_symlinks: bool,
|
||||||
include_hidden: bool,
|
include_hidden: bool,
|
||||||
include_junk: bool,
|
include_junk: bool,
|
||||||
|
file_order: FileOrder,
|
||||||
patterns: Vec<Pattern>,
|
patterns: Vec<Pattern>,
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
spinner: Option<ProgressBar>,
|
spinner: Option<ProgressBar>,
|
||||||
|
@ -23,6 +24,7 @@ impl Walker {
|
||||||
follow_symlinks: false,
|
follow_symlinks: false,
|
||||||
include_hidden: false,
|
include_hidden: false,
|
||||||
include_junk: false,
|
include_junk: false,
|
||||||
|
file_order: FileOrder::AlphabeticalAsc,
|
||||||
patterns: Vec::new(),
|
patterns: Vec::new(),
|
||||||
root: root.to_owned(),
|
root: root.to_owned(),
|
||||||
spinner: None,
|
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> {
|
pub(crate) fn globs(mut self, globs: &[String]) -> Result<Self, Error> {
|
||||||
for glob in globs {
|
for glob in globs {
|
||||||
let exclude = glob.starts_with('!');
|
let exclude = glob.starts_with('!');
|
||||||
|
@ -112,11 +118,10 @@ impl Walker {
|
||||||
true
|
true
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut paths = Vec::new();
|
let mut file_infos = Vec::new();
|
||||||
let mut total_size = 0;
|
let mut total_size = 0;
|
||||||
for result in WalkDir::new(&self.root)
|
for result in WalkDir::new(&self.root)
|
||||||
.follow_links(self.follow_symlinks)
|
.follow_links(self.follow_symlinks)
|
||||||
.sort_by(|a, b| a.file_name().cmp(b.file_name()))
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_entry(filter)
|
.filter_entry(filter)
|
||||||
{
|
{
|
||||||
|
@ -154,12 +159,26 @@ impl Walker {
|
||||||
continue;
|
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 {
|
fn pattern_filter(&self, relative: &Path) -> bool {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user