Merge Send file types, rename it to RemoteFile, improve error handling
This commit is contained in:
parent
376743915d
commit
8748d79934
10 changed files with 82 additions and 163 deletions
1
IDEAS.md
1
IDEAS.md
|
@ -2,6 +2,7 @@
|
|||
- Rename DownloadFile to RemoteFile
|
||||
- Box errors
|
||||
- Info endpoint, to view file info
|
||||
- On download, mention a wrong or missing password with a HTTP 401 response
|
||||
- Automatically get owner token, from file history when setting password
|
||||
- Implement error handling everywhere properly
|
||||
- `-y` flag for assume yes
|
||||
|
|
|
@ -19,8 +19,8 @@ use crypto::b64;
|
|||
use crypto::key_set::KeySet;
|
||||
use crypto::sig::signature_encoded;
|
||||
use ext::status_code::StatusCodeExt;
|
||||
use file::file::DownloadFile;
|
||||
use file::metadata::Metadata;
|
||||
use file::remote_file::RemoteFile;
|
||||
use reader::{EncryptedFileWriter, ProgressReporter, ProgressWriter};
|
||||
|
||||
/// The name of the header that is used for the authentication nonce.
|
||||
|
@ -31,16 +31,16 @@ const FILE_EXPIRED_STATUS: StatusCode = StatusCode::NotFound;
|
|||
|
||||
/// A file upload action to a Send server.
|
||||
pub struct Download<'a> {
|
||||
/// The Send file to download.
|
||||
file: &'a DownloadFile,
|
||||
/// The remote file to download.
|
||||
file: &'a RemoteFile,
|
||||
|
||||
/// An optional password to decrypt a protected file.
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> Download<'a> {
|
||||
/// Construct a new download action for the given file.
|
||||
pub fn new(file: &'a DownloadFile, password: Option<String>) -> Self {
|
||||
/// Construct a new download action for the given remote file.
|
||||
pub fn new(file: &'a RemoteFile, password: Option<String>) -> Self {
|
||||
Self {
|
||||
file,
|
||||
password,
|
||||
|
|
|
@ -7,15 +7,15 @@ use crypto::b64;
|
|||
use crypto::key_set::KeySet;
|
||||
use crypto::sig::signature_encoded;
|
||||
use ext::status_code::StatusCodeExt;
|
||||
use file::file::DownloadFile;
|
||||
use file::remote_file::RemoteFile;
|
||||
|
||||
/// The name of the header that is used for the authentication nonce.
|
||||
const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate";
|
||||
|
||||
/// An action to change a password of an uploaded Send file.
|
||||
pub struct Password<'a> {
|
||||
/// The uploaded file to change the password for.
|
||||
file: &'a DownloadFile,
|
||||
/// The remote file to change the password for.
|
||||
file: &'a RemoteFile,
|
||||
|
||||
/// The new password to use for the file.
|
||||
password: &'a str,
|
||||
|
@ -26,9 +26,9 @@ pub struct Password<'a> {
|
|||
}
|
||||
|
||||
impl<'a> Password<'a> {
|
||||
/// Construct a new password action for the given file.
|
||||
/// Construct a new password action for the given remote file.
|
||||
pub fn new(
|
||||
file: &'a DownloadFile,
|
||||
file: &'a RemoteFile,
|
||||
password: &'a str,
|
||||
nonce: Option<Vec<u8>>,
|
||||
) -> Self {
|
||||
|
@ -143,7 +143,7 @@ struct PasswordData {
|
|||
|
||||
impl PasswordData {
|
||||
/// Create the password data object from the given key set.
|
||||
pub fn from(file: &DownloadFile, key: &KeySet)
|
||||
pub fn from(file: &RemoteFile, key: &KeySet)
|
||||
-> Result<PasswordData, PrepareError>
|
||||
{
|
||||
Ok(
|
||||
|
|
|
@ -25,7 +25,7 @@ use url::{
|
|||
use crypto::b64;
|
||||
use crypto::key_set::KeySet;
|
||||
use ext::status_code::StatusCodeExt;
|
||||
use file::file::File as SendFile;
|
||||
use file::remote_file::RemoteFile;
|
||||
use file::metadata::{Metadata, XFileMetadata};
|
||||
use reader::{
|
||||
EncryptedFileReader,
|
||||
|
@ -70,7 +70,7 @@ impl Upload {
|
|||
self,
|
||||
client: &Client,
|
||||
reporter: Arc<Mutex<ProgressReporter>>,
|
||||
) -> Result<SendFile, Error> {
|
||||
) -> Result<RemoteFile, Error> {
|
||||
// Create file data, generate a key
|
||||
let file = FileData::from(&self.path)?;
|
||||
let key = KeySet::generate(true);
|
||||
|
@ -105,7 +105,7 @@ impl Upload {
|
|||
// Change the password if set
|
||||
if let Some(password) = self.password {
|
||||
Password::new(
|
||||
&result.to_download_file(),
|
||||
&result,
|
||||
&password,
|
||||
nonce,
|
||||
).invoke(client)?;
|
||||
|
@ -218,7 +218,7 @@ impl Upload {
|
|||
/// 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, Option<Vec<u8>>), UploadError>
|
||||
-> Result<(RemoteFile, Option<Vec<u8>>), UploadError>
|
||||
{
|
||||
// Execute the request
|
||||
let mut response = match client.execute(req) {
|
||||
|
@ -285,15 +285,15 @@ impl UploadResponse {
|
|||
///
|
||||
/// The `host` and `key` must be given.
|
||||
pub fn into_file(self, host: Url, key: &KeySet)
|
||||
-> Result<SendFile, UploadError>
|
||||
-> Result<RemoteFile, UploadError>
|
||||
{
|
||||
Ok(
|
||||
SendFile::new_now(
|
||||
RemoteFile::new_now(
|
||||
self.id,
|
||||
host,
|
||||
Url::parse(&self.url)?,
|
||||
key.secret().to_vec(),
|
||||
self.owner,
|
||||
Some(self.owner),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use openssl::symm::Cipher;
|
||||
use url::Url;
|
||||
|
||||
use file::file::DownloadFile;
|
||||
use file::remote_file::RemoteFile;
|
||||
use super::{b64, rand_bytes};
|
||||
use super::hdkf::{derive_auth_key, derive_file_key, derive_meta_key};
|
||||
|
||||
|
@ -38,12 +38,11 @@ impl KeySet {
|
|||
}
|
||||
|
||||
/// Create a key set from the given file ID and secret.
|
||||
/// This method may be used to create a key set based on a Send download
|
||||
/// URL.
|
||||
/// This method may be used to create a key set based on a share URL.
|
||||
// TODO: add a parameter for the password and URL
|
||||
// TODO: return a result?
|
||||
// TODO: supply a client instance as parameter
|
||||
pub fn from(file: &DownloadFile, password: Option<&String>) -> Self {
|
||||
pub fn from(file: &RemoteFile, password: Option<&String>) -> Self {
|
||||
// Create a new key set instance
|
||||
let mut set = Self::new(
|
||||
file.secret_raw().clone(),
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
pub mod file;
|
||||
pub mod remote_file;
|
||||
pub mod metadata;
|
||||
|
|
|
@ -10,27 +10,27 @@ use self::regex::Regex;
|
|||
|
||||
use crypto::b64;
|
||||
|
||||
/// A pattern for Send download URL paths, capturing the file ID.
|
||||
/// A pattern for share URL paths, capturing the file ID.
|
||||
// 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 SHARE_PATH_PATTERN: &'static str = r"^/?download/([[:alnum:]]{8,}={0,3})/?$";
|
||||
|
||||
/// A pattern for Send download URL fragments, capturing the file secret.
|
||||
/// A pattern for share URL fragments, capturing the file secret.
|
||||
// TODO: constrain the secret length?
|
||||
const DOWNLOAD_FRAGMENT_PATTERN: &'static str = r"^([a-zA-Z0-9-_+/]+)?\s*$";
|
||||
const SHARE_FRAGMENT_PATTERN: &'static str = r"^([a-zA-Z0-9-_+/]+)?\s*$";
|
||||
|
||||
/// A struct representing an uploaded file on a Send host.
|
||||
///
|
||||
/// The struct contains the file ID, the file URL, the key that is required
|
||||
/// in combination with the file, and the owner key.
|
||||
#[derive(Debug)]
|
||||
pub struct File {
|
||||
pub struct RemoteFile {
|
||||
/// The ID of the file on that server.
|
||||
id: String,
|
||||
|
||||
/// The time the file was uploaded at.
|
||||
time: DateTime<Utc>,
|
||||
/// The time the file was uploaded at, if known.
|
||||
time: Option<DateTime<Utc>>,
|
||||
|
||||
/// The host the file was uploaded to.
|
||||
host: Url,
|
||||
|
@ -42,19 +42,18 @@ pub struct File {
|
|||
secret: Vec<u8>,
|
||||
|
||||
/// The owner key, that can be used to manage the file on the server.
|
||||
// TODO: rename this to owner token
|
||||
owner_key: String,
|
||||
owner_token: Option<String>,
|
||||
}
|
||||
|
||||
impl File {
|
||||
impl RemoteFile {
|
||||
/// Construct a new file.
|
||||
pub fn new(
|
||||
id: String,
|
||||
time: DateTime<Utc>,
|
||||
time: Option<DateTime<Utc>>,
|
||||
host: Url,
|
||||
url: Url,
|
||||
secret: Vec<u8>,
|
||||
owner_key: String,
|
||||
owner_token: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
|
@ -62,7 +61,7 @@ impl File {
|
|||
host,
|
||||
url,
|
||||
secret,
|
||||
owner_key,
|
||||
owner_token,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,115 +71,31 @@ impl File {
|
|||
host: Url,
|
||||
url: Url,
|
||||
secret: Vec<u8>,
|
||||
owner_key: String,
|
||||
owner_token: Option<String>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
id,
|
||||
Utc::now(),
|
||||
host,
|
||||
url,
|
||||
secret,
|
||||
owner_key,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: this should be removed when merging the two file types
|
||||
pub fn to_download_file(&self) -> DownloadFile {
|
||||
DownloadFile::new(
|
||||
self.id.clone(),
|
||||
self.host.clone(),
|
||||
self.url.clone(),
|
||||
self.secret.clone(),
|
||||
Some(self.owner_key.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the raw secret.
|
||||
pub fn secret_raw(&self) -> &Vec<u8> {
|
||||
// A secret must have been set
|
||||
if !self.has_secret() {
|
||||
// TODO: don't panic, return an error instead
|
||||
panic!("missing secret");
|
||||
}
|
||||
|
||||
&self.secret
|
||||
}
|
||||
|
||||
/// Get the secret as base64 encoded string.
|
||||
pub fn secret(&self) -> String {
|
||||
b64::encode(self.secret_raw())
|
||||
}
|
||||
|
||||
/// Check whether a file secret is set.
|
||||
/// This secret must be set to decrypt a downloaded Send file.
|
||||
pub fn has_secret(&self) -> bool {
|
||||
!self.secret.is_empty()
|
||||
}
|
||||
|
||||
/// Get the owner token if set.
|
||||
pub fn owner_token(&self) -> Option<&String> {
|
||||
Some(&self.owner_key)
|
||||
}
|
||||
|
||||
/// Get the download URL of the file.
|
||||
/// Set `secret` to `true`, to include it in the URL if known.
|
||||
pub fn download_url(&self, secret: bool) -> Url {
|
||||
// Get the download URL, and add the secret fragment
|
||||
let mut url = self.url.clone();
|
||||
if secret && self.has_secret() {
|
||||
url.set_fragment(Some(&self.secret()));
|
||||
} else {
|
||||
url.set_fragment(None);
|
||||
}
|
||||
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: merge this struct with `File`.
|
||||
pub struct DownloadFile {
|
||||
/// The ID of the file on that server.
|
||||
id: String,
|
||||
|
||||
/// The host the file was uploaded to.
|
||||
host: Url,
|
||||
|
||||
/// The file URL that was provided by the server.
|
||||
url: Url,
|
||||
|
||||
/// The secret key that is required to download the file.
|
||||
secret: Vec<u8>,
|
||||
|
||||
owner_token: Option<String>,
|
||||
}
|
||||
|
||||
impl DownloadFile {
|
||||
/// Construct a new instance.
|
||||
pub fn new(
|
||||
id: String,
|
||||
host: Url,
|
||||
url: Url,
|
||||
secret: Vec<u8>,
|
||||
owner_token: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
Some(Utc::now()),
|
||||
host,
|
||||
url,
|
||||
secret,
|
||||
owner_token,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Try to parse the given Send download URL.
|
||||
/// Try to parse the given share URL.
|
||||
///
|
||||
/// The given URL is matched against a Send download URL pattern,
|
||||
/// this does not check whether the host is a valid and online Send host.
|
||||
/// The given URL is matched against a share URL pattern,
|
||||
/// this does not check whether the host is a valid and online host.
|
||||
///
|
||||
/// 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: Url) -> Result<DownloadFile, FileParseError> {
|
||||
///
|
||||
/// An optional owner token may be given.
|
||||
pub fn parse_url(url: Url, owner_token: Option<String>)
|
||||
-> Result<RemoteFile, FileParseError>
|
||||
{
|
||||
// Build the host
|
||||
let mut host = url.clone();
|
||||
host.set_fragment(None);
|
||||
|
@ -188,16 +103,16 @@ impl DownloadFile {
|
|||
host.set_path("");
|
||||
|
||||
// Validate the path, get the file ID
|
||||
let re_path = Regex::new(DOWNLOAD_PATH_PATTERN).unwrap();
|
||||
let re_path = Regex::new(SHARE_PATH_PATTERN).unwrap();
|
||||
let id = re_path.captures(url.path())
|
||||
.ok_or(FileParseError::InvalidDownloadUrl)?[1]
|
||||
.ok_or(FileParseError::InvalidUrl)?[1]
|
||||
.trim()
|
||||
.to_owned();
|
||||
|
||||
// Get the file secret
|
||||
let mut secret = Vec::new();
|
||||
if let Some(fragment) = url.fragment() {
|
||||
let re_fragment = Regex::new(DOWNLOAD_FRAGMENT_PATTERN).unwrap();
|
||||
let re_fragment = Regex::new(SHARE_FRAGMENT_PATTERN).unwrap();
|
||||
if let Some(raw) = re_fragment.captures(fragment)
|
||||
.ok_or(FileParseError::InvalidSecret)?
|
||||
.get(1)
|
||||
|
@ -210,10 +125,11 @@ impl DownloadFile {
|
|||
// Construct the file
|
||||
Ok(Self::new(
|
||||
id,
|
||||
None,
|
||||
host,
|
||||
url,
|
||||
secret,
|
||||
None,
|
||||
owner_token,
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -239,12 +155,6 @@ impl DownloadFile {
|
|||
!self.secret.is_empty()
|
||||
}
|
||||
|
||||
/// Set the secret for this file.
|
||||
/// An empty vector will clear the secret.
|
||||
pub fn set_secret(&mut self, secret: Vec<u8>) {
|
||||
self.secret = secret;
|
||||
}
|
||||
|
||||
/// Get the owner token if set.
|
||||
pub fn owner_token(&self) -> Option<&String> {
|
||||
self.owner_token.as_ref()
|
||||
|
@ -255,10 +165,11 @@ impl DownloadFile {
|
|||
self.owner_token = token;
|
||||
}
|
||||
|
||||
/// Get the download URL of the file.
|
||||
/// Get the download URL of the file
|
||||
/// This URL is identical to the share URL, a term used in this API.
|
||||
/// Set `secret` to `true`, to include it in the URL if known.
|
||||
pub fn download_url(&self, secret: bool) -> Url {
|
||||
// Get the download URL, and add the secret fragment
|
||||
// Get the share URL, and add the secret fragment
|
||||
let mut url = self.url.clone();
|
||||
if secret && self.has_secret() {
|
||||
url.set_fragment(Some(&self.secret()));
|
||||
|
@ -271,7 +182,7 @@ impl DownloadFile {
|
|||
|
||||
/// Get the API metadata URL of the file.
|
||||
pub fn api_meta_url(&self) -> Url {
|
||||
// Get the download URL, and add the secret fragment
|
||||
// Get the share 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);
|
||||
|
@ -281,7 +192,7 @@ impl DownloadFile {
|
|||
|
||||
/// Get the API download URL of the file.
|
||||
pub fn api_download_url(&self) -> Url {
|
||||
// Get the download URL, and add the secret fragment
|
||||
// Get the share URL, and add the secret fragment
|
||||
let mut url = self.url.clone();
|
||||
url.set_path(format!("/api/download/{}", self.id).as_str());
|
||||
url.set_fragment(None);
|
||||
|
@ -291,7 +202,7 @@ impl DownloadFile {
|
|||
|
||||
/// Get the API password URL of the file.
|
||||
pub fn api_password_url(&self) -> Url {
|
||||
// Get the download URL, and add the secret fragment
|
||||
// Get the share URL, and add the secret fragment
|
||||
let mut url = self.url.clone();
|
||||
url.set_path(format!("/api/password/{}", self.id).as_str());
|
||||
url.set_fragment(None);
|
||||
|
@ -300,14 +211,17 @@ impl DownloadFile {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum FileParseError {
|
||||
/// An URL format error.
|
||||
UrlFormatError(UrlParseError),
|
||||
#[fail(display = "Failed to parse remote file, invalid URL format")]
|
||||
UrlFormatError(#[cause] UrlParseError),
|
||||
|
||||
/// An error for an invalid download URL format.
|
||||
InvalidDownloadUrl,
|
||||
/// An error for an invalid share URL format.
|
||||
#[fail(display = "Failed to parse remote file, invalid URL")]
|
||||
InvalidUrl,
|
||||
|
||||
/// An error for an invalid secret format, if an URL fragmet exists.
|
||||
#[fail(display = "Failed to parse remote file, invalid secret in URL")]
|
||||
InvalidSecret,
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ffsend_api::action::download::Download as ApiDownload;
|
||||
use ffsend_api::file::file::DownloadFile;
|
||||
use ffsend_api::file::remote_file::RemoteFile;
|
||||
use ffsend_api::reqwest::Client;
|
||||
|
||||
use cmd::cmd_download::CmdDownload;
|
||||
|
@ -30,10 +30,9 @@ impl<'a> Download<'a> {
|
|||
// Create a reqwest client
|
||||
let client = Client::new();
|
||||
|
||||
// Parse the file based on the URL
|
||||
// Parse the remote file based on the share URL
|
||||
// TODO: handle error here
|
||||
let file = DownloadFile::parse_url(url)
|
||||
.expect("invalid share URL, could not parse file data");
|
||||
let file = RemoteFile::parse_url(url, None)?;
|
||||
|
||||
// Create a progress bar reporter
|
||||
let bar = Arc::new(Mutex::new(ProgressBar::new_download()));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use ffsend_api::action::password::Password as ApiPassword;
|
||||
use ffsend_api::file::file::DownloadFile;
|
||||
use ffsend_api::file::remote_file::RemoteFile;
|
||||
use ffsend_api::reqwest::Client;
|
||||
|
||||
use cmd::cmd_password::CmdPassword;
|
||||
|
@ -28,15 +28,9 @@ impl<'a> Password<'a> {
|
|||
// Create a reqwest client
|
||||
let client = Client::new();
|
||||
|
||||
// Parse the file based on the URL
|
||||
// Parse the remote file based on the share URL
|
||||
// TODO: handle error here
|
||||
let mut file = DownloadFile::parse_url(url)
|
||||
.expect("invalid share URL, could not parse file data");
|
||||
|
||||
// Set the owner token
|
||||
if let Some(token) = self.cmd.owner() {
|
||||
file.set_owner_token(Some(token));
|
||||
}
|
||||
let file = RemoteFile::parse_url(url, self.cmd.owner())?;
|
||||
|
||||
// TODO: show an informative error if the owner token isn't set
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use ffsend_api::action::download::Error as DownloadError;
|
||||
use ffsend_api::action::password::Error as PasswordError;
|
||||
use ffsend_api::action::upload::Error as UploadError;
|
||||
use ffsend_api::file::remote_file::FileParseError;
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum Error {
|
||||
|
@ -29,6 +30,11 @@ pub enum ActionError {
|
|||
/// An error occurred while invoking the password action.
|
||||
#[fail(display = "Failed to change the password")]
|
||||
Password(#[cause] PasswordError),
|
||||
|
||||
/// Failed to parse a share URL, it was invalid.
|
||||
/// This error is not related to a specific action.
|
||||
#[fail(display = "Invalid share URL")]
|
||||
InvalidUrl(#[cause] FileParseError),
|
||||
}
|
||||
|
||||
impl From<DownloadError> for ActionError {
|
||||
|
@ -48,3 +54,9 @@ impl From<UploadError> for ActionError {
|
|||
ActionError::Upload(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileParseError> for ActionError {
|
||||
fn from(err: FileParseError) -> ActionError {
|
||||
ActionError::InvalidUrl(err)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue