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,
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
process::{self, Command, ExitStatus},
|
||||
str::{self, FromStr},
|
||||
time::{SystemTime, SystemTimeError},
|
||||
usize,
|
||||
|
@ -36,13 +36,14 @@ pub(crate) use crate::{bencode, consts, error, torrent, use_color};
|
|||
|
||||
// traits
|
||||
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
|
||||
pub(crate) use crate::{
|
||||
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,
|
||||
};
|
||||
|
||||
|
@ -58,6 +59,7 @@ pub(crate) use std::{
|
|||
iter,
|
||||
ops::{Deref, DerefMut},
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
// 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 {
|
||||
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)]
|
||||
#[snafu(visibility(pub(crate)))]
|
||||
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))]
|
||||
Clap { source: clap::Error },
|
||||
#[snafu(display("I/O error at `{}`: {}", path.display(), source))]
|
||||
Filesystem { source: io::Error, path: PathBuf },
|
||||
#[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("Serialization failed: {}", source))]
|
||||
Serialize { source: serde_bencode::Error },
|
||||
#[snafu(display("Failed to invoke command `{}`: {}", command, source,))]
|
||||
CommandInvoke { command: String, source: io::Error },
|
||||
#[snafu(display("Command `{}` returned bad exit status: {}", command, status))]
|
||||
CommandStatus { command: String, status: ExitStatus },
|
||||
#[snafu(display("Filename was not valid unicode: {}", filename.to_string_lossy()))]
|
||||
FilenameDecode { filename: OsString },
|
||||
#[snafu(display("Path had no file name: {}", path.display()))]
|
||||
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))]
|
||||
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(
|
||||
"Feature `{}` cannot be used without passing the `--unstable` flag",
|
||||
feature
|
||||
|
|
|
@ -53,6 +53,8 @@ mod metainfo;
|
|||
mod mode;
|
||||
mod opt;
|
||||
mod path_ext;
|
||||
mod platform;
|
||||
mod platform_interface;
|
||||
mod reckoner;
|
||||
mod style;
|
||||
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."
|
||||
)]
|
||||
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(
|
||||
name = "OUTPUT",
|
||||
long = "output",
|
||||
|
@ -178,6 +185,10 @@ impl Create {
|
|||
|
||||
fs::write(&output, bytes).context(error::Filesystem { path: &output })?;
|
||||
|
||||
if self.open {
|
||||
Platform::open(&output)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -630,4 +641,56 @@ mod tests {
|
|||
);
|
||||
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