mirror of
https://github.com/timvisee/ffsend.git
synced 2025-10-06 10:39:57 +02:00
Create upload command, implement download data decryption logic
This commit is contained in:
parent
dd41dfbacc
commit
5b1724ef9e
10 changed files with 411 additions and 204 deletions
|
@ -1,39 +1,30 @@
|
|||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::path::Path;
|
||||
|
||||
use mime_guess::{get_mime_type, Mime};
|
||||
use openssl::symm::encrypt_aead;
|
||||
use openssl::symm::{decrypt_aead, encrypt_aead};
|
||||
use reqwest::{
|
||||
Client,
|
||||
Error as ReqwestError,
|
||||
Request,
|
||||
};
|
||||
use reqwest::header::Authorization;
|
||||
use reqwest::mime::APPLICATION_OCTET_STREAM;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use url::Url;
|
||||
use serde_json;
|
||||
|
||||
use crypto::b64;
|
||||
use crypto::key_set::KeySet;
|
||||
use reader::{
|
||||
EncryptedFileReaderTagged,
|
||||
ExactLengthReader,
|
||||
ProgressReader,
|
||||
ProgressReporter,
|
||||
};
|
||||
use file::file::DownloadFile;
|
||||
use file::metadata::{Metadata, XFileMetadata};
|
||||
use file::metadata::Metadata;
|
||||
|
||||
pub type Result<T> = ::std::result::Result<T, DownloadError>;
|
||||
|
||||
/// The name of the header that is used for the authentication nonce.
|
||||
const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate";
|
||||
|
||||
// TODO: experiment with `iv` of `None` in decrypt logic
|
||||
|
||||
/// A file upload action to a Send server.
|
||||
pub struct Download<'a> {
|
||||
/// The Send file to download.
|
||||
file: &DownloadFile,
|
||||
file: &'a DownloadFile,
|
||||
}
|
||||
|
||||
impl<'a> Download<'a> {
|
||||
|
@ -48,7 +39,7 @@ impl<'a> Download<'a> {
|
|||
pub fn invoke(
|
||||
self,
|
||||
client: &Client,
|
||||
) -> Result<SendFile> {
|
||||
) -> Result<()> {
|
||||
// Create a key set for the file
|
||||
let key = KeySet::from(self.file);
|
||||
|
||||
|
@ -68,7 +59,7 @@ impl<'a> Download<'a> {
|
|||
|
||||
// Get the download url, and parse the nonce
|
||||
// TODO: do not unwrap here, return error
|
||||
let download_url = file.download_url(false);
|
||||
let download_url = self.file.download_url(false);
|
||||
let response = client.get(download_url)
|
||||
.send()
|
||||
.expect("failed to get nonce, failed to send file request");
|
||||
|
@ -95,158 +86,213 @@ impl<'a> Download<'a> {
|
|||
.skip(1)
|
||||
.next()
|
||||
.expect("missing authentication nonce")
|
||||
);
|
||||
).expect("failed to decode authentication nonce");
|
||||
|
||||
// TODO: set the input vector
|
||||
|
||||
// Crpate metadata and a file reader
|
||||
let metadata = self.create_metadata(&key, &file)?;
|
||||
let reader = self.create_reader(&key, reporter.clone())?;
|
||||
let reader_len = reader.len().unwrap();
|
||||
|
||||
// Create the request to send
|
||||
let req = self.create_request(
|
||||
client,
|
||||
&key,
|
||||
metadata,
|
||||
reader,
|
||||
);
|
||||
|
||||
// Start the reporter
|
||||
reporter.lock()
|
||||
.expect("unable to start progress, failed to get lock")
|
||||
.start(reader_len);
|
||||
|
||||
// Execute the request
|
||||
let result = self.execute_request(req, client, &key);
|
||||
|
||||
// Mark the reporter as finished
|
||||
reporter.lock()
|
||||
.expect("unable to finish progress, failed to get lock")
|
||||
.finish();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Create a blob of encrypted metadata.
|
||||
fn create_metadata(&self, key: &KeySet, file: &FileData)
|
||||
-> Result<Vec<u8>>
|
||||
{
|
||||
// Construct the metadata
|
||||
let metadata = Metadata::from(
|
||||
key.iv(),
|
||||
file.name().to_owned(),
|
||||
file.mime().clone(),
|
||||
).to_json().into_bytes();
|
||||
|
||||
// Encrypt the metadata
|
||||
let mut metadata_tag = vec![0u8; 16];
|
||||
let mut metadata = match encrypt_aead(
|
||||
// Determine the signature
|
||||
// TODO: use a tag length const here
|
||||
// TODO: do not unwrap, return an error
|
||||
let mut sig = vec![0u8; 16];
|
||||
encrypt_aead(
|
||||
KeySet::cipher(),
|
||||
key.meta_key().unwrap(),
|
||||
Some(&[0u8; 12]),
|
||||
key.auth_key().unwrap(),
|
||||
None,
|
||||
&[],
|
||||
&metadata,
|
||||
&mut metadata_tag,
|
||||
) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(_) => return Err(DownloadError::EncryptionError),
|
||||
};
|
||||
&nonce,
|
||||
&mut sig,
|
||||
).expect("failed to derive signature");
|
||||
let sig_encoded = b64::encode(&sig);
|
||||
|
||||
// Append the encryption tag
|
||||
metadata.append(&mut metadata_tag);
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Create a reader that reads the file as encrypted stream.
|
||||
fn create_reader(
|
||||
&self,
|
||||
key: &KeySet,
|
||||
reporter: Arc<Mutex<ProgressReporter>>,
|
||||
) -> Result<EncryptedReader> {
|
||||
// Open the file
|
||||
let file = match File::open(self.path.as_path()) {
|
||||
Ok(file) => file,
|
||||
Err(_) => return Err(DownloadError::FileError),
|
||||
};
|
||||
|
||||
// Create an encrypted reader
|
||||
let reader = match EncryptedFileReaderTagged::new(
|
||||
file,
|
||||
KeySet::cipher(),
|
||||
key.file_key().unwrap(),
|
||||
key.iv(),
|
||||
) {
|
||||
Ok(reader) => reader,
|
||||
Err(_) => return Err(DownloadError::EncryptionError),
|
||||
};
|
||||
|
||||
// Buffer the encrypted reader
|
||||
let reader = BufReader::new(reader);
|
||||
|
||||
// Wrap into the encrypted reader
|
||||
let mut reader = ProgressReader::new(reader)
|
||||
.expect("failed to create progress reader");
|
||||
|
||||
// Initialize and attach the reporter
|
||||
reader.set_reporter(reporter);
|
||||
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
/// Build the request that will be send to the server.
|
||||
fn create_request(
|
||||
&self,
|
||||
client: &Client,
|
||||
key: &KeySet,
|
||||
metadata: Vec<u8>,
|
||||
reader: EncryptedReader,
|
||||
) -> Request {
|
||||
// Get the reader length
|
||||
let len = reader.len().expect("failed to get reader length");
|
||||
|
||||
// Configure a form to send
|
||||
let part = Part::reader_with_length(reader, len)
|
||||
// .file_name(file.name())
|
||||
.mime(APPLICATION_OCTET_STREAM);
|
||||
let form = Form::new()
|
||||
.part("data", part);
|
||||
|
||||
// Define the URL to call
|
||||
let url = self.host.join("api/upload").expect("invalid host");
|
||||
|
||||
// Build the request
|
||||
client.post(url.as_str())
|
||||
// Get the meta URL, fetch the metadata
|
||||
// TODO: do not unwrap here, return error
|
||||
let meta_url = self.file.api_meta_url();
|
||||
let mut response = client.get(meta_url)
|
||||
.header(Authorization(
|
||||
format!("send-v1 {}", key.auth_key_encoded().unwrap())
|
||||
format!("send-v1 {}", sig_encoded)
|
||||
))
|
||||
.header(XFileMetadata::from(&metadata))
|
||||
.multipart(form)
|
||||
.build()
|
||||
.expect("failed to build an API request")
|
||||
.send()
|
||||
.expect("failed to fetch metadata, failed to send request");
|
||||
|
||||
// Validate the status code
|
||||
// TODO: allow redirects here?
|
||||
if !response.status().is_success() {
|
||||
// TODO: return error here
|
||||
panic!("failed to fetch metadata, request status is not successful");
|
||||
}
|
||||
|
||||
/// Execute the given request, and create a file object that represents the
|
||||
/// uploaded file.
|
||||
fn execute_request(&self, req: Request, client: &Client, key: &KeySet)
|
||||
-> Result<SendFile>
|
||||
{
|
||||
// Execute the request
|
||||
let mut res = match client.execute(req) {
|
||||
Ok(res) => res,
|
||||
Err(err) => return Err(DownloadError::RequestError(err)),
|
||||
};
|
||||
// Get the metadata nonce
|
||||
// TODO: don't unwrap here, return an error
|
||||
let nonce = b64::decode(
|
||||
response.headers()
|
||||
.get_raw(HEADER_AUTH_NONCE)
|
||||
.expect("missing authenticate header")
|
||||
.one()
|
||||
.map(|line| String::from_utf8(line.to_vec())
|
||||
.expect("invalid authentication header contents")
|
||||
)
|
||||
.expect("authentication header is empty")
|
||||
.split_terminator(" ")
|
||||
.skip(1)
|
||||
.next()
|
||||
.expect("missing metadata nonce")
|
||||
);
|
||||
|
||||
// Decode the response
|
||||
let res: DownloadResponse = match res.json() {
|
||||
Ok(res) => res,
|
||||
Err(_) => return Err(DownloadError::DecodeError),
|
||||
};
|
||||
// Parse the metadata response
|
||||
let meta_response: MetadataResponse = response.json()
|
||||
.expect("failed to parse metadata response");
|
||||
|
||||
// Transform the responce into a file object
|
||||
Ok(res.into_file(self.host.clone(), &key))
|
||||
// Decrypt the metadata
|
||||
let metadata = meta_response.decrypt_metadata(&key);
|
||||
|
||||
println!("GOT METADATA: {:?}", metadata);
|
||||
|
||||
// // Crpate metadata and a file reader
|
||||
// let metadata = self.create_metadata(&key, &file)?;
|
||||
// let reader = self.create_reader(&key, reporter.clone())?;
|
||||
// let reader_len = reader.len().unwrap();
|
||||
|
||||
// // Create the request to send
|
||||
// let req = self.create_request(
|
||||
// client,
|
||||
// &key,
|
||||
// metadata,
|
||||
// reader,
|
||||
// );
|
||||
|
||||
// // Start the reporter
|
||||
// reporter.lock()
|
||||
// .expect("unable to start progress, failed to get lock")
|
||||
// .start(reader_len);
|
||||
|
||||
// // Execute the request
|
||||
// let result = self.execute_request(req, client, &key);
|
||||
|
||||
// // Mark the reporter as finished
|
||||
// reporter.lock()
|
||||
// .expect("unable to finish progress, failed to get lock")
|
||||
// .finish();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// /// Create a blob of encrypted metadata.
|
||||
// fn create_metadata(&self, key: &KeySet, file: &FileData)
|
||||
// -> Result<Vec<u8>>
|
||||
// {
|
||||
// // Construct the metadata
|
||||
// let metadata = Metadata::from(
|
||||
// key.iv(),
|
||||
// file.name().to_owned(),
|
||||
// file.mime().clone(),
|
||||
// ).to_json().into_bytes();
|
||||
|
||||
// // Encrypt the metadata
|
||||
// let mut metadata_tag = vec![0u8; 16];
|
||||
// let mut metadata = match encrypt_aead(
|
||||
// KeySet::cipher(),
|
||||
// key.meta_key().unwrap(),
|
||||
// Some(&[0u8; 12]),
|
||||
// &[],
|
||||
// &metadata,
|
||||
// &mut metadata_tag,
|
||||
// ) {
|
||||
// Ok(metadata) => metadata,
|
||||
// Err(_) => return Err(DownloadError::EncryptionError),
|
||||
// };
|
||||
|
||||
// // Append the encryption tag
|
||||
// metadata.append(&mut metadata_tag);
|
||||
|
||||
// Ok(metadata)
|
||||
// }
|
||||
|
||||
// /// Create a reader that reads the file as encrypted stream.
|
||||
// fn create_reader(
|
||||
// &self,
|
||||
// key: &KeySet,
|
||||
// reporter: Arc<Mutex<ProgressReporter>>,
|
||||
// ) -> Result<EncryptedReader> {
|
||||
// // Open the file
|
||||
// let file = match File::open(self.path.as_path()) {
|
||||
// Ok(file) => file,
|
||||
// Err(_) => return Err(DownloadError::FileError),
|
||||
// };
|
||||
|
||||
// // Create an encrypted reader
|
||||
// let reader = match EncryptedFileReaderTagged::new(
|
||||
// file,
|
||||
// KeySet::cipher(),
|
||||
// key.file_key().unwrap(),
|
||||
// key.iv(),
|
||||
// ) {
|
||||
// Ok(reader) => reader,
|
||||
// Err(_) => return Err(DownloadError::EncryptionError),
|
||||
// };
|
||||
|
||||
// // Buffer the encrypted reader
|
||||
// let reader = BufReader::new(reader);
|
||||
|
||||
// // Wrap into the encrypted reader
|
||||
// let mut reader = ProgressReader::new(reader)
|
||||
// .expect("failed to create progress reader");
|
||||
|
||||
// // Initialize and attach the reporter
|
||||
// reader.set_reporter(reporter);
|
||||
|
||||
// Ok(reader)
|
||||
// }
|
||||
|
||||
// /// Build the request that will be send to the server.
|
||||
// fn create_request(
|
||||
// &self,
|
||||
// client: &Client,
|
||||
// key: &KeySet,
|
||||
// metadata: Vec<u8>,
|
||||
// reader: EncryptedReader,
|
||||
// ) -> Request {
|
||||
// // Get the reader length
|
||||
// let len = reader.len().expect("failed to get reader length");
|
||||
|
||||
// // Configure a form to send
|
||||
// let part = Part::reader_with_length(reader, len)
|
||||
// // .file_name(file.name())
|
||||
// .mime(APPLICATION_OCTET_STREAM);
|
||||
// let form = Form::new()
|
||||
// .part("data", part);
|
||||
|
||||
// // Define the URL to call
|
||||
// let url = self.host.join("api/upload").expect("invalid host");
|
||||
|
||||
// // Build the request
|
||||
// client.post(url.as_str())
|
||||
// .header(Authorization(
|
||||
// format!("send-v1 {}", key.auth_key_encoded().unwrap())
|
||||
// ))
|
||||
// .header(XFileMetadata::from(&metadata))
|
||||
// .multipart(form)
|
||||
// .build()
|
||||
// .expect("failed to build an API request")
|
||||
// }
|
||||
|
||||
// /// Execute the given request, and create a file object that represents the
|
||||
// /// uploaded file.
|
||||
// fn execute_request(&self, req: Request, client: &Client, key: &KeySet)
|
||||
// -> Result<SendFile>
|
||||
// {
|
||||
// // Execute the request
|
||||
// let mut res = match client.execute(req) {
|
||||
// Ok(res) => res,
|
||||
// Err(err) => return Err(DownloadError::RequestError(err)),
|
||||
// };
|
||||
|
||||
// // Decode the response
|
||||
// let res: DownloadResponse = match res.json() {
|
||||
// Ok(res) => res,
|
||||
// Err(_) => return Err(DownloadError::DecodeError),
|
||||
// };
|
||||
|
||||
// // Transform the responce into a file object
|
||||
// Ok(res.into_file(self.host.clone(), &key))
|
||||
// }
|
||||
}
|
||||
|
||||
/// Errors that may occur in the upload action.
|
||||
|
@ -270,39 +316,50 @@ pub enum DownloadError {
|
|||
DecodeError,
|
||||
}
|
||||
|
||||
/// The response from the server after a file has been uploaded.
|
||||
/// This response contains the file ID and owner key, to manage the file.
|
||||
/// The metadata response from the server, when fetching the data through
|
||||
/// the API.
|
||||
///
|
||||
/// It also contains the download URL, although an additional secret is
|
||||
/// required.
|
||||
///
|
||||
/// The download URL can be generated using `download_url()` which will
|
||||
/// include the required secret in the URL.
|
||||
/// This metadata is required to successfully download and decrypt the
|
||||
/// corresponding file.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DownloadResponse {
|
||||
/// The file ID.
|
||||
id: String,
|
||||
|
||||
/// The URL the file is reachable at.
|
||||
/// This includes the file ID, but does not include the secret.
|
||||
url: String,
|
||||
|
||||
/// The owner key, used to do further file modifications.
|
||||
owner: String,
|
||||
struct MetadataResponse {
|
||||
/// The encrypted metadata.
|
||||
#[serde(rename="metadata")]
|
||||
meta: String,
|
||||
}
|
||||
|
||||
impl DownloadResponse {
|
||||
/// Convert this response into a file object.
|
||||
impl MetadataResponse {
|
||||
/// Get and decrypt the metadata, based on the raw data in this response.
|
||||
///
|
||||
/// The `host` and `key` must be given.
|
||||
pub fn into_file(self, host: Url, key: &KeySet) -> SendFile {
|
||||
SendFile::new_now(
|
||||
self.id,
|
||||
host,
|
||||
Url::parse(&self.url)
|
||||
.expect("upload response URL parse error"),
|
||||
key.secret().to_vec(),
|
||||
self.owner,
|
||||
/// The decrypted data is verified using an included tag.
|
||||
/// If verification failed, an error is returned.
|
||||
// TODO: do not unwrap, return a proper error
|
||||
pub fn decrypt_metadata(&self, key_set: &KeySet) -> Result<Metadata> {
|
||||
// Decode the metadata
|
||||
let raw = b64::decode(&self.meta)
|
||||
.expect("failed to decode metadata from server");
|
||||
|
||||
// Get the encrypted metadata, and it's tag
|
||||
let (encrypted, tag) = raw.split_at(raw.len() - 16);
|
||||
// TODO: is the tag length correct, remove assert if it is
|
||||
assert_eq!(tag.len(), 16);
|
||||
|
||||
// Decrypt the metadata
|
||||
// TODO: is the tag verified here?
|
||||
// TODO: do not unwrap, return an error
|
||||
let meta = decrypt_aead(
|
||||
KeySet::cipher(),
|
||||
key_set.meta_key().unwrap(),
|
||||
Some(key_set.iv()),
|
||||
&[],
|
||||
encrypted,
|
||||
&tag,
|
||||
).expect("failed to decrypt metadata");
|
||||
|
||||
// Parse the metadata, and return
|
||||
Ok(
|
||||
serde_json::from_slice(&meta)
|
||||
.expect("failed to parse decrypted metadata as JSON")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
//pub mod download;
|
||||
pub mod download;
|
||||
pub mod upload;
|
||||
|
|
|
@ -14,11 +14,11 @@ use crypto::b64;
|
|||
// TODO: match any sub-path?
|
||||
// TODO: match URL-safe base64 chars for the file ID?
|
||||
// TODO: constrain the ID length?
|
||||
const DOWNLOAD_PATH_PATTERN: &'static str = r"$/?download/([[:alnum:]]{8,}={0,3})/?^";
|
||||
const DOWNLOAD_PATH_PATTERN: &'static str = r"^/?download/([[:alnum:]]{8,}={0,3})/?$";
|
||||
|
||||
/// A pattern for Send download URL fragments, capturing the file secret.
|
||||
// TODO: constrain the secret length?
|
||||
const DOWNLOAD_FRAGMENT_PATTERN: &'static str = r"$([a-zA-Z0-9-_+\/]+)?\s*^";
|
||||
const DOWNLOAD_FRAGMENT_PATTERN: &'static str = r"^([a-zA-Z0-9-_+/]+)?\s*$";
|
||||
|
||||
/// A struct representing an uploaded file on a Send host.
|
||||
///
|
||||
|
@ -159,20 +159,13 @@ impl DownloadFile {
|
|||
/// If the URL fragmet contains a file secret, it is also parsed.
|
||||
/// If it does not, the secret is left empty and must be specified
|
||||
/// manually.
|
||||
pub fn parse_url(url: String) -> Result<DownloadFile, FileParseError> {
|
||||
// Try to parse as an URL
|
||||
let url = Url::parse(&url)
|
||||
.map_err(|err| FileParseError::UrlFormatError(err))?;
|
||||
|
||||
pub fn parse_url(url: Url) -> Result<DownloadFile, FileParseError> {
|
||||
// Build the host
|
||||
let mut host = url.clone();
|
||||
host.set_fragment(None);
|
||||
host.set_query(None);
|
||||
host.set_path("");
|
||||
|
||||
// TODO: remove this after debugging
|
||||
println!("DEBUG: Extracted host: {}", host);
|
||||
|
||||
// Validate the path, get the file ID
|
||||
let re_path = Regex::new(DOWNLOAD_PATH_PATTERN).unwrap();
|
||||
let id = re_path.captures(url.path())
|
||||
|
@ -243,8 +236,19 @@ impl DownloadFile {
|
|||
|
||||
url
|
||||
}
|
||||
|
||||
/// Get the API metadata URL of the file.
|
||||
pub fn api_meta_url(&self) -> Url {
|
||||
// Get the download URL, and add the secret fragment
|
||||
let mut url = self.url.clone();
|
||||
url.set_path(format!("/api/metadata/{}", self.id).as_str());
|
||||
url.set_fragment(None);
|
||||
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FileParseError {
|
||||
/// An URL format error.
|
||||
UrlFormatError(UrlParseError),
|
||||
|
|
|
@ -14,7 +14,7 @@ use serde_json;
|
|||
use crypto::b64;
|
||||
|
||||
/// File metadata, which is send to the server.
|
||||
#[derive(Serialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Metadata {
|
||||
/// The input vector.
|
||||
iv: String,
|
||||
|
|
66
cli/src/action/download.rs
Normal file
66
cli/src/action/download.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ffsend_api::action::download::Download as ApiDownload;
|
||||
use ffsend_api::file::file::DownloadFile;
|
||||
use ffsend_api::reqwest::Client;
|
||||
|
||||
use cmd::cmd_download::CmdDownload;
|
||||
use progress::ProgressBar;
|
||||
use util::open_url;
|
||||
#[cfg(feature = "clipboard")]
|
||||
use util::set_clipboard;
|
||||
|
||||
/// A file download action.
|
||||
pub struct Download<'a> {
|
||||
cmd: &'a CmdDownload<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Download<'a> {
|
||||
/// Construct a new download action.
|
||||
pub fn new(cmd: &'a CmdDownload<'a>) -> Self {
|
||||
Self {
|
||||
cmd,
|
||||
}
|
||||
}
|
||||
|
||||
/// Invoke the download action.
|
||||
// TODO: create a trait for this method
|
||||
pub fn invoke(&self) {
|
||||
// Get the download URL
|
||||
let url = self.cmd.url();
|
||||
|
||||
// Create a reqwest client
|
||||
let client = Client::new();
|
||||
|
||||
// Parse the file based on the URL
|
||||
let file = DownloadFile::parse_url(url)
|
||||
.expect("invalid download URL, could not parse file data");
|
||||
|
||||
// Execute an download action
|
||||
// TODO: do not unwrap, but return an error
|
||||
ApiDownload::new(&file).invoke(&client).unwrap();
|
||||
|
||||
// // Get the download URL, and report it in the console
|
||||
// let url = file.download_url(true);
|
||||
// println!("Download URL: {}", url);
|
||||
|
||||
// // Open the URL in the browser
|
||||
// if self.cmd.open() {
|
||||
// // TODO: do not expect, but return an error
|
||||
// open_url(url.clone()).expect("failed to open URL");
|
||||
// }
|
||||
|
||||
// // Copy the URL in the user's clipboard
|
||||
// #[cfg(feature = "clipboard")]
|
||||
// {
|
||||
// if self.cmd.copy() {
|
||||
// // TODO: do not expect, but return an error
|
||||
// set_clipboard(url.as_str().to_owned())
|
||||
// .expect("failed to put download URL in user clipboard");
|
||||
// }
|
||||
// }
|
||||
|
||||
panic!("DONE");
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
pub mod download;
|
||||
pub mod upload;
|
||||
|
|
65
cli/src/cmd/cmd_download.rs
Normal file
65
cli/src/cmd/cmd_download.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use ffsend_api::url::{ParseError, Url};
|
||||
|
||||
use super::clap::{App, Arg, ArgMatches, SubCommand};
|
||||
|
||||
use app::SEND_DEF_HOST;
|
||||
use util::quit_error;
|
||||
|
||||
/// The download command.
|
||||
pub struct CmdDownload<'a> {
|
||||
matches: &'a ArgMatches<'a>,
|
||||
}
|
||||
|
||||
impl<'a: 'b, 'b> CmdDownload<'a> {
|
||||
/// Build the sub command definition.
|
||||
pub fn build<'y, 'z>() -> App<'y, 'z> {
|
||||
// Build the subcommand
|
||||
#[allow(unused_mut)]
|
||||
let mut cmd = SubCommand::with_name("download")
|
||||
.about("Download files")
|
||||
.visible_alias("d")
|
||||
.visible_alias("down")
|
||||
.arg(Arg::with_name("URL")
|
||||
.help("The download URL")
|
||||
.required(true)
|
||||
.multiple(false));
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Parse CLI arguments, from the given parent command matches.
|
||||
pub fn parse(parent: &'a ArgMatches<'a>) -> Option<CmdDownload<'a>> {
|
||||
parent.subcommand_matches("download")
|
||||
.map(|matches| CmdDownload { matches })
|
||||
}
|
||||
|
||||
/// Get the URL to download the file from.
|
||||
///
|
||||
/// This method parses the URL into an `Url`.
|
||||
/// If the given URL is invalid,
|
||||
/// the program will quit with an error message.
|
||||
pub fn url(&'a self) -> Url {
|
||||
// Get the host
|
||||
let url = self.matches.value_of("URL")
|
||||
.expect("missing URL");
|
||||
|
||||
// Parse the URL
|
||||
// TODO: improve these error messages
|
||||
match Url::parse(url) {
|
||||
Ok(url) => url,
|
||||
Err(ParseError::EmptyHost) =>
|
||||
quit_error("emtpy host given"),
|
||||
Err(ParseError::InvalidPort) =>
|
||||
quit_error("invalid host port"),
|
||||
Err(ParseError::InvalidIpv4Address) =>
|
||||
quit_error("invalid IPv4 address in host"),
|
||||
Err(ParseError::InvalidIpv6Address) =>
|
||||
quit_error("invalid IPv6 address in host"),
|
||||
Err(ParseError::InvalidDomainCharacter) =>
|
||||
quit_error("host domains contains an invalid character"),
|
||||
Err(ParseError::RelativeUrlWithoutBase) =>
|
||||
quit_error("host domain doesn't contain a host"),
|
||||
_ => quit_error("the given host is invalid"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ use super::clap::{App, ArgMatches};
|
|||
|
||||
use app::*;
|
||||
|
||||
use super::cmd_download::CmdDownload;
|
||||
use super::cmd_upload::CmdUpload;
|
||||
|
||||
/// CLI argument handler.
|
||||
|
@ -18,6 +19,7 @@ impl<'a: 'b, 'b> Handler<'a> {
|
|||
.author(APP_AUTHOR)
|
||||
.about(APP_ABOUT)
|
||||
.subcommand(CmdUpload::build().display_order(1))
|
||||
.subcommand(CmdDownload::build().display_order(2))
|
||||
}
|
||||
|
||||
/// Parse CLI arguments.
|
||||
|
@ -32,4 +34,9 @@ impl<'a: 'b, 'b> Handler<'a> {
|
|||
pub fn upload(&'a self) -> Option<CmdUpload<'a>> {
|
||||
CmdUpload::parse(&self.matches)
|
||||
}
|
||||
|
||||
/// Get the download sub command, if matched.
|
||||
pub fn download(&'a self) -> Option<CmdDownload<'a>> {
|
||||
CmdDownload::parse(&self.matches)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
extern crate clap;
|
||||
|
||||
pub mod cmd_download;
|
||||
pub mod cmd_upload;
|
||||
pub mod handler;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ mod cmd;
|
|||
mod progress;
|
||||
mod util;
|
||||
|
||||
use action::download::Download;
|
||||
use action::upload::Upload;
|
||||
use cmd::Handler;
|
||||
|
||||
|
@ -28,6 +29,11 @@ fn invoke_action(handler: &Handler) {
|
|||
return Upload::new(&cmd).invoke();
|
||||
}
|
||||
|
||||
// Match the download command
|
||||
if let Some(cmd) = handler.download() {
|
||||
return Download::new(&cmd).invoke();
|
||||
}
|
||||
|
||||
// No subcommand was selected, show general help
|
||||
Handler::build()
|
||||
.print_help()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue