Merge branch 'send3' into 'master'

Implement support for Firefox Send v3

Closes #53

See merge request timvisee/ffsend!8
This commit is contained in:
Tim Visée 2019-02-28 18:40:11 +00:00
commit 0a5b4dc7e8
28 changed files with 923 additions and 283 deletions

View file

@ -38,7 +38,8 @@ variables:
stage: check
script:
- cargo check --verbose
- cargo check --no-default-features --verbose
- cargo check --no-default-features --features send2 --verbose
- cargo check --no-default-features --features send3 --verbose
- cargo check --features no-color --verbose
rust-stable:
<<: *check-base
@ -109,6 +110,7 @@ cargo-test:
# Run integration test with the public Send service
public-send-test:
stage: test
allow_failure: true
dependencies:
- build-static
variables:
@ -116,11 +118,16 @@ public-send-test:
before_script: []
script:
# Generate random file, upload/download and assert equality
- "head -c16m </dev/urandom >testfile"
- "head -c2m </dev/urandom >testfile"
- "./ffsend upload testfile -d=10 -p=secret -I"
- "./ffsend download $(./ffsend history -q) -p=secret -I -o=downloadfile"
- "cmp --silent ./testfile ./downloadfile || (echo ERROR: Downloaded file is different than original; exit 1)"
# Also test Firefox Send v3
- "./ffsend upload --host http://send2.dev.lcip.org/ testfile -d=10 -p=secret -I"
- "./ffsend download $(./ffsend history -q) -p=secret -I -o=downloadfile2"
- "cmp --silent ./testfile ./downloadfile2 || (echo ERROR: Downloaded file is different than original; exit 1)"
# Cargo crate release
crate:
stage: release
@ -177,3 +184,22 @@ pkg-aur:
- git add PKGBUILD .SRCINFO
- git commit -m "Release v$VERSION"
- git push
# # Snap release
# snap:
# image: snapcore/snapcraft:edge
# stage: release
# # only:
# # - /^v(\d+\.)*\d+$/
# before_script: []
# script:
# - echo "Building snap package..."
# - cd pkg/snap
# - snapcraft
# # TODO: See: https://docs.snapcraft.io/rust-applications/7826
# # TODO: - login to registry
# # TODO: - test built snap
# # TODO: - push snap to snapcraft.io
# # - echo "Publishing snap package..."
# # - echo "$SNAP_USER\n&SNAP_PASS" | snapcraft login
# # - snapcraft push --release=edge ffsend_amd64.snap

713
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -50,7 +50,7 @@ name = "ffsend"
path = "src/main.rs"
[features]
default = ["archive", "clipboard", "history"]
default = ["archive", "clipboard", "history", "send2", "send3"]
# Compile with file archiving support
archive = ["tar"]
@ -61,6 +61,12 @@ history = []
# Compile without colored output support
no-color = ["colored/no-color"]
# Support for Firefox Send v2
send2 = ["ffsend-api/send2"]
# Support for Firefox Send v3
send3 = ["ffsend-api/send3"]
[dependencies]
chbs = "0.0.8"
chrono = "0.4"
@ -69,7 +75,7 @@ colored = "1.7"
derive_builder = "0.7"
directories = "1.0"
failure = "0.1"
ffsend-api = "0.1.1"
ffsend-api = { version = "0.2.0", default-features = false }
fs2 = "0.4"
lazy_static = "1.0"
open = "1"

View file

@ -53,6 +53,7 @@ _Note: this tool is currently in the alpha phase_
- Upload and download files and directories securely
- Always encrypted on the client
- Additional password protection, generation and configurable download limits
- Supports old and new Firefox Send versions
- Built-in file and directory archiving and extraction
- History tracking your files for easy management
- Ability to use your own Send host
@ -239,6 +240,8 @@ The following features are available, some of which are enabled by default:
| Feature | Enabled | Description |
| :---------: | :-----: | :--------------------------------------------------------- |
| `send2` | Default | Compile with support for Firefox Send v2 servers |
| `send3` | Default | Compile with support for Firefox Send v3 servers |
| `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 |
@ -268,11 +271,12 @@ defaults. The CLI flag is shown along with it, to better describe the relation
to command line arguments:
| Variable | CLI flag | Description |
| :------------------------ | :----------------------------: | :------------------------------ |
| :------------------------ | :----------------------------: | :-------------------------------- |
| `FFSEND_HISTORY` | `--history <FILE>` | History file path |
| `FFSEND_HOST` | `--host <URL>` | Upload host |
| `FFSEND_TIMEOUT` | `--timeout <SECONDS>` | Request timeout (0 to disable) |
| `FFSEND_TRANSFER_TIMEOUT` | `--transfer-timeout <SECONDS>` | Transfer timeout (0 to disable) |
| `FFSEND_API` | `--api <VERSION>` | Server API version, `-` to lookup |
These environment variables may be used to toggle a flag, simply by making them
available. The actual value of these variables is ignored, and variables may be
@ -375,6 +379,7 @@ SUBCOMMANDS:
info Fetch info about a shared file [aliases: i]
parameters Change parameters of a shared file [aliases: params]
password Change the password of a shared file [aliases: pass, p]
version Determine the Send server version [aliases: v]
The public Send service that is used as default host is provided by Mozilla.
This application is not affiliated with Mozilla, Firefox or Firefox Send.

4
pkg/snap/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
parts/
prime/
snap/
stage/

32
pkg/snap/snapcraft.yaml Normal file
View file

@ -0,0 +1,32 @@
name: ffsend
version: git
summary: Easily and securely share files from the command line.
description: |
Easily and securely share files and directories from the command line through
a safe, private and encrypted link using a single simple command.
Files are shared using the Send service and may be up to 2GB.
Others are able to download these files with this tool
or through their web browser.
All files are always encrypted on the client,
and secrets are never shared with the remote host.
An optional password may be specified, and a default file lifetime of 1
(up to 20) download or 24 hours is enforced to ensure your stuff does not
remain online forever. This provides a secure platform to share your files.
grade: devel
confinement: devmode
apps:
ffsend:
command: ffsend
plugs: [home, removable-media]
parts:
ffsend:
source: ../../
plugin: rust
build-attributes: [no-system-libraries]
build-packages: [make, cmake, pkg-config]
# build-packages: [g++, libudev-dev, libssl-dev, make, pkg-config]
stage-packages: [libssl1.0.0, xclip]

View file

@ -6,7 +6,7 @@ use prettytable::{format::FormatBuilder, Cell, Row, Table};
use crate::client::to_duration;
use crate::cmd::matcher::{debug::DebugMatcher, main::MainMatcher, Matcher};
use crate::error::ActionError;
use crate::util::{features_list, format_bool, format_duration};
use crate::util::{api_version_list, features_list, format_bool, format_duration};
/// A file debug action.
pub struct Debug<'a> {
@ -74,7 +74,7 @@ impl<'a> Debug<'a> {
table.add_row(Row::new(vec![
Cell::new("Default expiry:"),
Cell::new(&format_duration(Duration::seconds(
SEND_DEFAULT_EXPIRE_TIME,
SEND_DEFAULT_EXPIRE_TIME as i64,
))),
]));
@ -84,6 +84,12 @@ impl<'a> Debug<'a> {
Cell::new(&features_list().join(", ")),
]));
// Render a list of compiled features
table.add_row(Row::new(vec![
Cell::new("API support:"),
Cell::new(&api_version_list().join(", ")),
]));
// Show whether quiet is used
table.add_row(Row::new(vec![
Cell::new("Quiet:"),

View file

@ -10,8 +10,9 @@ use failure::Fail;
use ffsend_api::action::download::{Download as ApiDownload, Error as DownloadError};
use ffsend_api::action::exists::{Error as ExistsError, Exists as ApiExists};
use ffsend_api::action::metadata::{Error as MetadataError, Metadata as ApiMetadata};
use ffsend_api::action::version::Error as VersionError;
use ffsend_api::file::remote_file::{FileParseError, RemoteFile};
use ffsend_api::reader::ProgressReporter;
use ffsend_api::pipe::ProgressReporter;
#[cfg(feature = "archive")]
use tempfile::{Builder as TempBuilder, NamedTempFile};
@ -25,6 +26,7 @@ use crate::progress::ProgressBar;
use crate::util::{
ensure_enough_space, ensure_password, prompt_yes, quit, quit_error, quit_error_msg, ErrorHints,
};
use super::select_api_version;
/// A file download action.
pub struct Download<'a> {
@ -44,12 +46,18 @@ 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
// Get the share URL, derive the host
let url = matcher_download.url();
let host = matcher_download.guess_host();
// Create a reqwest client capable for downloading files
let client = create_transfer_client(&matcher_main);
// Determine the API version to use
let mut desired_version = matcher_main.api();
select_api_version(&client, host, &mut desired_version)?;
let api_version = desired_version.version().unwrap();
// Parse the remote file based on the share URL
let file = RemoteFile::parse_url(url, None)?;
@ -68,7 +76,7 @@ impl<'a> Download<'a> {
}
// Ensure a password is set when required
ensure_password(&mut password, exists.has_password(), &matcher_main);
ensure_password(&mut password, exists.requires_password(), &matcher_main);
// Fetch the file metadata
let metadata = ApiMetadata::new(&file, password.clone(), false).invoke(&client)?;
@ -143,11 +151,11 @@ impl<'a> Download<'a> {
// Execute an download action
let progress = if !matcher_main.quiet() {
Some(&progress_reader)
Some(progress_reader)
} else {
None
};
ApiDownload::new(&file, target, password, false, Some(metadata))
ApiDownload::new(api_version, &file, target, password, false, Some(metadata))
.invoke(&client, progress)?;
// Extract the downloaded file if working with an archive
@ -324,6 +332,11 @@ impl<'a> Download<'a> {
#[derive(Debug, Fail)]
pub enum Error {
/// Selecting the API version to use failed.
// TODO: enable `api` hint!
#[fail(display = "failed to select API version to use")]
Version(#[cause] VersionError),
/// Failed to parse a share URL, it was invalid.
/// This error is not related to a specific action.
#[fail(display = "invalid share link")]
@ -351,6 +364,12 @@ pub enum Error {
Expired,
}
impl From<VersionError> for Error {
fn from(err: VersionError) -> Error {
Error::Version(err)
}
}
impl From<FileParseError> for Error {
fn from(err: FileParseError) -> Error {
Error::InvalidUrl(err)

View file

@ -43,7 +43,7 @@ impl<'a> Exists<'a> {
// Print the results
println!("Exists: {:?}", exists);
if exists {
println!("Password: {:?}", exists_response.has_password());
println!("Password: {:?}", exists_response.requires_password());
}
// Add or remove the file from the history

View file

@ -61,7 +61,7 @@ impl<'a> Info<'a> {
let mut password = matcher_info.password();
// Ensure a password is set when required
ensure_password(&mut password, exists.has_password(), &matcher_main);
ensure_password(&mut password, exists.requires_password(), &matcher_main);
// Fetch both file info and metadata
let info = ApiInfo::new(&file, None).invoke(&client)?;

View file

@ -8,3 +8,46 @@ pub mod info;
pub mod params;
pub mod password;
pub mod upload;
pub mod version;
use ffsend_api::action::version::{Version as ApiVersion, Error as VersionError};
use ffsend_api::api::DesiredVersion;
use ffsend_api::url::Url;
use crate::client::Client;
use crate::config::API_VERSION_ASSUME;
use crate::util::print_warning;
/// Based on the given desired API version, select a version we can use.
///
/// If the current desired version is set to the `DesiredVersion::Lookup` variant, this method
/// will look up the server API version. It it's `DesiredVersion::Use` it will return and
/// attempt to use the specified version.
fn select_api_version(client: &Client, host: Url, desired: &mut DesiredVersion) -> Result<(), VersionError> {
// Break if already specified
if let DesiredVersion::Use(_) = desired {
return Ok(());
}
// TODO: only lookup if `DesiredVersion::Assume` after first operation attempt failed
// Look up the version
match ApiVersion::new(host).invoke(&client) {
// Use the probed version
Ok(v) => *desired = DesiredVersion::Use(v),
// If unknown, just assume the default version
Err(VersionError::Unknown) => {
*desired = DesiredVersion::Use(API_VERSION_ASSUME);
print_warning(format!(
"server API version could not be determined, assuming v{}",
API_VERSION_ASSUME,
));
}
// Propegate other errors
Err(e) => return Err(e)
}
Ok(())
}

View file

@ -8,8 +8,9 @@ use clap::ArgMatches;
use failure::Fail;
use ffsend_api::action::params::ParamsDataBuilder;
use ffsend_api::action::upload::{Error as UploadError, Upload as ApiUpload};
use ffsend_api::action::version::Error as VersionError;
use ffsend_api::config::{UPLOAD_SIZE_MAX, UPLOAD_SIZE_MAX_RECOMMENDED};
use ffsend_api::reader::ProgressReporter;
use ffsend_api::pipe::ProgressReporter;
use prettytable::{format::FormatBuilder, Cell, Row, Table};
#[cfg(feature = "archive")]
use tempfile::{Builder as TempBuilder, NamedTempFile};
@ -27,6 +28,7 @@ use crate::util::{
format_bytes, open_url, print_error, print_error_msg, prompt_yes, quit, quit_error_msg,
ErrorHintsBuilder,
};
use super::select_api_version;
/// A file upload action.
pub struct Upload<'a> {
@ -51,6 +53,14 @@ impl<'a> Upload<'a> {
let mut path = Path::new(matcher_upload.file()).to_path_buf();
let host = matcher_upload.host();
// Create a reqwest client capable for uploading files
let client = create_transfer_client(&matcher_main);
// Determine the API version to use
let mut desired_version = matcher_main.api();
select_api_version(&client, host.clone(), &mut desired_version)?;
let api_version = desired_version.version().unwrap();
// TODO: ensure the file exists and is accessible
// Get the file size to warn about large files
@ -91,7 +101,7 @@ impl<'a> Upload<'a> {
}
// Create a reqwest client capable for uploading files
let client = create_transfer_client(&matcher_main);
let transfer_client = create_transfer_client(&matcher_main);
// Create a progress bar reporter
let progress_bar = Arc::new(Mutex::new(ProgressBar::new_upload()));
@ -203,8 +213,14 @@ impl<'a> Upload<'a> {
} else {
None
};
let file = ApiUpload::new(host, path.clone(), file_name, password.clone(), params)
.invoke(&client, reporter)?;
let file = ApiUpload::new(
api_version,
host,
path.clone(),
file_name,
password.clone(),
params,
).invoke(&transfer_client, reporter)?;
let url = file.download_url(true);
// Report the result
@ -275,6 +291,11 @@ impl<'a> Upload<'a> {
#[derive(Debug, Fail)]
pub enum Error {
/// Selecting the API version to use failed.
// TODO: enable `api` hint!
#[fail(display = "failed to select API version to use")]
Version(#[cause] VersionError),
/// An error occurred while archiving the file to upload.
#[cfg(feature = "archive")]
#[fail(display = "failed to archive file to upload")]
@ -285,6 +306,12 @@ pub enum Error {
Upload(#[cause] UploadError),
}
impl From<VersionError> for Error {
fn from(err: VersionError) -> Error {
Error::Version(err)
}
}
#[cfg(feature = "archive")]
impl From<ArchiveError> for Error {
fn from(err: ArchiveError) -> Error {

59
src/action/version.rs Normal file
View file

@ -0,0 +1,59 @@
use clap::ArgMatches;
use ffsend_api::action::version::{Error as VersionError, Version as ApiVersion};
use crate::client::create_client;
use crate::cmd::matcher::main::MainMatcher;
use crate::cmd::matcher::{version::VersionMatcher, Matcher};
use crate::error::ActionError;
/// A file version action.
pub struct Version<'a> {
cmd_matches: &'a ArgMatches<'a>,
}
impl<'a> Version<'a> {
/// Construct a new version action.
pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self {
Self { cmd_matches }
}
/// Invoke the version action.
// TODO: create a trait for this method
pub fn invoke(&self) -> Result<(), ActionError> {
// Create the command matchers
let matcher_version = VersionMatcher::with(self.cmd_matches).unwrap();
let matcher_main = MainMatcher::with(self.cmd_matches).unwrap();
// Get the host
let host = matcher_version.host();
// Create a reqwest client
let client = create_client(&matcher_main);
// Make sure the file version
let response = ApiVersion::new(host).invoke(&client);
// Print the result
match response {
Ok(v) => println!("API version: {}", v),
Err(VersionError::Unknown) => println!("Version: unknown"),
Err(VersionError::Unsupported(v)) => println!("Version: {} (unsupported)", v),
Err(e) => return Err(e.into()),
}
Ok(())
}
}
#[derive(Debug, Fail)]
pub enum Error {
/// An error occurred while attempting to determine the Send server version.
#[fail(display = "failed to check the server version")]
Version(#[cause] VersionError),
}
impl From<VersionError> for Error {
fn from(err: VersionError) -> Error {
Error::Version(err)
}
}

View file

@ -1,6 +1,7 @@
use std::time::Duration;
use ffsend_api::reqwest::{Client, ClientBuilder};
pub use ffsend_api::reqwest::Client;
use ffsend_api::reqwest::ClientBuilder;
use crate::cmd::matcher::MainMatcher;

63
src/cmd/arg/api.rs Normal file
View file

@ -0,0 +1,63 @@
use clap::{Arg, ArgMatches};
use ffsend_api::api::{DesiredVersion, Version};
use super::{CmdArg, CmdArgOption};
use crate::config::API_VERSION_DESIRED_DEFAULT;
use crate::util::{ErrorHints, quit_error_msg};
/// The api argument.
pub struct ArgApi {}
impl CmdArg for ArgApi {
fn name() -> &'static str {
"api"
}
fn build<'b, 'c>() -> Arg<'b, 'c> {
Arg::with_name("api")
.long("api")
.short("A")
.value_name("VERSION")
.env("FFSEND_API")
.hide_env_values(true)
.global(true)
.help("Server API version to use, '-' to lookup")
.long_help("Server API version to use, one of:\n\
2, 3: Firefox Send API versions\n\
auto, -: probe server to determine\
")
}
}
impl<'a> CmdArgOption<'a> for ArgApi {
type Value = DesiredVersion;
fn value<'b: 'a>(matches: &'a ArgMatches<'b>) -> Self::Value {
// Get the version string
let version = match Self::value_raw(matches) {
Some(version) => version,
None => return API_VERSION_DESIRED_DEFAULT,
};
// Parse the lookup version string
if is_auto(version) {
return DesiredVersion::Lookup;
}
// Parse the given API version
match Version::parse(version) {
Ok(version) => DesiredVersion::Use(version),
Err(_) => quit_error_msg(
"failed to determine given server API version, version unknown",
ErrorHints::default(),
),
}
}
}
/// Check whether the given API version argument means we've to probe the server for the proper
/// version.
fn is_auto(arg: &str) -> bool {
let arg = arg.trim().to_lowercase();
arg == "a" || arg == "auto" || arg == "-"
}

View file

@ -1,3 +1,4 @@
pub mod api;
pub mod download_limit;
pub mod gen_passphrase;
pub mod host;
@ -5,7 +6,8 @@ pub mod owner;
pub mod password;
pub mod url;
// Reexport to arg module
// Re-eexport to arg module
pub use self::api::ArgApi;
pub use self::download_limit::ArgDownloadLimit;
pub use self::gen_passphrase::ArgGenPassphrase;
pub use self::host::ArgHost;

View file

@ -2,16 +2,18 @@ extern crate directories;
use clap::{App, AppSettings, Arg, ArgMatches};
use super::arg::{ArgApi, CmdArg};
#[cfg(feature = "history")]
use super::matcher::HistoryMatcher;
use super::matcher::{
DebugMatcher, DeleteMatcher, DownloadMatcher, ExistsMatcher, InfoMatcher, Matcher,
ParamsMatcher, PasswordMatcher, UploadMatcher,
ParamsMatcher, PasswordMatcher, UploadMatcher, VersionMatcher,
};
#[cfg(feature = "history")]
use super::subcmd::CmdHistory;
use super::subcmd::{
CmdDebug, CmdDelete, CmdDownload, CmdExists, CmdInfo, CmdParams, CmdPassword, CmdUpload,
CmdVersion,
};
use crate::config::{CLIENT_TIMEOUT, CLIENT_TRANSFER_TIMEOUT};
#[cfg(feature = "history")]
@ -138,6 +140,7 @@ impl<'a: 'b, 'b> Handler<'a> {
.global(true)
.help("Enable verbose information and logging"),
)
.arg(ArgApi::build())
.subcommand(CmdDebug::build())
.subcommand(CmdDelete::build())
.subcommand(CmdDownload::build().display_order(2))
@ -145,7 +148,8 @@ impl<'a: 'b, 'b> Handler<'a> {
.subcommand(CmdInfo::build())
.subcommand(CmdParams::build())
.subcommand(CmdPassword::build())
.subcommand(CmdUpload::build().display_order(1));
.subcommand(CmdUpload::build().display_order(1))
.subcommand(CmdVersion::build());
// With history support, a flag for the history file and incognito mode
#[cfg(feature = "history")]
@ -239,4 +243,9 @@ impl<'a: 'b, 'b> Handler<'a> {
pub fn upload(&'a self) -> Option<UploadMatcher> {
UploadMatcher::with(&self.matches)
}
/// Get the version sub command, if matched.
pub fn version(&'a self) -> Option<VersionMatcher> {
VersionMatcher::with(&self.matches)
}
}

View file

@ -23,6 +23,17 @@ impl<'a: 'b, 'b> DownloadMatcher<'a> {
ArgUrl::value(self.matches)
}
/// 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();
url.set_path("");
url.set_query(None);
url.set_fragment(None);
url
}
/// Get the password.
/// `None` is returned if no password was specified.
pub fn password(&'a self) -> Option<String> {

View file

@ -2,8 +2,10 @@
use std::path::PathBuf;
use clap::ArgMatches;
use ffsend_api::api::DesiredVersion;
use super::Matcher;
use crate::cmd::arg::{ArgApi, CmdArgOption};
use crate::util::env_var_present;
#[cfg(feature = "history")]
use crate::util::{quit_error_msg, ErrorHintsBuilder};
@ -29,6 +31,11 @@ impl<'a: 'b, 'b> MainMatcher<'a> {
self.matches.is_present("yes") || env_var_present("FFSEND_YES")
}
/// Get the desired API version to use.
pub fn api(&'a self) -> DesiredVersion {
ArgApi::value(self.matches)
}
/// Get the history file to use.
#[cfg(feature = "history")]
pub fn history(&self) -> PathBuf {

View file

@ -9,8 +9,9 @@ pub mod main;
pub mod params;
pub mod password;
pub mod upload;
pub mod version;
// Reexport to matcher module
// Re-export to matcher module
pub use self::debug::DebugMatcher;
pub use self::delete::DeleteMatcher;
pub use self::download::DownloadMatcher;
@ -22,6 +23,7 @@ pub use self::main::MainMatcher;
pub use self::params::ParamsMatcher;
pub use self::password::PasswordMatcher;
pub use self::upload::UploadMatcher;
pub use self::version::VersionMatcher;
use clap::ArgMatches;

View file

@ -0,0 +1,30 @@
use ffsend_api::url::Url;
use clap::ArgMatches;
use super::Matcher;
use crate::cmd::arg::{ArgHost, CmdArgOption};
/// The version command matcher.
pub struct VersionMatcher<'a> {
matches: &'a ArgMatches<'a>,
}
impl<'a: 'b, 'b> VersionMatcher<'a> {
/// Get the host to probe.
///
/// This method parses the host into an `Url`.
/// If the given host is invalid,
/// the program will quit with an error message.
pub fn host(&'a self) -> Url {
ArgHost::value(self.matches)
}
}
impl<'a> Matcher<'a> for VersionMatcher<'a> {
fn with(matches: &'a ArgMatches) -> Option<Self> {
matches
.subcommand_matches("version")
.map(|matches| VersionMatcher { matches })
}
}

View file

@ -8,8 +8,9 @@ pub mod info;
pub mod params;
pub mod password;
pub mod upload;
pub mod version;
// Reexport to cmd module
// Re-export to cmd module
pub use self::debug::CmdDebug;
pub use self::delete::CmdDelete;
pub use self::download::CmdDownload;
@ -20,3 +21,4 @@ pub use self::info::CmdInfo;
pub use self::params::CmdParams;
pub use self::password::CmdPassword;
pub use self::upload::CmdUpload;
pub use self::version::CmdVersion;

16
src/cmd/subcmd/version.rs Normal file
View file

@ -0,0 +1,16 @@
use clap::{App, SubCommand};
use crate::cmd::arg::{ArgHost, CmdArg};
/// The version command definition.
pub struct CmdVersion;
impl CmdVersion {
pub fn build<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("version")
.about("Determine the Send server version")
.alias("ver")
.visible_alias("v")
.arg(ArgHost::build())
}
}

View file

@ -1,6 +1,17 @@
use ffsend_api::api::{DesiredVersion, Version};
/// The timeout for the Send client for generic requests, `0` to disable.
pub const CLIENT_TIMEOUT: u64 = 30;
/// The timeout for the Send client used to transfer (upload/download) files.
/// Make sure this is big enough, or file uploads will be dropped. `0` to disable.
pub const CLIENT_TRANSFER_TIMEOUT: u64 = 24 * 60 * 60;
/// The default desired version to select for the server API.
pub const API_VERSION_DESIRED_DEFAULT: DesiredVersion = DesiredVersion::Assume(API_VERSION_ASSUME);
/// The default server API version to assume when it could not be determined.
#[cfg(feature = "send2")]
pub const API_VERSION_ASSUME: Version = Version::V2;
#[cfg(not(feature = "send2"))]
pub const API_VERSION_ASSUME: Version = Version::V3;

View file

@ -2,6 +2,7 @@ use ffsend_api::action::delete::Error as DeleteError;
use ffsend_api::action::exists::Error as ExistsError;
use ffsend_api::action::params::Error as ParamsError;
use ffsend_api::action::password::Error as PasswordError;
use ffsend_api::action::version::Error as VersionError;
use ffsend_api::file::remote_file::FileParseError;
use crate::action::download::Error as CliDownloadError;
@ -72,6 +73,10 @@ pub enum ActionError {
#[fail(display = "failed to change the password")]
Password(#[cause] PasswordError),
/// An error occurred while invoking the version action.
#[fail(display = "failed to determine server version")]
Version(#[cause] VersionError),
/// An error occurred while invoking the upload action.
#[fail(display = "failed to upload the specified file")]
Upload(#[cause] CliUploadError),
@ -113,6 +118,12 @@ impl From<PasswordError> for ActionError {
}
}
impl From<VersionError> for ActionError {
fn from(err: VersionError) -> ActionError {
ActionError::Version(err)
}
}
impl From<FileParseError> for ActionError {
fn from(err: FileParseError) -> ActionError {
ActionError::InvalidUrl(err)

View file

@ -43,6 +43,7 @@ use crate::action::info::Info;
use crate::action::params::Params;
use crate::action::password::Password;
use crate::action::upload::Upload;
use crate::action::version::Version;
use crate::cmd::{
matcher::{MainMatcher, Matcher},
Handler,
@ -135,6 +136,13 @@ fn invoke_action(handler: &Handler) -> Result<(), Error> {
.map_err(|err| err.into());
}
// Match the version command
if handler.version().is_some() {
return Version::new(handler.matches())
.invoke()
.map_err(|err| err.into());
}
// Get the main matcher
let matcher_main = MainMatcher::with(handler.matches()).unwrap();

View file

@ -4,7 +4,7 @@ use std::io::{stderr, Stderr};
use std::time::Duration;
use self::pbr::{ProgressBar as Pbr, Units};
use ffsend_api::reader::ProgressReporter;
use ffsend_api::pipe::ProgressReporter;
/// The refresh rate of the progress bar, in milliseconds.
const PROGRESS_BAR_FPS_MILLIS: u64 = 200;

View file

@ -78,7 +78,6 @@ where
}
/// Print a warning.
#[cfg(feature = "history")]
pub fn print_warning<S>(err: S)
where
S: AsRef<str> + Display + Debug + Sync + Send + 'static,
@ -117,6 +116,9 @@ where
#[derive(Clone, Builder)]
#[builder(default)]
pub struct ErrorHints {
/// Show about specifying an API version.
api: bool,
/// A list of info messages to print along with the error.
info: Vec<String>,
@ -171,6 +173,12 @@ impl ErrorHints {
eprint!("\n");
// Print hints
if self.api {
eprintln!(
"Use '{}' to select a server API version",
highlight("--api <VERSION>")
);
}
if self.password {
eprintln!(
"Use '{}' to specify a password",
@ -210,6 +218,7 @@ impl ErrorHints {
impl Default for ErrorHints {
fn default() -> Self {
ErrorHints {
api: false,
info: Vec::new(),
password: false,
owner: false,
@ -250,7 +259,6 @@ pub fn highlight_error(msg: &str) -> ColoredString {
}
/// Highlight the given text with an warning color.
#[cfg(feature = "history")]
pub fn highlight_warning(msg: &str) -> ColoredString {
highlight(msg).bold()
}
@ -743,6 +751,25 @@ pub fn features_list() -> Vec<&'static str> {
features.push("history");
#[cfg(feature = "no-color")]
features.push("no-color");
#[cfg(feature = "send2")]
features.push("send2");
#[cfg(feature = "send3")]
features.push("send3");
features
}
/// Get a list of supported API versions.
pub fn api_version_list() -> Vec<&'static str> {
// Build the list
#[allow(unused_mut)]
let mut versions = Vec::new();
// Add each feature
#[cfg(feature = "send2")]
versions.push("v2");
#[cfg(feature = "send3")]
versions.push("v3");
versions
}