Add ability to create single-file torrents from stdin

Torrents may now be created from standard input by passing `--input -`.

Since `--name` and `--output` cannot be deduced, they are required when
`--input -`.

type: added
This commit is contained in:
Eric Siegel 2020-04-02 20:04:12 -07:00 committed by Casey Rodarmor
parent 796024bec9
commit c23b0635ee
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
18 changed files with 322 additions and 149 deletions

View File

@ -2,7 +2,9 @@
set -euxo pipefail
cargo build
for script in completions/*; do
shell=${script##*.}
cargo run -- completions --shell $shell > $script
./target/debug/imdl completions --shell $shell > $script
done

View File

@ -70,10 +70,10 @@ Examples:
--node [2001:db8:4275:7920:6269:7463:6f69:6e21]:8832'
cand -g 'Include or exclude files that match `GLOB`. Multiple glob may be provided, with the last one taking precedence. Precede a glob with `!` to exclude it.'
cand --glob 'Include or exclude files that match `GLOB`. Multiple glob may be provided, with the last one taking precedence. Precede a glob with `!` to exclude it.'
cand -i 'Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent, if `PATH` is a directory, torrent will be a multi-file torrent.'
cand --input 'Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent, if `PATH` is a directory, torrent will be a multi-file torrent.'
cand -N 'Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`.'
cand --name 'Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`.'
cand -i 'Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent. If `PATH` is a directory, torrent will be a multi-file torrent. If `PATH` is `-`, read from standard input. Piece length defaults to 256KiB when reading from standard input if `--piece-length` is not given.'
cand --input 'Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent. If `PATH` is a directory, torrent will be a multi-file torrent. If `PATH` is `-`, read from standard input. Piece length defaults to 256KiB when reading from standard input if `--piece-length` is not given.'
cand -N 'Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`. Required when `--input -`.'
cand --name 'Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`. Required when `--input -`.'
cand --sort-by '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:
@ -87,8 +87,8 @@ Sort in ascending order by path, more concisely:
Sort in ascending order by size, break ties in descending path order:
--sort-by size:ascending --sort-by path:descending'
cand -o 'Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to `$INPUT.torrent`.'
cand --output 'Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to `$INPUT.torrent`.'
cand -o 'Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to the argument to `--input` with an `.torrent` extension appended. Required when `--input -`.'
cand --output 'Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to the argument to `--input` with an `.torrent` extension appended. Required when `--input -`.'
cand --peer 'Add `PEER` to magnet link.'
cand -p 'Set piece length to `BYTES`. Accepts SI units, e.g. kib, mib, and gib.'
cand --piece-length 'Set piece length to `BYTES`. Accepts SI units, e.g. kib, mib, and gib.'

View File

@ -34,8 +34,8 @@ Examples:
--node [2001:db8:4275:7920:6269:7463:6f69:6e21]:8832'
complete -c imdl -n "__fish_seen_subcommand_from create" -s g -l glob -d 'Include or exclude files that match `GLOB`. Multiple glob may be provided, with the last one taking precedence. Precede a glob with `!` to exclude it.'
complete -c imdl -n "__fish_seen_subcommand_from create" -s i -l input -d 'Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent, if `PATH` is a directory, torrent will be a multi-file torrent.'
complete -c imdl -n "__fish_seen_subcommand_from create" -s N -l name -d 'Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`.'
complete -c imdl -n "__fish_seen_subcommand_from create" -s i -l input -d 'Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent. If `PATH` is a directory, torrent will be a multi-file torrent. If `PATH` is `-`, read from standard input. Piece length defaults to 256KiB when reading from standard input if `--piece-length` is not given.'
complete -c imdl -n "__fish_seen_subcommand_from create" -s N -l name -d 'Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`. Required when `--input -`.'
complete -c imdl -n "__fish_seen_subcommand_from create" -l sort-by -d '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:
@ -49,7 +49,7 @@ Sort in ascending order by path, more concisely:
Sort in ascending order by size, break ties in descending path order:
--sort-by size:ascending --sort-by path:descending'
complete -c imdl -n "__fish_seen_subcommand_from create" -s o -l output -d 'Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to `$INPUT.torrent`.'
complete -c imdl -n "__fish_seen_subcommand_from create" -s o -l output -d 'Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to the argument to `--input` with an `.torrent` extension appended. Required when `--input -`.'
complete -c imdl -n "__fish_seen_subcommand_from create" -l peer -d 'Add `PEER` to magnet link.'
complete -c imdl -n "__fish_seen_subcommand_from create" -s p -l piece-length -d 'Set piece length to `BYTES`. Accepts SI units, e.g. kib, mib, and gib.'
complete -c imdl -n "__fish_seen_subcommand_from create" -s s -l source -d 'Set torrent source to `TEXT`. Stored under `source` key of info dictionary. This is useful for keeping statistics from being mis-reported when participating in swarms with the same contents, but with different trackers. When source is set to a unique value for torrents with the same contents, torrent clients will treat them as distinct torrents, and not share peers between them, and will correctly report download and upload statistics to multiple trackers.'

View File

@ -77,10 +77,10 @@ Examples:
--node [2001:db8:4275:7920:6269:7463:6f69:6e21]:8832')
[CompletionResult]::new('-g', 'g', [CompletionResultType]::ParameterName, 'Include or exclude files that match `GLOB`. Multiple glob may be provided, with the last one taking precedence. Precede a glob with `!` to exclude it.')
[CompletionResult]::new('--glob', 'glob', [CompletionResultType]::ParameterName, 'Include or exclude files that match `GLOB`. Multiple glob may be provided, with the last one taking precedence. Precede a glob with `!` to exclude it.')
[CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent, if `PATH` is a directory, torrent will be a multi-file torrent.')
[CompletionResult]::new('--input', 'input', [CompletionResultType]::ParameterName, 'Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent, if `PATH` is a directory, torrent will be a multi-file torrent.')
[CompletionResult]::new('-N', 'N', [CompletionResultType]::ParameterName, 'Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`.')
[CompletionResult]::new('--name', 'name', [CompletionResultType]::ParameterName, 'Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`.')
[CompletionResult]::new('-i', 'i', [CompletionResultType]::ParameterName, 'Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent. If `PATH` is a directory, torrent will be a multi-file torrent. If `PATH` is `-`, read from standard input. Piece length defaults to 256KiB when reading from standard input if `--piece-length` is not given.')
[CompletionResult]::new('--input', 'input', [CompletionResultType]::ParameterName, 'Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent. If `PATH` is a directory, torrent will be a multi-file torrent. If `PATH` is `-`, read from standard input. Piece length defaults to 256KiB when reading from standard input if `--piece-length` is not given.')
[CompletionResult]::new('-N', 'N', [CompletionResultType]::ParameterName, 'Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`. Required when `--input -`.')
[CompletionResult]::new('--name', 'name', [CompletionResultType]::ParameterName, 'Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`. Required when `--input -`.')
[CompletionResult]::new('--sort-by', 'sort-by', [CompletionResultType]::ParameterName, '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:
@ -94,8 +94,8 @@ Sort in ascending order by path, more concisely:
Sort in ascending order by size, break ties in descending path order:
--sort-by size:ascending --sort-by path:descending')
[CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to `$INPUT.torrent`.')
[CompletionResult]::new('--output', 'output', [CompletionResultType]::ParameterName, 'Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to `$INPUT.torrent`.')
[CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to the argument to `--input` with an `.torrent` extension appended. Required when `--input -`.')
[CompletionResult]::new('--output', 'output', [CompletionResultType]::ParameterName, 'Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to the argument to `--input` with an `.torrent` extension appended. Required when `--input -`.')
[CompletionResult]::new('--peer', 'peer', [CompletionResultType]::ParameterName, 'Add `PEER` to magnet link.')
[CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'Set piece length to `BYTES`. Accepts SI units, e.g. kib, mib, and gib.')
[CompletionResult]::new('--piece-length', 'piece-length', [CompletionResultType]::ParameterName, 'Set piece length to `BYTES`. Accepts SI units, e.g. kib, mib, and gib.')

View File

@ -79,10 +79,10 @@ Examples:
--node \[2001:db8:4275:7920:6269:7463:6f69:6e21\]:8832]' \
'*-g+[Include or exclude files that match `GLOB`. Multiple glob may be provided, with the last one taking precedence. Precede a glob with `!` to exclude it.]' \
'*--glob=[Include or exclude files that match `GLOB`. Multiple glob may be provided, with the last one taking precedence. Precede a glob with `!` to exclude it.]' \
'-i+[Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent, if `PATH` is a directory, torrent will be a multi-file torrent.]' \
'--input=[Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent, if `PATH` is a directory, torrent will be a multi-file torrent.]' \
'-N+[Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`.]' \
'--name=[Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`.]' \
'-i+[Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent. If `PATH` is a directory, torrent will be a multi-file torrent. If `PATH` is `-`, read from standard input. Piece length defaults to 256KiB when reading from standard input if `--piece-length` is not given.]' \
'--input=[Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file torrent. If `PATH` is a directory, torrent will be a multi-file torrent. If `PATH` is `-`, read from standard input. Piece length defaults to 256KiB when reading from standard input if `--piece-length` is not given.]' \
'-N+[Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`. Required when `--input -`.]' \
'--name=[Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`. Required when `--input -`.]' \
'*--sort-by=[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:
@ -96,8 +96,8 @@ Sort in ascending order by path, more concisely:
Sort in ascending order by size, break ties in descending path order:
--sort-by size:ascending --sort-by path:descending]' \
'-o+[Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to `$INPUT.torrent`.]' \
'--output=[Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to `$INPUT.torrent`.]' \
'-o+[Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to the argument to `--input` with an `.torrent` extension appended. Required when `--input -`.]' \
'--output=[Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. Defaults to the argument to `--input` with an `.torrent` extension appended. Required when `--input -`.]' \
'*--peer=[Add `PEER` to magnet link.]' \
'-p+[Set piece length to `BYTES`. Accepts SI units, e.g. kib, mib, and gib.]' \
'--piece-length=[Set piece length to `BYTES`. Accepts SI units, e.g. kib, mib, and gib.]' \

View File

@ -10,7 +10,7 @@ pub(crate) use std::{
fmt::{self, Display, Formatter},
fs::{self, File},
hash::Hash,
io::{self, Cursor, Read, Write},
io::{self, BufRead, BufReader, Cursor, Read, Write},
iter::{self, Sum},
num::{ParseFloatError, ParseIntError, TryFromIntError},
ops::{AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign},
@ -35,7 +35,10 @@ 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, StructOpt};
pub(crate) use structopt::{
clap::{self, AppSettings},
StructOpt,
};
pub(crate) use strum::VariantNames;
pub(crate) use strum_macros::{EnumString, EnumVariantNames, IntoStaticStr};
pub(crate) use unicode_width::UnicodeWidthStr;
@ -51,7 +54,7 @@ pub(crate) use crate::{consts, error, host_port_parse_error};
// traits
pub(crate) use crate::{
into_u64::IntoU64, into_usize::IntoUsize, path_ext::PathExt,
input_stream::InputStream, into_u64::IntoU64, into_usize::IntoUsize, path_ext::PathExt,
platform_interface::PlatformInterface, print::Print, reckoner::Reckoner, step::Step,
};

View File

@ -3,7 +3,7 @@ use crate::common::*;
pub(crate) struct Env {
args: Vec<OsString>,
dir: PathBuf,
input: Box<dyn Read>,
input: Box<dyn InputStream>,
err: OutputStream,
out: OutputStream,
}
@ -79,7 +79,7 @@ impl Env {
pub(crate) fn new<S, I>(
dir: PathBuf,
args: I,
input: Box<dyn Read>,
input: Box<dyn InputStream>,
out: OutputStream,
err: OutputStream,
) -> Self
@ -147,6 +147,10 @@ impl Env {
&self.err
}
pub(crate) fn input<'a>(&'a mut self) -> Box<dyn BufRead + 'a> {
self.input.as_mut().buf_read()
}
pub(crate) fn err_mut(&mut self) -> &mut OutputStream {
&mut self.err
}
@ -165,13 +169,17 @@ impl Env {
pub(crate) fn read(&mut self, source: InputTarget) -> Result<Input> {
let data = match &source {
InputTarget::File(path) => {
InputTarget::Path(path) => {
let absolute = self.resolve(path);
fs::read(absolute).context(error::Filesystem { path })?
}
InputTarget::Stdin => {
let mut buffer = Vec::new();
self.input.read_to_end(&mut buffer).context(error::Stdin)?;
self
.input
.buf_read()
.read_to_end(&mut buffer)
.context(error::Stdin)?;
buffer
}
};

View File

@ -1,7 +1,5 @@
use crate::common::*;
use structopt::clap;
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub(crate) enum Error {

View File

@ -12,16 +12,7 @@ pub(crate) struct Hasher {
}
impl Hasher {
pub(crate) fn hash(
files: &Files,
md5sum: bool,
piece_length: usize,
progress_bar: Option<ProgressBar>,
) -> Result<(Mode, PieceList), Error> {
Self::new(md5sum, piece_length, progress_bar).hash_files(files)
}
fn new(md5sum: bool, piece_length: usize, progress_bar: Option<ProgressBar>) -> Self {
pub(crate) fn new(md5sum: bool, piece_length: usize, progress_bar: Option<ProgressBar>) -> Self {
Self {
buffer: vec![0; piece_length],
length: 0,
@ -34,7 +25,7 @@ impl Hasher {
}
}
fn hash_files(mut self, files: &Files) -> Result<(Mode, PieceList), Error> {
pub(crate) fn hash_files(mut self, files: &Files) -> Result<(Mode, PieceList), Error> {
let mode = if let Some(contents) = files.contents() {
let files = self.hash_contents(&files.root(), contents)?;
@ -42,19 +33,30 @@ impl Hasher {
} else {
let (md5sum, length) = self.hash_file(files.root())?;
Mode::Single {
md5sum: md5sum.map(|md5sum| md5sum.into()),
length,
}
Mode::Single { md5sum, length }
};
self.finish();
Ok((mode, self.pieces))
}
pub(crate) fn hash_stdin(mut self, stdin: &mut dyn BufRead) -> Result<(Mode, PieceList), Error> {
let (md5sum, length) = self.hash_read_io(stdin).context(error::Stdin)?;
let mode = Mode::Single { md5sum, length };
self.finish();
Ok((mode, self.pieces))
}
fn finish(&mut self) {
if self.piece_bytes_hashed > 0 {
self.pieces.push(self.sha1.digest().into());
self.sha1.reset();
self.piece_bytes_hashed = 0;
}
Ok((mode, self.pieces))
}
fn hash_contents(
@ -70,8 +72,8 @@ impl Hasher {
let (md5sum, length) = self.hash_file(&path)?;
files.push(FileInfo {
md5sum: md5sum.map(|md5sum| md5sum.into()),
path: file_path.clone(),
md5sum,
length,
});
}
@ -79,18 +81,17 @@ impl Hasher {
Ok(files)
}
fn hash_file(&mut self, file: &Path) -> Result<(Option<md5::Digest>, Bytes), Error> {
fn hash_file(&mut self, path: &Path) -> Result<(Option<Md5Digest>, Bytes), Error> {
let file = File::open(path).context(error::Filesystem { path })?;
self
.hash_file_io(file)
.context(error::Filesystem { path: file })
.hash_read_io(&mut BufReader::new(file))
.context(error::Filesystem { path })
}
fn hash_file_io(&mut self, file: &Path) -> io::Result<(Option<md5::Digest>, Bytes)> {
let length = file.metadata()?.len();
let mut remaining = length;
let mut file = File::open(file)?;
fn hash_read_io(&mut self, file: &mut dyn BufRead) -> io::Result<(Option<Md5Digest>, Bytes)> {
let buffer_len = self.buffer.len();
let mut bytes_hashed = 0;
let mut md5 = if self.md5sum {
Some(md5::Context::new())
@ -98,17 +99,20 @@ impl Hasher {
None
};
while remaining > 0 {
let to_buffer: usize = remaining
.min(self.buffer.len().into_u64())
.try_into()
.unwrap();
loop {
let buffer = &mut self.buffer[..buffer_len];
let buffer = &mut self.buffer[0..to_buffer];
let bytes_read = file.read(buffer)?;
file.read_exact(buffer)?;
if bytes_read == 0 {
break;
}
for byte in buffer.iter().cloned() {
bytes_hashed += bytes_read;
let read = &buffer[0..bytes_read];
for byte in read.iter().cloned() {
self.sha1.update(&[byte]);
self.piece_bytes_hashed += 1;
@ -121,18 +125,19 @@ impl Hasher {
}
if let Some(md5) = md5.as_mut() {
md5.consume(&buffer);
md5.consume(read);
}
remaining -= buffer.len().into_u64();
if let Some(progress_bar) = &self.progress_bar {
progress_bar.inc(to_buffer.into_u64());
progress_bar.inc(bytes_read.into_u64());
}
}
self.length += length;
self.length += bytes_hashed.into_u64();
Ok((md5.map(md5::Context::compute), Bytes::from(length)))
Ok((
md5.map(|context| context.compute().into()),
Bytes::from(bytes_hashed.into_u64()),
))
}
}

View File

@ -22,7 +22,7 @@ impl Input {
pub(crate) fn from_path(path: &Path) -> Result<Input> {
let data = fs::read(path).context(error::Filesystem { path })?;
Ok(Input {
source: InputTarget::File(path.to_owned()),
source: InputTarget::Path(path.to_owned()),
data,
})
}

23
src/input_stream.rs Normal file
View File

@ -0,0 +1,23 @@
use crate::common::*;
pub(crate) trait InputStream {
fn buf_read<'a>(&'a mut self) -> Box<dyn BufRead + 'a>;
}
impl InputStream for io::Stdin {
fn buf_read<'a>(&'a mut self) -> Box<dyn BufRead + 'a> {
Box::new(self.lock())
}
}
impl InputStream for io::Empty {
fn buf_read<'a>(&'a mut self) -> Box<dyn BufRead + 'a> {
Box::new(BufReader::new(self))
}
}
impl InputStream for Cursor<Vec<u8>> {
fn buf_read<'a>(&'a mut self) -> Box<dyn BufRead + 'a> {
Box::new(BufReader::new(self))
}
}

View File

@ -2,16 +2,25 @@ use crate::common::*;
#[derive(PartialEq, Debug, Clone)]
pub(crate) enum InputTarget {
File(PathBuf),
Path(PathBuf),
Stdin,
}
impl InputTarget {
pub(crate) fn resolve(&self, env: &Env) -> Self {
match self {
Self::Path(path) => Self::Path(env.resolve(path)),
Self::Stdin => Self::Stdin,
}
}
}
impl From<&OsStr> for InputTarget {
fn from(text: &OsStr) -> Self {
if text == OsStr::new("-") {
Self::Stdin
} else {
Self::File(text.into())
Self::Path(text.into())
}
}
}
@ -20,16 +29,15 @@ impl Display for InputTarget {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Stdin => write!(f, "standard input"),
Self::File(path) => write!(f, "`{}`", path.display()),
Self::Path(path) => write!(f, "`{}`", path.display()),
}
}
}
#[cfg(test)]
impl<P: AsRef<Path>> PartialEq<P> for InputTarget {
fn eq(&self, other: &P) -> bool {
match self {
Self::File(path) => path == other.as_ref(),
Self::Path(path) => path == other.as_ref(),
Self::Stdin => Path::new("-") == other.as_ref(),
}
}
@ -43,7 +51,7 @@ mod tests {
fn file() {
assert_eq!(
InputTarget::from(OsStr::new("foo")),
InputTarget::File("foo".into())
InputTarget::Path("foo".into())
);
}
@ -55,7 +63,7 @@ mod tests {
#[test]
fn display_file() {
let path = PathBuf::from("./path");
let have = InputTarget::File(path).to_string();
let have = InputTarget::Path(path).to_string();
let want = "`./path`";
assert_eq!(have, want);
}

View File

@ -65,6 +65,7 @@ mod host_port_parse_error;
mod info;
mod infohash;
mod input;
mod input_stream;
mod input_target;
mod into_u64;
mod into_usize;

View File

@ -76,7 +76,7 @@ impl Metainfo {
#[cfg(test)]
pub(crate) fn from_bytes(bytes: &[u8]) -> Metainfo {
Self::deserialize(&InputTarget::File("<TEST>".into()), bytes).unwrap()
Self::deserialize(&InputTarget::Path("<TEST>".into()), bytes).unwrap()
}
#[cfg(test)]

View File

@ -117,10 +117,12 @@ Examples:
short = "i",
value_name = "PATH",
help = "Read torrent contents from `PATH`. If `PATH` is a file, torrent will be a single-file \
torrent, if `PATH` is a directory, torrent will be a multi-file torrent.",
torrent. If `PATH` is a directory, torrent will be a multi-file torrent. If `PATH` \
is `-`, read from standard input. Piece length defaults to 256KiB when reading from \
standard input if `--piece-length` is not given.",
parse(from_os_str)
)]
input: PathBuf,
input: InputTarget,
#[structopt(
long = "link",
help = "Print created torrent `magnet:` URL to standard output"
@ -137,7 +139,9 @@ Examples:
long = "name",
short = "N",
value_name = "TEXT",
help = "Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`."
help = "Set name of torrent to `TEXT`. Defaults to the filename of the argument to `--input`. \
Required when `--input -`.",
required_if("input", "-")
)]
name: Option<String>,
#[structopt(
@ -184,7 +188,9 @@ Sort in ascending order by size, break ties in descending path order:
short = "o",
value_name = "TARGET",
help = "Save `.torrent` file to `TARGET`, or print to standard output if `TARGET` is `-`. \
Defaults to `$INPUT.torrent`.",
Defaults to the argument to `--input` with an `.torrent` extension appended. Required \
when `--input -`.",
required_if("input", "-"),
parse(from_os_str)
)]
output: Option<OutputTarget>,
@ -234,7 +240,8 @@ Sort in ascending order by size, break ties in descending path order:
impl Create {
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
let input = env.resolve(&self.input);
let input = self.input.resolve(env);
let output = self.output.map(|output| output.resolve(env));
let mut linter = Linter::new();
linter.allow(self.allowed_lints.iter().cloned());
@ -268,18 +275,79 @@ impl Create {
None
};
let files = Walker::new(&input)
.include_junk(self.include_junk)
.include_hidden(self.include_hidden)
.follow_symlinks(self.follow_symlinks)
.sort_by(self.sort_by)
.globs(&self.globs)?
.spinner(spinner)
.files()?;
let files;
let piece_length;
let progress_bar;
let name;
let output = match &input {
InputTarget::Path(path) => {
let files_inner = Walker::new(&path)
.include_junk(self.include_junk)
.include_hidden(self.include_hidden)
.follow_symlinks(self.follow_symlinks)
.sort_by(self.sort_by)
.globs(&self.globs)?
.spinner(spinner)
.files()?;
let piece_length = self
.piece_length
.unwrap_or_else(|| PieceLengthPicker::from_content_size(files.total_size()));
piece_length = self
.piece_length
.unwrap_or_else(|| PieceLengthPicker::from_content_size(files_inner.total_size()));
let style = ProgressStyle::default_bar()
.template(
"{spinner:.green} ⟪{elapsed_precise}⟫ ⟦{bar:40.cyan}⟧ \
{binary_bytes}/{binary_total_bytes} {binary_bytes_per_sec}, {eta}",
)
.tick_chars(consts::TICK_CHARS)
.progress_chars(consts::PROGRESS_CHARS);
progress_bar = ProgressBar::new(files_inner.total_size().count()).with_style(style);
let filename = path
.file_name()
.ok_or_else(|| Error::FilenameExtract { path: path.clone() })?;
name = match &self.name {
Some(name) => name.clone(),
None => filename
.to_str()
.ok_or_else(|| Error::FilenameDecode {
filename: PathBuf::from(filename),
})?
.to_owned(),
};
files = Some(files_inner);
output
.as_ref()
.map(|output| output.resolve(env))
.unwrap_or_else(|| {
let mut torrent_name = name.to_owned();
torrent_name.push_str(".torrent");
OutputTarget::File(path.parent().unwrap().join(torrent_name))
})
}
InputTarget::Stdin => {
files = None;
piece_length = self.piece_length.unwrap_or(Bytes::kib() * 256);
let style = ProgressStyle::default_bar()
.template("{spinner:.green} ⟪{elapsed_precise}⟫ {binary_bytes} ⟨{binary_bytes_per_sec}⟩")
.tick_chars(consts::TICK_CHARS);
progress_bar = ProgressBar::new_spinner().with_style(style);
name = self
.name
.ok_or_else(|| Error::internal("Expected `--name` to be set when `--input -`."))?;
output.ok_or_else(|| Error::internal("Expected `--output` to be set when `--input -`."))?
}
};
if piece_length.count() == 0 {
return Err(Error::PieceLengthZero);
@ -295,31 +363,6 @@ impl Create {
return Err(Error::PieceLengthSmall);
}
let filename = input.file_name().ok_or_else(|| Error::FilenameExtract {
path: input.clone(),
})?;
let name = match &self.name {
Some(name) => name.clone(),
None => filename
.to_str()
.ok_or_else(|| Error::FilenameDecode {
filename: PathBuf::from(filename),
})?
.to_owned(),
};
let output = self
.output
.as_ref()
.map(|output| output.resolve(env))
.unwrap_or_else(|| {
let mut torrent_name = name.to_owned();
torrent_name.push_str(".torrent");
OutputTarget::File(input.parent().unwrap().join(torrent_name))
});
if let OutputTarget::File(path) = &output {
if !self.force && path.exists() {
return Err(Error::OutputExists {
@ -348,26 +391,21 @@ impl Create {
CreateStep::Hashing.print(env)?;
let progress_bar = if env.err().is_styled_term() {
let style = ProgressStyle::default_bar()
.template(
"{spinner:.green} ⟪{elapsed_precise}⟫ ⟦{bar:40.cyan}⟧ \
{binary_bytes}/{binary_total_bytes} {binary_bytes_per_sec}, {eta}",
)
.tick_chars(consts::TICK_CHARS)
.progress_chars(consts::PROGRESS_CHARS);
Some(ProgressBar::new(files.total_size().count()).with_style(style))
} else {
None
};
let (mode, pieces) = Hasher::hash(
&files,
let hasher = Hasher::new(
self.md5sum,
piece_length.as_piece_length()?.into_usize(),
progress_bar,
)?;
if env.err().is_styled_term() {
Some(progress_bar)
} else {
None
},
);
let (mode, pieces) = if let Some(files) = files {
hasher.hash_files(&files)?
} else {
hasher.hash_stdin(&mut env.input())?
};
CreateStep::Writing { output: &output }.print(env)?;
@ -423,14 +461,18 @@ impl Create {
#[cfg(test)]
{
let deserialized = bendy::serde::de::from_bytes::<Metainfo>(&bytes).unwrap();
if let InputTarget::Path(path) = &input {
let deserialized = bendy::serde::de::from_bytes::<Metainfo>(&bytes).unwrap();
assert_eq!(deserialized, metainfo);
assert_eq!(deserialized, metainfo);
let status = metainfo.verify(&input, None)?;
let status = metainfo.verify(path, None)?;
if !status.good() {
return Err(Error::Verify);
status.print(env)?;
if !status.good() {
return Err(Error::Verify);
}
}
}
@ -1349,7 +1391,7 @@ mod tests {
},
},
};
env.run().unwrap();
env.assert_ok();
let metainfo = env.load_metainfo("foo.torrent");
assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["abchijxyz"]));
match metainfo.info.mode {
@ -1917,7 +1959,7 @@ Content Size 9 bytes
},
}
};
env.run().unwrap();
env.assert_ok();
let metainfo = env.load_metainfo("foo.torrent");
assert_matches!(
metainfo.info.mode,
@ -2586,4 +2628,75 @@ Content Size 9 bytes
let torrent = env.load_metainfo("foo.torrent");
assert_eq!(torrent.file_paths(), &["d/e", "b", "a", "c"]);
}
#[test]
fn name_required_when_input_is_stdin() {
let mut env = test_env! {
args: [
"torrent",
"create",
"--input",
"-",
"--announce",
"http://bar",
"--output",
"foo.torrent",
],
tree: {},
};
assert!(matches!(env.run(), Err(Error::Clap { .. })));
}
#[test]
fn foo_required_when_input_is_stdin() {
let mut env = test_env! {
args: [
"torrent",
"create",
"--input",
"-",
"--announce",
"http://bar",
"--name",
"foo",
],
tree: {},
};
assert!(matches!(env.run(), Err(Error::Clap { .. })));
}
#[test]
fn create_from_stdin() {
let mut env = test_env! {
args: [
"torrent",
"create",
"--input",
"-",
"--announce",
"http://bar",
"--name",
"foo",
"--output",
"foo.torrent",
"--md5",
],
input: "hello",
tree: {},
};
env.assert_ok();
let metainfo = env.load_metainfo("foo.torrent");
assert_eq!(metainfo.info.pieces, PieceList::from_pieces(&["hello"]));
assert_eq!(
metainfo.info.mode,
Mode::Single {
length: Bytes(5),
md5sum: Some(Md5Digest::from_data("hello")),
}
);
}
}

View File

@ -45,7 +45,7 @@ impl Verify {
content.clone()
} else {
match &self.metainfo {
InputTarget::File(path) => path.parent().unwrap().join(&metainfo.info.name),
InputTarget::Path(path) => path.parent().unwrap().join(&metainfo.info.name),
InputTarget::Stdin => PathBuf::from(&metainfo.info.name),
}
};

View File

@ -77,6 +77,18 @@ impl TestEnv {
fs::set_permissions(self.env.resolve(path), permissions).unwrap();
}
pub(crate) fn assert_ok(&mut self) {
match self.run() {
Ok(()) => {}
Err(err) => {
eprintln!("Run failed: {}", err);
eprintln!("Std error:\n{}", self.err());
eprintln!("Std output:\n{}", self.out());
panic!();
}
}
}
pub(crate) fn load_metainfo(&mut self, filename: impl AsRef<Path>) -> Metainfo {
let input = self.env.read(filename.as_ref().as_os_str().into()).unwrap();
Metainfo::from_input(&input).unwrap()

View File

@ -4,7 +4,7 @@ pub(crate) struct TestEnvBuilder {
args: Vec<OsString>,
current_dir: Option<PathBuf>,
err_style: bool,
input: Option<Box<dyn Read>>,
input: Option<Box<dyn InputStream>>,
out_is_term: bool,
tempdir: Option<TempDir>,
use_color: bool,