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:
parent
796024bec9
commit
c23b0635ee
|
@ -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
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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.]' \
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
16
src/env.rs
16
src/env.rs
|
@ -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
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use crate::common::*;
|
||||
|
||||
use structopt::clap;
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
#[snafu(visibility(pub(crate)))]
|
||||
pub(crate) enum Error {
|
||||
|
|
|
@ -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()),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
23
src/input_stream.rs
Normal 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))
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,7 +275,13 @@ impl Create {
|
|||
None
|
||||
};
|
||||
|
||||
let files = Walker::new(&input)
|
||||
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)
|
||||
|
@ -277,9 +290,64 @@ impl Create {
|
|||
.spinner(spinner)
|
||||
.files()?;
|
||||
|
||||
let piece_length = self
|
||||
piece_length = self
|
||||
.piece_length
|
||||
.unwrap_or_else(|| PieceLengthPicker::from_content_size(files.total_size()));
|
||||
.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,16 +461,20 @@ impl Create {
|
|||
|
||||
#[cfg(test)]
|
||||
{
|
||||
if let InputTarget::Path(path) = &input {
|
||||
let deserialized = bendy::serde::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
||||
|
||||
assert_eq!(deserialized, metainfo);
|
||||
|
||||
let status = metainfo.verify(&input, None)?;
|
||||
let status = metainfo.verify(path, None)?;
|
||||
|
||||
status.print(env)?;
|
||||
|
||||
if !status.good() {
|
||||
return Err(Error::Verify);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errln!(env, "\u{2728}\u{2728} Done! \u{2728}\u{2728}")?;
|
||||
|
||||
|
@ -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")),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue
Block a user