Skip to content

Commit

Permalink
Open torrents with imdl create --open ...
Browse files Browse the repository at this point in the history
Invokes an OS-dependent opener to open the `.torrent` file after
creation.

type: added
  • Loading branch information
casey committed Apr 8, 2020
1 parent 495316e commit e8ab0e1
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 16 deletions.
8 changes: 5 additions & 3 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
};

Expand All @@ -58,6 +59,7 @@ pub(crate) use std::{
iter,
ops::{Deref, DerefMut},
rc::Rc,
time::{Duration, Instant},
};

// test structs and enums
Expand Down
6 changes: 5 additions & 1 deletion src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand Down
30 changes: 18 additions & 12 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("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("Serialization failed: {}", source))]
Serialize { source: serde_bencode::Error },
#[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("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
Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ mod metainfo;
mod mode;
mod opt;
mod path_ext;
mod platform;
mod platform_interface;
mod reckoner;
mod style;
mod subcommand;
Expand Down
39 changes: 39 additions & 0 deletions src/platform.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 35 additions & 0 deletions src/platform_interface.rs
Original file line number Diff line number Diff line change
@@ -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>;
}
63 changes: 63 additions & 0 deletions src/torrent/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -178,6 +185,10 @@ impl Create {

fs::write(&output, bytes).context(error::Filesystem { path: &output })?;

if self.open {
Platform::open(&output)?;
}

Ok(())
}
}
Expand Down Expand Up @@ -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`.");
}
}

0 comments on commit e8ab0e1

Please sign in to comment.