Merge branch 'send3' into 'master'
Implement support for Firefox Send v3 Closes #53 See merge request timvisee/ffsend!8
This commit is contained in:
commit
0a5b4dc7e8
28 changed files with 923 additions and 283 deletions
|
@ -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
713
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
@ -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"
|
||||
|
|
|
@ -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
4
pkg/snap/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
parts/
|
||||
prime/
|
||||
snap/
|
||||
stage/
|
32
pkg/snap/snapcraft.yaml
Normal file
32
pkg/snap/snapcraft.yaml
Normal 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]
|
|
@ -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:"),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
59
src/action/version.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
63
src/cmd/arg/api.rs
Normal 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 == "-"
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
30
src/cmd/matcher/version.rs
Normal file
30
src/cmd/matcher/version.rs
Normal 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 })
|
||||
}
|
||||
}
|
|
@ -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
16
src/cmd/subcmd/version.rs
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
11
src/error.rs
11
src/error.rs
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
31
src/util.rs
31
src/util.rs
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue