Open torrents with imdl create --open ...
Invokes an OS-dependent opener to open the `.torrent` file after creation. type: added
This commit is contained in:
parent
495316e821
commit
e8ab0e1c4f
|
@ -11,7 +11,7 @@ pub(crate) use std::{
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process,
|
process::{self, Command, ExitStatus},
|
||||||
str::{self, FromStr},
|
str::{self, FromStr},
|
||||||
time::{SystemTime, SystemTimeError},
|
time::{SystemTime, SystemTimeError},
|
||||||
usize,
|
usize,
|
||||||
|
@ -36,13 +36,14 @@ pub(crate) use crate::{bencode, consts, error, torrent, use_color};
|
||||||
|
|
||||||
// traits
|
// traits
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
into_u64::IntoU64, into_usize::IntoUsize, path_ext::PathExt, reckoner::Reckoner,
|
into_u64::IntoU64, into_usize::IntoUsize, path_ext::PathExt,
|
||||||
|
platform_interface::PlatformInterface, reckoner::Reckoner,
|
||||||
};
|
};
|
||||||
|
|
||||||
// structs and enums
|
// structs and enums
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info, metainfo::Metainfo,
|
env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info, metainfo::Metainfo,
|
||||||
mode::Mode, opt::Opt, style::Style, subcommand::Subcommand, torrent::Torrent,
|
mode::Mode, opt::Opt, platform::Platform, style::Style, subcommand::Subcommand, torrent::Torrent,
|
||||||
use_color::UseColor,
|
use_color::UseColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -58,6 +59,7 @@ pub(crate) use std::{
|
||||||
iter,
|
iter,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
// test structs and enums
|
// test structs and enums
|
||||||
|
|
|
@ -98,8 +98,12 @@ impl Env {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dir(&self) -> &Path {
|
||||||
|
self.dir.as_ref().as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve(&self, path: impl AsRef<Path>) -> PathBuf {
|
pub(crate) fn resolve(&self, path: impl AsRef<Path>) -> PathBuf {
|
||||||
self.dir.as_ref().as_ref().join(path).clean()
|
self.dir().join(path).clean()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
34
src/error.rs
34
src/error.rs
|
@ -5,28 +5,34 @@ use structopt::clap;
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
#[snafu(visibility(pub(crate)))]
|
#[snafu(visibility(pub(crate)))]
|
||||||
pub(crate) enum Error {
|
pub(crate) enum Error {
|
||||||
|
#[snafu(display("Must provide at least one announce URL"))]
|
||||||
|
AnnounceEmpty,
|
||||||
|
#[snafu(display("Failed to parse announce URL: {}", source))]
|
||||||
|
AnnounceUrlParse { source: url::ParseError },
|
||||||
|
#[snafu(display("Failed to decode bencode: {}", source))]
|
||||||
|
BencodeDecode { source: serde_bencode::Error },
|
||||||
#[snafu(display("{}", source))]
|
#[snafu(display("{}", source))]
|
||||||
Clap { source: clap::Error },
|
Clap { source: clap::Error },
|
||||||
#[snafu(display("I/O error at `{}`: {}", path.display(), source))]
|
#[snafu(display("Failed to invoke command `{}`: {}", command, source,))]
|
||||||
Filesystem { source: io::Error, path: PathBuf },
|
CommandInvoke { command: String, source: io::Error },
|
||||||
#[snafu(display("Failed to write to standard error: {}", source))]
|
#[snafu(display("Command `{}` returned bad exit status: {}", command, status))]
|
||||||
Stderr { source: io::Error },
|
CommandStatus { command: String, status: ExitStatus },
|
||||||
#[snafu(display("Failed to write to standard output: {}", source))]
|
|
||||||
Stdout { source: io::Error },
|
|
||||||
#[snafu(display("Serialization failed: {}", source))]
|
|
||||||
Serialize { source: serde_bencode::Error },
|
|
||||||
#[snafu(display("Filename was not valid unicode: {}", filename.to_string_lossy()))]
|
#[snafu(display("Filename was not valid unicode: {}", filename.to_string_lossy()))]
|
||||||
FilenameDecode { filename: OsString },
|
FilenameDecode { filename: OsString },
|
||||||
#[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("I/O error at `{}`: {}", path.display(), source))]
|
||||||
|
Filesystem { source: io::Error, path: PathBuf },
|
||||||
|
#[snafu(display("Failed to find opener utility, please install one of {}", tried.join(",")))]
|
||||||
|
OpenerMissing { tried: &'static [&'static str] },
|
||||||
|
#[snafu(display("Serialization failed: {}", source))]
|
||||||
|
Serialize { source: serde_bencode::Error },
|
||||||
|
#[snafu(display("Failed to write to standard error: {}", source))]
|
||||||
|
Stderr { source: io::Error },
|
||||||
|
#[snafu(display("Failed to write to standard output: {}", source))]
|
||||||
|
Stdout { source: io::Error },
|
||||||
#[snafu(display("Failed to retrieve system time: {}", source))]
|
#[snafu(display("Failed to retrieve system time: {}", source))]
|
||||||
SystemTime { source: SystemTimeError },
|
SystemTime { source: SystemTimeError },
|
||||||
#[snafu(display("Failed to parse announce URL: {}", source))]
|
|
||||||
AnnounceUrlParse { source: url::ParseError },
|
|
||||||
#[snafu(display("Must provide at least one announce URL"))]
|
|
||||||
AnnounceEmpty,
|
|
||||||
#[snafu(display("Failed to decode bencode: {}", source))]
|
|
||||||
BencodeDecode { source: serde_bencode::Error },
|
|
||||||
#[snafu(display(
|
#[snafu(display(
|
||||||
"Feature `{}` cannot be used without passing the `--unstable` flag",
|
"Feature `{}` cannot be used without passing the `--unstable` flag",
|
||||||
feature
|
feature
|
||||||
|
|
|
@ -53,6 +53,8 @@ mod metainfo;
|
||||||
mod mode;
|
mod mode;
|
||||||
mod opt;
|
mod opt;
|
||||||
mod path_ext;
|
mod path_ext;
|
||||||
|
mod platform;
|
||||||
|
mod platform_interface;
|
||||||
mod reckoner;
|
mod reckoner;
|
||||||
mod style;
|
mod style;
|
||||||
mod subcommand;
|
mod subcommand;
|
||||||
|
|
39
src/platform.rs
Normal file
39
src/platform.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
pub(crate) struct Platform;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
impl PlatformInterface for Platform {
|
||||||
|
fn opener() -> Result<Vec<OsString>, Error> {
|
||||||
|
let exe = if cfg!(test) { "open.bat" } else { "cmd" };
|
||||||
|
Ok(vec![
|
||||||
|
OsString::from(exe),
|
||||||
|
OsString::from("/C"),
|
||||||
|
OsString::from("start"),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
impl PlatformInterface for Platform {
|
||||||
|
fn opener() -> Result<Vec<OsString>, Error> {
|
||||||
|
Ok(vec![OsString::from("open")])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||||
|
impl PlatformInterface for Platform {
|
||||||
|
fn opener() -> Result<Vec<OsString>, Error> {
|
||||||
|
const OPENERS: &[&str] = &["xdg-open", "gnome-open", "kde-open"];
|
||||||
|
|
||||||
|
for opener in OPENERS {
|
||||||
|
if let Ok(output) = Command::new(opener).arg("--version").output() {
|
||||||
|
if output.status.success() {
|
||||||
|
return Ok(vec![OsString::from(opener)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Error::OpenerMissing { tried: OPENERS })
|
||||||
|
}
|
||||||
|
}
|
35
src/platform_interface.rs
Normal file
35
src/platform_interface.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
pub(crate) trait PlatformInterface {
|
||||||
|
fn open(path: &Path) -> Result<(), Error> {
|
||||||
|
let mut command = Self::opener()?;
|
||||||
|
command.push(OsString::from(path));
|
||||||
|
|
||||||
|
let command_string = || {
|
||||||
|
command
|
||||||
|
.iter()
|
||||||
|
.map(|arg| arg.to_string_lossy().into_owned())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(",")
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = Command::new(&command[0])
|
||||||
|
.args(&command[1..])
|
||||||
|
.status()
|
||||||
|
.map_err(|source| Error::CommandInvoke {
|
||||||
|
source,
|
||||||
|
command: command_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::CommandStatus {
|
||||||
|
command: command_string(),
|
||||||
|
status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opener() -> Result<Vec<OsString>, Error>;
|
||||||
|
}
|
|
@ -68,6 +68,13 @@ Note: Many BitTorrent clients do not implement the behavior described in BEP 12.
|
||||||
help = "Do not populate `creation date` key of generated torrent with current time."
|
help = "Do not populate `creation date` key of generated torrent with current time."
|
||||||
)]
|
)]
|
||||||
no_creation_date: bool,
|
no_creation_date: bool,
|
||||||
|
#[structopt(
|
||||||
|
name = "OPEN",
|
||||||
|
long = "open",
|
||||||
|
help = "Open `.torrent` file after creation",
|
||||||
|
long_help = "Open `.torrent` file after creation. Uses `xdg-open`, `gnome-open`, or `kde-open` on Linux; `open` on macOS; and `cmd /C start on Windows"
|
||||||
|
)]
|
||||||
|
open: bool,
|
||||||
#[structopt(
|
#[structopt(
|
||||||
name = "OUTPUT",
|
name = "OUTPUT",
|
||||||
long = "output",
|
long = "output",
|
||||||
|
@ -178,6 +185,10 @@ impl Create {
|
||||||
|
|
||||||
fs::write(&output, bytes).context(error::Filesystem { path: &output })?;
|
fs::write(&output, bytes).context(error::Filesystem { path: &output })?;
|
||||||
|
|
||||||
|
if self.open {
|
||||||
|
Platform::open(&output)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -630,4 +641,56 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() })
|
assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open() {
|
||||||
|
let mut env = environment(&["--input", "foo", "--announce", "http://bar", "--open"]);
|
||||||
|
|
||||||
|
let opened = env.resolve("opened.txt");
|
||||||
|
let torrent = env.resolve("foo.torrent");
|
||||||
|
|
||||||
|
let expected = if cfg!(target_os = "windows") {
|
||||||
|
let script = env.resolve("open.bat");
|
||||||
|
fs::write(&script, format!("echo %3 > {}", opened.display())).unwrap();
|
||||||
|
format!("{} \r\n", torrent.display())
|
||||||
|
} else {
|
||||||
|
let script = env.resolve(&Platform::opener().unwrap()[0]);
|
||||||
|
fs::write(
|
||||||
|
&script,
|
||||||
|
format!("#!/usr/bin/env sh\necho $1 > {}", opened.display()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Command::new("chmod")
|
||||||
|
.arg("+x")
|
||||||
|
.arg(&script)
|
||||||
|
.status()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
format!("{}\n", torrent.display())
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEY: &str = "PATH";
|
||||||
|
let path = env::var_os(KEY).unwrap();
|
||||||
|
let mut split = env::split_paths(&path)
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<PathBuf>>();
|
||||||
|
split.insert(0, env.dir().to_owned());
|
||||||
|
let new = env::join_paths(split).unwrap();
|
||||||
|
env::set_var(KEY, new);
|
||||||
|
|
||||||
|
fs::write(env.resolve("foo"), "").unwrap();
|
||||||
|
env.run().unwrap();
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
while start.elapsed() < Duration::new(2, 0) {
|
||||||
|
if let Ok(text) = fs::read_to_string(&opened) {
|
||||||
|
assert_eq!(text, expected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("Failed to read `opened.txt`.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user