Merge branch 'shorten-urls' into 'master'

Implement URL shortener support

Closes #68

See merge request timvisee/ffsend!20
This commit is contained in:
Tim Visée 2019-03-14 23:57:17 +00:00
commit 866579c80b
12 changed files with 242 additions and 25 deletions

11
Cargo.lock generated
View file

@ -550,6 +550,7 @@ dependencies = [
"tar 0.4.22 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"urlshortener 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"version-compare 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1973,6 +1974,15 @@ dependencies = [
"url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "urlshortener"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "utf8-ranges"
version = "1.0.2"
@ -2318,6 +2328,7 @@ dependencies = [
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
"checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
"checksum url_serde 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "74e7d099f1ee52f823d4bdd60c93c3602043c728f5db3b97bdb548467f7bddea"
"checksum urlshortener 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0f16270e97b5ed9c2e8affcbc8a2e694860fda13a43c3e551d0d5eb29618a308"
"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
"checksum uuid 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0238db0c5b605dd1cf51de0f21766f97fba2645897024461d6a00c036819a768"
"checksum vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d"

View file

@ -50,7 +50,7 @@ name = "ffsend"
path = "src/main.rs"
[features]
default = ["archive", "clipboard", "history", "send2", "send3", "qrcode"]
default = ["archive", "clipboard", "history", "send2", "send3", "qrcode", "urlshorten"]
# Compile with file archiving support
archive = ["tar"]
@ -70,6 +70,9 @@ send3 = ["ffsend-api/send3"]
# Support for generating QR codes for share URLs
qrcode = ["qr2term"]
# Support for shortening share URLs
urlshorten = ["urlshortener"]
[dependencies]
chbs = "0.0.8"
chrono = "0.4"
@ -93,6 +96,7 @@ tar = { version = "0.4", optional = true }
tempfile = "3"
toml = "0.4"
version-compare = "0.0.6"
urlshortener = { version = "0.9", default-features = false, optional = true }
[target.'cfg(not(target_os = "linux"))'.dependencies]
clipboard = { version = "0.5", optional = true }

View file

@ -394,6 +394,8 @@ The following features are available, some of which are enabled by default:
| `clipboard` | Default | Support for copying links to the clipboard |
| `history` | Default | Support for tracking files in history |
| `archive` | Default | Support for archiving and extracting uploads and downloads |
| `qrcode` | Default | Support for rendering a QR code for a share URL |
| `urlshorten`| Default | Support for shortening share URLs |
| `no-color` | | Compile without color support in error and help messages |
To enable features during building or installation, specify them with

View file

@ -19,13 +19,14 @@ use tempfile::{Builder as TempBuilder, NamedTempFile};
use super::select_api_version;
#[cfg(feature = "archive")]
use crate::archive::archive::Archive;
use crate::client::create_transfer_client;
use crate::client::{create_client, create_transfer_client};
use crate::cmd::matcher::{download::DownloadMatcher, main::MainMatcher, Matcher};
#[cfg(feature = "history")]
use crate::history_tool;
use crate::progress::ProgressBar;
use crate::util::{
ensure_enough_space, ensure_password, prompt_yes, quit, quit_error, quit_error_msg, ErrorHints,
ensure_enough_space, ensure_password, follow_url, print_error, prompt_yes, quit, quit_error,
quit_error_msg, ErrorHints,
};
/// A file download action.
@ -46,12 +47,21 @@ impl<'a> Download<'a> {
let matcher_main = MainMatcher::with(self.cmd_matches).unwrap();
let matcher_download = DownloadMatcher::with(self.cmd_matches).unwrap();
// Get the share URL, derive the host
let url = matcher_download.url();
let host = matcher_download.guess_host();
// Create a regular client
let client = create_client(&matcher_main);
// Create a reqwest client capable for downloading files
let client = create_transfer_client(&matcher_main);
// Get the share URL, attempt to follow it
let url = matcher_download.url();
let url = match follow_url(&client, &url) {
Ok(url) => url,
Err(err) => {
print_error(err.context("failed to follow share URL, ignoring").compat());
url
}
};
// Guess the host
let host = matcher_download.guess_host(Some(url.clone()));
// Determine the API version to use
let mut desired_version = matcher_main.api();
@ -154,6 +164,9 @@ impl<'a> Download<'a> {
let progress_bar = Arc::new(Mutex::new(ProgressBar::new_download()));
let progress_reader: Arc<Mutex<ProgressReporter>> = progress_bar;
// Create a transfer client
let transfer_client = create_transfer_client(&matcher_main);
// Execute an download action
let progress = if !matcher_main.quiet() {
Some(progress_reader)
@ -161,7 +174,7 @@ impl<'a> Download<'a> {
None
};
ApiDownload::new(api_version, &file, target, password, false, Some(metadata))
.invoke(&client, progress)?;
.invoke(&transfer_client, progress)?;
// Extract the downloaded file if working with an archive
#[cfg(feature = "archive")]

View file

@ -20,11 +20,13 @@ use tempfile::{Builder as TempBuilder, NamedTempFile};
use super::select_api_version;
#[cfg(feature = "archive")]
use crate::archive::archiver::Archiver;
use crate::client::create_transfer_client;
use crate::client::{create_client, create_transfer_client};
use crate::cmd::matcher::{MainMatcher, Matcher, UploadMatcher};
#[cfg(feature = "history")]
use crate::history_tool;
use crate::progress::ProgressBar;
#[cfg(feature = "urlshorten")]
use crate::urlshorten;
#[cfg(feature = "clipboard")]
use crate::util::set_clipboard;
use crate::util::{
@ -56,7 +58,7 @@ impl<'a> Upload<'a> {
let host = matcher_upload.host();
// Create a reqwest client capable for uploading files
let client = create_transfer_client(&matcher_main);
let client = create_client(&matcher_main);
// Determine the API version to use
let mut desired_version = matcher_main.api();
@ -228,23 +230,58 @@ impl<'a> Upload<'a> {
params,
)
.invoke(&transfer_client, reporter)?;
let url = file.download_url(true);
#[allow(unused_mut)]
let mut url = file.download_url(true);
// Shorten the share URL if requested, prompt the user to confirm
#[cfg(feature = "urlshorten")]
{
if matcher_upload.shorten() {
if prompt_yes("URL shortening is a security risk. This shares the secret URL with a 3rd party.\nDo you want to shorten the share URL?", Some(false), &matcher_main) {
match urlshorten::shorten_url(&client, &url) {
Ok(short) => url = short,
Err(err) => print_error(
err.context("failed to shorten share URL, ignoring")
.compat(),
),
}
}
}
}
// Report the result
if !matcher_main.quiet() {
// Show a table
// Create a table
let mut table = Table::new();
table.set_format(FormatBuilder::new().padding(0, 2).build());
// Show the original URL when shortening, verbose and different
#[cfg(feature = "urlshorten")]
{
let full_url = file.download_url(true);
if matcher_main.verbose() && matcher_upload.shorten() && url != full_url {
table.add_row(Row::new(vec![
Cell::new("Full share link:"),
Cell::new(full_url.as_str()),
]));
}
}
// Show the share URL
table.add_row(Row::new(vec![
Cell::new("Share link:"),
Cell::new(url.as_str()),
]));
// Show a generate passphrase
if password_generated {
table.add_row(Row::new(vec![
Cell::new("Passphrase:"),
Cell::new(&password.unwrap_or("?".into())),
]));
}
// Show the owner token
if matcher_main.verbose() {
table.add_row(Row::new(vec![
Cell::new("Owner token:"),
@ -284,10 +321,7 @@ impl<'a> Upload<'a> {
{
if matcher_upload.qrcode() {
if let Err(err) = print_qr(url.as_str()) {
print_error(
err.context("failed to print QR code, ignoring")
.compat(),
);
print_error(err.context("failed to print QR code, ignoring").compat());
}
}
}

View file

@ -1,8 +1,4 @@
use chbs::{
config::BasicConfig,
prelude::*,
word::WordList,
};
use chbs::{config::BasicConfig, prelude::*, word::WordList};
use clap::Arg;
use super::{CmdArg, CmdArgFlag};

View file

@ -26,8 +26,8 @@ impl<'a: 'b, 'b> DownloadMatcher<'a> {
/// Guess the file share host, based on the file share URL.
///
/// See `Self::url`.
pub fn guess_host(&'a self) -> Url {
let mut url = self.url();
pub fn guess_host(&'a self, url: Option<Url>) -> Url {
let mut url = url.unwrap_or(self.url());
url.set_path("");
url.set_query(None);
url.set_fragment(None);

View file

@ -110,6 +110,12 @@ impl<'a: 'b, 'b> UploadMatcher<'a> {
}
}
/// Check whether to shorten a share URL
#[cfg(feature = "urlshorten")]
pub fn shorten(&self) -> bool {
self.matches.is_present("shorten")
}
/// Check whether to print a QR code for the share URL.
#[cfg(feature = "qrcode")]
pub fn qrcode(&self) -> bool {

View file

@ -73,6 +73,19 @@ impl CmdUpload {
);
}
// Optional url shortening support
#[cfg(feature = "urlshorten")]
{
cmd = cmd.arg(
Arg::with_name("shorten")
.long("shorten")
.alias("short")
.alias("url-shorten")
.short("S")
.help("Shorten share URLs with a public service"),
)
}
// Optional qrcode support
#[cfg(feature = "qrcode")]
{

View file

@ -23,6 +23,8 @@ mod history;
mod history_tool;
mod host;
mod progress;
#[cfg(feature = "urlshorten")]
mod urlshorten;
mod util;
use crate::action::debug::Debug;

97
src/urlshorten.rs Normal file
View file

@ -0,0 +1,97 @@
//! URL shortening mechanics.
use ffsend_api::{
api::request::{ensure_success, ResponseError},
reqwest::{self, Client},
url::{self, Url},
};
use urlshortener::{
providers::{self, Provider},
request::{Method, Request},
};
/// An URL shortening result.
type Result<T> = ::std::result::Result<T, Error>;
/// Shorten the given URL.
pub fn shorten(client: &Client, url: &str) -> Result<String> {
// TODO: allow selecting other shorteners
request(client, providers::request(url, &Provider::IsGd))
}
/// Shorten the given URL.
pub fn shorten_url(client: &Client, url: &Url) -> Result<Url> {
Url::parse(&shorten(client, url.as_str())?).map_err(|err| err.into())
}
/// Do the request as given, return the response.
fn request(client: &Client, req: Request) -> Result<String> {
// Start the request builder
let mut builder = match req.method {
Method::Get => client.get(&req.url),
Method::Post => client.post(&req.url),
};
// Define the custom user agent
if let Some(_agent) = req.user_agent.clone() {
// TODO: implement this
// builder.header(header::UserAgent::new(agent.0));
panic!("Custom UserAgent for URL shortener not yet implemented");
}
// Define the custom content type
if let Some(_content_type) = req.content_type {
// TODO: implement this
// match content_type {
// ContentType::Json => builder.header(header::ContentType::json()),
// ContentType::FormUrlEncoded => {
// builder.header(header::ContentType::form_url_encoded())
// }
// };
panic!("Custom UserAgent for URL shortener not yet implemented");
}
// Define the custom body
if let Some(body) = req.body.clone() {
builder = builder.body(body);
}
// Send the request, ensure success
let mut response = builder.send().map_err(Error::Request)?;
ensure_success(&response)?;
// Respond with the body text
response.text().map_err(Error::Malformed)
}
/// An URL shortening error.
#[derive(Debug, Fail)]
pub enum Error {
/// Failed to send the shortening request.
#[fail(display = "failed to send URL shorten request")]
Request(#[cause] reqwest::Error),
/// The server responded with a bad response.
#[fail(display = "failed to shorten URL, got bad response")]
Response(#[cause] ResponseError),
/// The server resonded with a malformed repsonse.
#[fail(display = "failed to shorten URL, got malformed response")]
Malformed(#[cause] reqwest::Error),
/// An error occurred while parsing the shortened URL.
#[fail(display = "failed to shorten URL, could not parse URL")]
Url(#[cause] url::ParseError),
}
impl From<url::ParseError> for Error {
fn from(err: url::ParseError) -> Self {
Error::Url(err)
}
}
impl From<ResponseError> for Error {
fn from(err: ResponseError) -> Self {
Error::Response(err)
}
}

View file

@ -29,7 +29,11 @@ use chrono::Duration;
use failure::{err_msg, Fail};
#[cfg(all(feature = "clipboard", not(target_os = "linux")))]
use failure::{Compat, Error};
use ffsend_api::url::Url;
use ffsend_api::{
api::request::{ensure_success, ResponseError},
reqwest::{self, Client},
url::Url,
};
use rpassword::prompt_password_stderr;
use crate::cmd::matcher::MainMatcher;
@ -857,3 +861,38 @@ pub fn api_version_list() -> Vec<&'static str> {
versions
}
/// Follow redirects on the given URL, and return the final full URL.
///
/// This is used to obtain share URLs from shortened links.
///
// TODO: extract this into module
pub fn follow_url(client: &Client, url: &Url) -> Result<Url, FollowError> {
// Send the request, follow the URL, ensure success
let response = client
.get(url.as_str())
.send()
.map_err(FollowError::Request)?;
ensure_success(&response)?;
// Obtain the final URL
Ok(response.url().clone())
}
/// URL following error.
#[derive(Debug, Fail)]
pub enum FollowError {
/// Failed to send the shortening request.
#[fail(display = "failed to send URL follow request")]
Request(#[cause] reqwest::Error),
/// The server responded with a bad response.
#[fail(display = "failed to shorten URL, got bad response")]
Response(#[cause] ResponseError),
}
impl From<ResponseError> for FollowError {
fn from(err: ResponseError) -> Self {
FollowError::Response(err)
}
}