From 97018031c1abaaf12c1cdc8f645aa9417c1937c8 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 27 Mar 2020 19:00:40 -0700 Subject: [PATCH] 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 --- src/common.rs | 23 +++---- src/file_order.rs | 110 ------------------------------- src/main.rs | 4 +- src/sort_key.rs | 14 ++++ src/sort_order.rs | 20 ++++++ src/sort_spec.rs | 95 ++++++++++++++++++++++++++ src/subcommand/torrent/create.rs | 97 ++++++++++++++++++++------- src/walker.rs | 10 +-- 8 files changed, 220 insertions(+), 153 deletions(-) delete mode 100644 src/file_order.rs create mode 100644 src/sort_key.rs create mode 100644 src/sort_order.rs create mode 100644 src/sort_spec.rs diff --git a/src/common.rs b/src/common.rs index 9603741b..df39ec72 100644 --- a/src/common.rs +++ b/src/common.rs @@ -35,10 +35,7 @@ pub(crate) use serde_with::rust::unwrap_or_skip; pub(crate) use sha1::Sha1; pub(crate) use snafu::{ResultExt, Snafu}; pub(crate) use static_assertions::const_assert; -pub(crate) use structopt::{ - clap::{AppSettings, ArgSettings}, - StructOpt, -}; +pub(crate) use structopt::{clap::AppSettings, StructOpt}; pub(crate) use strum::VariantNames; pub(crate) use strum_macros::{EnumString, EnumVariantNames, IntoStaticStr}; pub(crate) use unicode_width::UnicodeWidthStr; @@ -61,15 +58,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_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, + 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, sort_key::SortKey, sort_order::SortOrder, sort_spec::SortSpec, + status::Status, style::Style, subcommand::Subcommand, table::Table, + torrent_summary::TorrentSummary, use_color::UseColor, verifier::Verifier, walker::Walker, }; // type aliases diff --git a/src/file_order.rs b/src/file_order.rs deleted file mode 100644 index 3b8c6836..00000000 --- a/src/file_order.rs +++ /dev/null @@ -1,110 +0,0 @@ -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 { - 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::(), - Err(Error::FileOrderUnknown { text }) if text == "foo" - ); - } -} diff --git a/src/main.rs b/src/main.rs index 448c43fd..7608da22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,7 +56,6 @@ mod env; mod error; mod file_error; mod file_info; -mod file_order; mod file_path; mod file_status; mod files; @@ -87,6 +86,9 @@ mod platform_interface; mod print; mod reckoner; mod sha1_digest; +mod sort_key; +mod sort_order; +mod sort_spec; mod status; mod step; mod style; diff --git a/src/sort_key.rs b/src/sort_key.rs new file mode 100644 index 00000000..d4b0da1c --- /dev/null +++ b/src/sort_key.rs @@ -0,0 +1,14 @@ +use crate::common::*; + +#[derive(Clone, Copy, Debug, PartialEq, IntoStaticStr, EnumString)] +#[strum(serialize_all = "kebab-case")] +pub(crate) enum SortKey { + Path, + Size, +} + +impl SortKey { + pub(crate) fn name(self) -> &'static str { + self.into() + } +} diff --git a/src/sort_order.rs b/src/sort_order.rs new file mode 100644 index 00000000..4dc537e9 --- /dev/null +++ b/src/sort_order.rs @@ -0,0 +1,20 @@ +use crate::common::*; + +#[derive(Clone, Copy, Debug, PartialEq, IntoStaticStr, EnumString)] +#[strum(serialize_all = "kebab-case")] +pub(crate) enum SortOrder { + Ascending, + Descending, +} + +impl SortOrder { + pub(crate) fn name(self) -> &'static str { + self.into() + } +} + +impl Default for SortOrder { + fn default() -> Self { + Self::Ascending + } +} diff --git a/src/sort_spec.rs b/src/sort_spec.rs new file mode 100644 index 00000000..b627f73f --- /dev/null +++ b/src/sort_spec.rs @@ -0,0 +1,95 @@ +use crate::common::*; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct SortSpec { + key: SortKey, + order: SortOrder, +} + +impl SortSpec { + pub(crate) fn compare(specs: &[SortSpec], a: &FileInfo, b: &FileInfo) -> Ordering { + let mut specs = specs.to_vec(); + + specs.push(SortSpec::default()); + + Self::compare_specs(&specs, a, b) + } + + fn compare_specs(specs: &[SortSpec], a: &FileInfo, b: &FileInfo) -> Ordering { + specs.iter().fold(Ordering::Equal, |ordering, spec| { + ordering.then_with(|| spec.compare_file_info(a, b)) + }) + } + + fn compare_file_info(self, a: &FileInfo, b: &FileInfo) -> Ordering { + let ordering = match self.key { + SortKey::Path => a.path.cmp(&b.path), + SortKey::Size => a.length.cmp(&b.length), + }; + + match self.order { + SortOrder::Ascending => ordering, + SortOrder::Descending => ordering.reverse(), + } + } +} + +impl Default for SortSpec { + fn default() -> Self { + Self { + key: SortKey::Path, + order: SortOrder::default(), + } + } +} + +impl FromStr for SortSpec { + type Err = strum::ParseError; + + fn from_str(text: &str) -> Result { + if let Some(index) = text.find(':') { + Ok(SortSpec { + key: text[..index].parse()?, + order: text[index + 1..].parse()?, + }) + } else { + Ok(SortSpec { + key: text.parse()?, + order: SortOrder::default(), + }) + } + } +} + +impl Display for SortSpec { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}:{}", self.key.name(), self.order.name()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default() { + assert_eq!( + SortSpec::default(), + SortSpec { + key: SortKey::Path, + order: SortOrder::Ascending + } + ); + } + + #[test] + fn parse() { + assert_eq!( + SortSpec { + key: SortKey::Path, + order: SortOrder::Ascending + }, + "path:ascending".parse().unwrap() + ); + } +} diff --git a/src/subcommand/torrent/create.rs b/src/subcommand/torrent/create.rs index 3f76c701..5122d807 100644 --- a/src/subcommand/torrent/create.rs +++ b/src/subcommand/torrent/create.rs @@ -61,10 +61,15 @@ pub(crate) struct Create { value_name = "NODE", help = "Add DHT bootstrap node `NODE` to torrent. `NODE` should be in the form `HOST:PORT`, \ where `HOST` is a domain name, an IPv4 address, or an IPv6 address surrounded by \ - brackets. May be given more than once to add multiple bootstrap nodes. Examples: - `--node router.example.com:1337` - `--node 203.0.113.0:2290` - `--node [2001:db8:4275:7920:6269:7463:6f69:6e21]:8832`" + brackets. May be given more than once to add multiple bootstrap nodes. + +Examples: + + --node router.example.com:1337 + + --node 203.0.113.0:2290 + + --node [2001:db8:4275:7920:6269:7463:6f69:6e21]:8832" )] dht_nodes: Vec, #[structopt( @@ -153,14 +158,27 @@ pub(crate) struct Create { )] 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." + long = "sort-by", + value_name = "SPEC", + help = "Set the order of files within a torrent. `SPEC` should be of the form `KEY:ORDER`, \ + with `KEY` being one of `path` or `size`, and `ORDER` being `ascending` or \ + `descending`. `:ORDER` defaults to `ascending` if omitted. The `--sort-by` flag may \ + be given more than once, with later values being used to break ties. Ties that remain \ + are broken in ascending path order. + +Sort in ascending order by path, the default: + + --sort-by path:ascending + +Sort in ascending order by path, more concisely: + + --sort-by path + +Sort in ascending order by size, break ties in descending path order: + + --sort-by size:ascending --sort-by path:descending" )] - order: Option, + sort_by: Vec, #[structopt( long = "output", short = "o", @@ -254,7 +272,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)) + .sort_by(self.sort_by) .globs(&self.globs)? .spinner(spinner) .files()?; @@ -2423,15 +2441,15 @@ Content Size 9 bytes } #[test] - fn file_ordering_by_alpha_asc() { + fn file_ordering_by_path_ascending() { let mut env = test_env! { args: [ "torrent", "create", "--input", "foo", - "--order", - "alphabetical-asc", + "--sort-by", + "path", ], tree: { foo: { @@ -2452,15 +2470,15 @@ Content Size 9 bytes } #[test] - fn file_ordering_by_alpha_desc() { + fn file_ordering_by_path_descending() { let mut env = test_env! { args: [ "torrent", "create", "--input", "foo", - "--order", - "alphabetical-desc", + "--sort-by", + "path:descending", ], tree: { foo: { @@ -2481,15 +2499,15 @@ Content Size 9 bytes } #[test] - fn file_ordering_by_size_asc() { + fn file_ordering_by_size_ascending() { let mut env = test_env! { args: [ "torrent", "create", "--input", "foo", - "--order", - "size-asc", + "--sort-by", + "size:ascending", ], tree: { foo: { @@ -2510,15 +2528,15 @@ Content Size 9 bytes } #[test] - fn file_ordering_by_size_desc() { + fn file_ordering_by_size_descending() { let mut env = test_env! { args: [ "torrent", "create", "--input", "foo", - "--order", - "size-desc", + "--sort-by", + "size:descending", ], tree: { foo: { @@ -2537,4 +2555,35 @@ Content Size 9 bytes let torrent = env.load_metainfo("foo.torrent"); assert_eq!(torrent.file_paths(), &["c", "a", "b", "d/e"]); } + + #[test] + fn file_ordering_by_size_ascending_break_ties_path_descending() { + let mut env = test_env! { + args: [ + "torrent", + "create", + "--input", + "foo", + "--sort-by", + "size:ascending", + "--sort-by", + "path:descending", + ], + 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(), &["d/e", "b", "a", "c"]); + } } diff --git a/src/walker.rs b/src/walker.rs index 43220da8..b6d5c6d9 100644 --- a/src/walker.rs +++ b/src/walker.rs @@ -12,7 +12,7 @@ pub(crate) struct Walker { follow_symlinks: bool, include_hidden: bool, include_junk: bool, - file_order: FileOrder, + sort_by: Vec, patterns: Vec, root: PathBuf, spinner: Option, @@ -24,7 +24,7 @@ impl Walker { follow_symlinks: false, include_hidden: false, include_junk: false, - file_order: FileOrder::AlphabeticalAsc, + sort_by: Vec::new(), patterns: Vec::new(), root: root.to_owned(), spinner: None, @@ -45,8 +45,8 @@ impl Walker { } } - pub(crate) fn file_order(self, file_order: FileOrder) -> Self { - Self { file_order, ..self } + pub(crate) fn sort_by(self, sort_by: Vec) -> Self { + Self { sort_by, ..self } } pub(crate) fn globs(mut self, globs: &[String]) -> Result { @@ -169,7 +169,7 @@ impl Walker { }); } - file_infos.sort_by(|a, b| self.file_order.compare_file_info(a, b)); + file_infos.sort_by(|a, b| SortSpec::compare(&self.sort_by, a, b)); Ok(Files::dir( self.root,