Merge branch 'shorten-urls' into 'master'
Implement URL shortener support Closes #68 See merge request timvisee/ffsend!20
This commit is contained in:
commit
866579c80b
12 changed files with 242 additions and 25 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")]
|
||||
{
|
||||
|
|
|
@ -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
97
src/urlshorten.rs
Normal 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)
|
||||
}
|
||||
}
|
41
src/util.rs
41
src/util.rs
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue