Use open
crate to open files and URLs
Opening URLs on Windows is very complex, so delegate to the `open` crate. type: changed
This commit is contained in:
parent
35d90adab4
commit
6328118c00
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -330,6 +330,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"md5",
|
"md5",
|
||||||
|
"open",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"regex",
|
"regex",
|
||||||
|
@ -447,6 +448,15 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a"
|
checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "open"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c283bf0114efea9e42f1a60edea9859e8c47528eae09d01df4b29c1e489cc48"
|
||||||
|
dependencies = [
|
||||||
|
"winapi 0.3.8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "output_vt100"
|
name = "output_vt100"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|
|
@ -22,6 +22,7 @@ lazy_static = "1.4.0"
|
||||||
libc = "0.2.0"
|
libc = "0.2.0"
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
md5 = "0.7.0"
|
md5 = "0.7.0"
|
||||||
|
open = "1.4.0"
|
||||||
pretty_assertions = "0.6.0"
|
pretty_assertions = "0.6.0"
|
||||||
pretty_env_logger = "0.4.0"
|
pretty_env_logger = "0.4.0"
|
||||||
regex = "1.0.0"
|
regex = "1.0.0"
|
||||||
|
|
|
@ -15,7 +15,7 @@ pub(crate) use std::{
|
||||||
num::{ParseFloatError, ParseIntError, TryFromIntError},
|
num::{ParseFloatError, ParseIntError, TryFromIntError},
|
||||||
ops::{AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign},
|
ops::{AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign},
|
||||||
path::{self, Path, PathBuf},
|
path::{self, Path, PathBuf},
|
||||||
process::{self, Command, ExitStatus},
|
process::{self, ExitStatus},
|
||||||
str::{self, FromStr},
|
str::{self, FromStr},
|
||||||
sync::Once,
|
sync::Once,
|
||||||
time::{SystemTime, SystemTimeError},
|
time::{SystemTime, SystemTimeError},
|
||||||
|
@ -79,8 +79,8 @@ mod test {
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
io::Cursor,
|
io::Cursor,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
|
process::Command,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// test dependencies
|
// test dependencies
|
||||||
|
|
44
src/error.rs
44
src/error.rs
|
@ -7,23 +7,6 @@ use structopt::clap;
|
||||||
pub(crate) enum Error {
|
pub(crate) enum Error {
|
||||||
#[snafu(display("Failed to parse announce URL: {}", source))]
|
#[snafu(display("Failed to parse announce URL: {}", source))]
|
||||||
AnnounceUrlParse { source: url::ParseError },
|
AnnounceUrlParse { source: url::ParseError },
|
||||||
#[snafu(display("Failed to deserialize torrent metainfo from {}: {}", input, source))]
|
|
||||||
MetainfoDeserialize {
|
|
||||||
source: bendy::serde::Error,
|
|
||||||
input: InputTarget,
|
|
||||||
},
|
|
||||||
#[snafu(display("Failed to serialize torrent metainfo: {}", source))]
|
|
||||||
MetainfoSerialize { source: bendy::serde::Error },
|
|
||||||
#[snafu(display("Failed to decode torrent metainfo from {}: {}", input, error))]
|
|
||||||
MetainfoDecode {
|
|
||||||
input: InputTarget,
|
|
||||||
error: bendy::decoding::Error,
|
|
||||||
},
|
|
||||||
#[snafu(display("Metainfo from {} failed to validate: {}", input, source))]
|
|
||||||
MetainfoValidate {
|
|
||||||
input: InputTarget,
|
|
||||||
source: MetainfoError,
|
|
||||||
},
|
|
||||||
#[snafu(display("Failed to parse byte count `{}`: {}", text, source))]
|
#[snafu(display("Failed to parse byte count `{}`: {}", text, source))]
|
||||||
ByteParse {
|
ByteParse {
|
||||||
text: String,
|
text: String,
|
||||||
|
@ -45,8 +28,6 @@ pub(crate) enum Error {
|
||||||
Filesystem { source: io::Error, path: PathBuf },
|
Filesystem { source: io::Error, path: PathBuf },
|
||||||
#[snafu(display("Invalid glob: {}", source))]
|
#[snafu(display("Invalid glob: {}", source))]
|
||||||
GlobParse { source: globset::Error },
|
GlobParse { source: globset::Error },
|
||||||
#[snafu(display("Unknown lint: {}", text))]
|
|
||||||
LintUnknown { text: String },
|
|
||||||
#[snafu(display("Failed to serialize torrent info dictionary: {}", source))]
|
#[snafu(display("Failed to serialize torrent info dictionary: {}", source))]
|
||||||
InfoSerialize { source: bendy::serde::Error },
|
InfoSerialize { source: bendy::serde::Error },
|
||||||
#[snafu(display(
|
#[snafu(display(
|
||||||
|
@ -55,8 +36,29 @@ pub(crate) enum Error {
|
||||||
message,
|
message,
|
||||||
))]
|
))]
|
||||||
Internal { message: String },
|
Internal { message: String },
|
||||||
#[snafu(display("Failed to find opener utility, please install one of {}", tried.join(",")))]
|
#[snafu(display("Unknown lint: {}", text))]
|
||||||
OpenerMissing { tried: &'static [&'static str] },
|
LintUnknown { text: String },
|
||||||
|
#[snafu(display("Failed to deserialize torrent metainfo from {}: {}", input, source))]
|
||||||
|
MetainfoDeserialize {
|
||||||
|
source: bendy::serde::Error,
|
||||||
|
input: InputTarget,
|
||||||
|
},
|
||||||
|
#[snafu(display("Failed to serialize torrent metainfo: {}", source))]
|
||||||
|
MetainfoSerialize { source: bendy::serde::Error },
|
||||||
|
#[snafu(display("Failed to decode torrent metainfo from {}: {}", input, error))]
|
||||||
|
MetainfoDecode {
|
||||||
|
input: InputTarget,
|
||||||
|
error: bendy::decoding::Error,
|
||||||
|
},
|
||||||
|
#[snafu(display("Metainfo from {} failed to validate: {}", input, source))]
|
||||||
|
MetainfoValidate {
|
||||||
|
input: InputTarget,
|
||||||
|
source: MetainfoError,
|
||||||
|
},
|
||||||
|
#[snafu(display("Failed to invoke opener: {}", source))]
|
||||||
|
OpenerInvoke { source: io::Error },
|
||||||
|
#[snafu(display("Opener failed: {}", exit_status))]
|
||||||
|
OpenerExitStatus { exit_status: ExitStatus },
|
||||||
#[snafu(display("Output path already exists: `{}`", path.display()))]
|
#[snafu(display("Output path already exists: `{}`", path.display()))]
|
||||||
OutputExists { path: PathBuf },
|
OutputExists { path: PathBuf },
|
||||||
#[snafu(display(
|
#[snafu(display(
|
||||||
|
|
|
@ -4,15 +4,6 @@ pub(crate) struct Platform;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
impl PlatformInterface for Platform {
|
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"),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hidden(path: &Path) -> Result<bool, Error> {
|
fn hidden(path: &Path) -> Result<bool, Error> {
|
||||||
use std::os::windows::fs::MetadataExt;
|
use std::os::windows::fs::MetadataExt;
|
||||||
|
|
||||||
|
@ -25,10 +16,6 @@ impl PlatformInterface for Platform {
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
impl PlatformInterface for Platform {
|
impl PlatformInterface for Platform {
|
||||||
fn opener() -> Result<Vec<OsString>, Error> {
|
|
||||||
Ok(vec![OsString::from("open")])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hidden(path: &Path) -> Result<bool, Error> {
|
fn hidden(path: &Path) -> Result<bool, Error> {
|
||||||
use std::{ffi::CString, mem, os::unix::ffi::OsStrExt};
|
use std::{ffi::CString, mem, os::unix::ffi::OsStrExt};
|
||||||
|
|
||||||
|
@ -53,20 +40,6 @@ impl PlatformInterface for Platform {
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||||
impl PlatformInterface for Platform {
|
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 })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hidden(_path: &Path) -> Result<bool, Error> {
|
fn hidden(_path: &Path) -> Result<bool, Error> {
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,49 +2,22 @@ use crate::common::*;
|
||||||
|
|
||||||
pub(crate) trait PlatformInterface {
|
pub(crate) trait PlatformInterface {
|
||||||
fn open_file(path: &Path) -> Result<(), Error> {
|
fn open_file(path: &Path) -> Result<(), Error> {
|
||||||
Self::open_raw(path.as_os_str())
|
Self::open_target(path.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_url(url: &Url) -> Result<(), Error> {
|
fn open_url(url: &Url) -> Result<(), Error> {
|
||||||
if cfg!(windows) {
|
Self::open_target(url.as_str().as_ref())
|
||||||
let escaped = format!("\"{}\"", url);
|
|
||||||
Self::open_raw(escaped.as_str().as_ref())
|
|
||||||
} else {
|
|
||||||
Self::open_raw(url.as_str().as_ref())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_raw(target: &OsStr) -> Result<(), Error> {
|
fn open_target(target: &OsStr) -> Result<(), Error> {
|
||||||
let mut command = Self::opener()?;
|
let exit_status = open::that(target).context(error::OpenerInvoke)?;
|
||||||
command.push(OsString::from(target));
|
|
||||||
|
|
||||||
let command_string = || {
|
if !exit_status.success() {
|
||||||
command
|
return Err(Error::OpenerExitStatus { exit_status });
|
||||||
.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>;
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn hidden(path: &Path) -> Result<bool, Error>;
|
fn hidden(path: &Path) -> Result<bool, Error>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1352,69 +1352,6 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn open() {
|
|
||||||
let mut env = test_env! {
|
|
||||||
args: [
|
|
||||||
"torrent",
|
|
||||||
"create",
|
|
||||||
"--input",
|
|
||||||
"foo",
|
|
||||||
"--announce",
|
|
||||||
"http://bar",
|
|
||||||
"--open",
|
|
||||||
],
|
|
||||||
tree: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
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`.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn uneven_piece_length() {
|
fn uneven_piece_length() {
|
||||||
let mut env = test_env! {
|
let mut env = test_env! {
|
||||||
|
|
|
@ -239,81 +239,4 @@ mod tests {
|
||||||
if input == "foo.torrent"
|
if input == "foo.torrent"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn open() {
|
|
||||||
let mut create_env = test_env! {
|
|
||||||
args: [
|
|
||||||
"torrent",
|
|
||||||
"create",
|
|
||||||
"--input",
|
|
||||||
"foo",
|
|
||||||
],
|
|
||||||
tree: {
|
|
||||||
foo: "",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_matches!(create_env.run(), Ok(()));
|
|
||||||
|
|
||||||
let torrent = create_env.resolve("foo.torrent");
|
|
||||||
|
|
||||||
let mut env = test_env! {
|
|
||||||
args: [
|
|
||||||
"torrent",
|
|
||||||
"link",
|
|
||||||
"--input",
|
|
||||||
&torrent,
|
|
||||||
"--open",
|
|
||||||
],
|
|
||||||
tree: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
let opened = env.resolve("opened.txt");
|
|
||||||
|
|
||||||
let link = "magnet:?xt=urn:btih:516735f4b80f2b5487eed5f226075bdcde33a54e&dn=foo";
|
|
||||||
|
|
||||||
let expected = if cfg!(target_os = "windows") {
|
|
||||||
let script = env.resolve("open.bat");
|
|
||||||
fs::write(&script, format!("echo > {}", opened.display())).unwrap();
|
|
||||||
format!("ECHO is on.\r\n")
|
|
||||||
} 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", link)
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
assert_matches!(env.run(), Ok(()));
|
|
||||||
|
|
||||||
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