Implement failure based error handling for upload action

This commit is contained in:
timvisee 2018-03-28 16:14:44 +02:00
parent 8ea1b5971c
commit a40bcbb1e4
No known key found for this signature in database
GPG key ID: A28432A0AE6E6306
4 changed files with 190 additions and 60 deletions

View file

@ -452,7 +452,7 @@ pub enum DownloadError {
#[fail(display = "Failed to compute cryptographic signature")] #[fail(display = "Failed to compute cryptographic signature")]
ComputeSignature, ComputeSignature,
/// Sending the request to gather the metadata encryption nonce failed. /// Sending the request to download the file failed.
#[fail(display = "Failed to request file download")] #[fail(display = "Failed to request file download")]
Request, Request,
@ -494,6 +494,7 @@ pub enum FileError {
} }
/// Reqwest status code extention, to easily retrieve an error message. /// Reqwest status code extention, to easily retrieve an error message.
// TODO: implement this globally somewhere
trait StatusCodeExt { trait StatusCodeExt {
/// Build a basic error message based on the status code. /// Build a basic error message based on the status code.
fn err_text(&self) -> String; fn err_text(&self) -> String;

View file

@ -1,6 +1,10 @@
use std::fs::File; use std::fs::File;
use std::io::BufReader; use std::io::{
use std::path::{Path, PathBuf}; BufReader,
Error as IoError,
};
use std::path::PathBuf;
use std::result::Result as StdResult;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use mime_guess::{guess_mime_type, Mime}; use mime_guess::{guess_mime_type, Mime};
@ -9,11 +13,15 @@ use reqwest::{
Client, Client,
Error as ReqwestError, Error as ReqwestError,
Request, Request,
StatusCode,
}; };
use reqwest::header::Authorization; use reqwest::header::Authorization;
use reqwest::mime::APPLICATION_OCTET_STREAM; use reqwest::mime::APPLICATION_OCTET_STREAM;
use reqwest::multipart::{Form, Part}; use reqwest::multipart::{Form, Part};
use url::Url; use url::{
ParseError as UrlParseError,
Url,
};
use crypto::key_set::KeySet; use crypto::key_set::KeySet;
use reader::{ use reader::{
@ -25,6 +33,7 @@ use reader::{
use file::file::File as SendFile; use file::file::File as SendFile;
use file::metadata::{Metadata, XFileMetadata}; use file::metadata::{Metadata, XFileMetadata};
// TODO: remove these specified types
type EncryptedReader = ProgressReader<BufReader<EncryptedFileReader>>; type EncryptedReader = ProgressReader<BufReader<EncryptedFileReader>>;
pub type Result<T> = ::std::result::Result<T, UploadError>; pub type Result<T> = ::std::result::Result<T, UploadError>;
@ -51,12 +60,12 @@ impl Upload {
self, self,
client: &Client, client: &Client,
reporter: Arc<Mutex<ProgressReporter>>, reporter: Arc<Mutex<ProgressReporter>>,
) -> Result<SendFile> { ) -> StdResult<SendFile, Error> {
// Create file data, generate a key // Create file data, generate a key
let file = FileData::from(Box::new(&self.path))?; let file = FileData::from(&self.path)?;
let key = KeySet::generate(true); let key = KeySet::generate(true);
// Crpate metadata and a file reader // Create metadata and a file reader
let metadata = self.create_metadata(&key, &file)?; let metadata = self.create_metadata(&key, &file)?;
let reader = self.create_reader(&key, reporter.clone())?; let reader = self.create_reader(&key, reporter.clone())?;
let reader_len = reader.len().unwrap(); let reader_len = reader.len().unwrap();
@ -71,15 +80,16 @@ impl Upload {
// Start the reporter // Start the reporter
reporter.lock() reporter.lock()
.expect("unable to start progress, failed to get lock") .map_err(|_| UploadError::Progress)?
.start(reader_len); .start(reader_len);
// Execute the request // Execute the request
let result = self.execute_request(req, client, &key); let result = self.execute_request(req, client, &key)
.map_err(|err| err.into());
// Mark the reporter as finished // Mark the reporter as finished
reporter.lock() reporter.lock()
.expect("unable to finish progress, failed to get lock") .map_err(|_| UploadError::Progress)?
.finish(); .finish();
result result
@ -87,7 +97,7 @@ impl Upload {
/// Create a blob of encrypted metadata. /// Create a blob of encrypted metadata.
fn create_metadata(&self, key: &KeySet, file: &FileData) fn create_metadata(&self, key: &KeySet, file: &FileData)
-> Result<Vec<u8>> -> StdResult<Vec<u8>, MetaError>
{ {
// Construct the metadata // Construct the metadata
let metadata = Metadata::from( let metadata = Metadata::from(
@ -107,7 +117,7 @@ impl Upload {
&mut metadata_tag, &mut metadata_tag,
) { ) {
Ok(metadata) => metadata, Ok(metadata) => metadata,
Err(_) => return Err(UploadError::EncryptionError), Err(_) => return Err(MetaError::Encrypt),
}; };
// Append the encryption tag // Append the encryption tag
@ -121,11 +131,11 @@ impl Upload {
&self, &self,
key: &KeySet, key: &KeySet,
reporter: Arc<Mutex<ProgressReporter>>, reporter: Arc<Mutex<ProgressReporter>>,
) -> Result<EncryptedReader> { ) -> StdResult<EncryptedReader, Error> {
// Open the file // Open the file
let file = match File::open(self.path.as_path()) { let file = match File::open(self.path.as_path()) {
Ok(file) => file, Ok(file) => file,
Err(_) => return Err(UploadError::FileError), Err(err) => return Err(FileError::Open(err).into()),
}; };
// Create an encrypted reader // Create an encrypted reader
@ -136,7 +146,7 @@ impl Upload {
key.iv(), key.iv(),
) { ) {
Ok(reader) => reader, Ok(reader) => reader,
Err(_) => return Err(UploadError::EncryptionError), Err(_) => return Err(ReaderError::Encrypt.into()),
}; };
// Buffer the encrypted reader // Buffer the encrypted reader
@ -144,7 +154,7 @@ impl Upload {
// Wrap into the encrypted reader // Wrap into the encrypted reader
let mut reader = ProgressReader::new(reader) let mut reader = ProgressReader::new(reader)
.expect("failed to create progress reader"); .map_err(|_| ReaderError::Progress)?;
// Initialize and attach the reporter // Initialize and attach the reporter
reader.set_reporter(reporter); reader.set_reporter(reporter);
@ -165,13 +175,15 @@ impl Upload {
// Configure a form to send // Configure a form to send
let part = Part::reader_with_length(reader, len) let part = Part::reader_with_length(reader, len)
// .file_name(file.name()) // TODO: keep this here? .file_name(file.name())
.mime(APPLICATION_OCTET_STREAM); .mime(APPLICATION_OCTET_STREAM);
let form = Form::new() let form = Form::new()
.part("data", part); .part("data", part);
// Define the URL to call // Define the URL to call
let url = self.host.join("api/upload").expect("invalid host"); // TODO: create an error for this unwrap
let url = self.host.join("api/upload")
.expect("invalid host");
// Build the request // Build the request
client.post(url.as_str()) client.post(url.as_str())
@ -187,46 +199,35 @@ impl Upload {
/// Execute the given request, and create a file object that represents the /// Execute the given request, and create a file object that represents the
/// uploaded file. /// uploaded file.
fn execute_request(&self, req: Request, client: &Client, key: &KeySet) fn execute_request(&self, req: Request, client: &Client, key: &KeySet)
-> Result<SendFile> -> StdResult<SendFile, UploadError>
{ {
// Execute the request // Execute the request
let mut res = match client.execute(req) { let mut response = match client.execute(req) {
Ok(res) => res, Ok(response) => response,
Err(err) => return Err(UploadError::RequestError(err)), // TODO: attach the error context
Err(_) => return Err(UploadError::Request),
}; };
// Validate the status code
let status = response.status();
if !status.is_success() {
return Err(
UploadError::RequestStatus(status, status.err_text())
);
}
// Decode the response // Decode the response
let res: UploadResponse = match res.json() { let response: UploadResponse = match response.json() {
Ok(res) => res, Ok(response) => response,
Err(_) => return Err(UploadError::DecodeError), Err(err) => return Err(UploadError::Decode(err)),
}; };
// Transform the responce into a file object // Transform the responce into a file object
Ok(res.into_file(self.host.clone(), &key)) // TODO: do some error handling in this into_file method
Ok(response.into_file(self.host.clone(), &key)?)
} }
} }
/// Errors that may occur in the upload action.
#[derive(Debug)]
pub enum UploadError {
/// The given file is not not an existing file.
/// Maybe it is a directory, or maybe it doesn't exist.
NotAFile,
/// An error occurred while opening or reading a file.
FileError,
/// An error occurred while encrypting the file.
EncryptionError,
/// An error occurred while while processing the request.
/// This also covers things like HTTP 404 errors.
RequestError(ReqwestError),
/// An error occurred while decoding the response data.
DecodeError,
}
/// The response from the server after a file has been uploaded. /// The response from the server after a file has been uploaded.
/// This response contains the file ID and owner key, to manage the file. /// This response contains the file ID and owner key, to manage the file.
/// ///
@ -252,14 +253,18 @@ impl UploadResponse {
/// Convert this response into a file object. /// Convert this response into a file object.
/// ///
/// The `host` and `key` must be given. /// The `host` and `key` must be given.
pub fn into_file(self, host: Url, key: &KeySet) -> SendFile { pub fn into_file(self, host: Url, key: &KeySet)
SendFile::new_now( -> StdResult<SendFile, UploadError>
self.id, {
host, Ok(
Url::parse(&self.url) SendFile::new_now(
.expect("upload response URL parse error"), self.id,
key.secret().to_vec(), host,
self.owner, Url::parse(&self.url)
.map_err(|err| UploadError::ParseUrl(err))?,
key.secret().to_vec(),
self.owner,
)
) )
} }
} }
@ -276,10 +281,10 @@ struct FileData<'a> {
impl<'a> FileData<'a> { impl<'a> FileData<'a> {
/// Create a file data object, from the file at the given path. /// Create a file data object, from the file at the given path.
pub fn from(path: Box<&'a Path>) -> Result<Self> { pub fn from(path: &'a PathBuf) -> StdResult<Self, FileError> {
// Make sure the given path is a file // Make sure the given path is a file
if !path.is_file() { if !path.is_file() {
return Err(UploadError::NotAFile); return Err(FileError::NotAFile);
} }
// Get the file name // Get the file name
@ -306,3 +311,126 @@ impl<'a> FileData<'a> {
&self.mime &self.mime
} }
} }
#[derive(Fail, Debug)]
pub enum Error {
/// An error occurred while preparing a file for uploading.
#[fail(display = "Failed to prepare uploading the file")]
Prepare(#[cause] PrepareError),
/// An error occurred while opening, reading or using the file that
/// the should be uploaded.
// TODO: maybe append the file path here for further information
#[fail(display = "Failed to use the file to upload")]
File(#[cause] FileError),
/// An error occurred while uploading the file.
#[fail(display = "Failed to upload the file")]
Upload(#[cause] UploadError),
}
impl From<MetaError> for Error {
fn from(err: MetaError) -> Error {
Error::Prepare(PrepareError::Meta(err))
}
}
impl From<FileError> for Error {
fn from(err: FileError) -> Error {
Error::File(err)
}
}
impl From<ReaderError> for Error {
fn from(err: ReaderError) -> Error {
Error::Prepare(PrepareError::Reader(err))
}
}
impl From<UploadError> for Error {
fn from(err: UploadError) -> Error {
Error::Upload(err)
}
}
#[derive(Fail, Debug)]
pub enum PrepareError {
/// Failed to prepare the file metadata for uploading.
#[fail(display = "Failed to prepare file metadata")]
Meta(#[cause] MetaError),
/// Failed to create an encrypted file reader, that encrypts
/// the file on the fly when it is read.
#[fail(display = "Failed to access the file to upload")]
Reader(#[cause] ReaderError),
}
#[derive(Fail, Debug)]
pub enum MetaError {
/// An error occurred while encrypting the file metadata.
#[fail(display = "Failed to encrypt file metadata")]
Encrypt,
}
#[derive(Fail, Debug)]
pub enum ReaderError {
/// An error occurred while creating the file encryptor.
#[fail(display = "Failed to create file encryptor")]
Encrypt,
/// Failed to create the progress reader, attached to the file reader,
/// to measure the uploading progress.
#[fail(display = "Failed to create progress reader")]
Progress,
}
#[derive(Fail, Debug)]
pub enum FileError {
/// The given path, is not not a file or doesn't exist.
#[fail(display = "The path is not an existing file")]
NotAFile,
/// Failed to open the file that must be uploaded for reading.
#[fail(display = "Failed to open the file to upload")]
Open(#[cause] IoError),
}
#[derive(Fail, Debug)]
pub enum UploadError {
/// Failed to start or update the uploading progress, because of this the
/// upload can't continue.
#[fail(display = "Failed to update upload progress")]
Progress,
/// Sending the request to upload the file failed.
#[fail(display = "Failed to request file upload")]
Request,
/// The response for downloading the indicated an error and wasn't successful.
#[fail(display = "Bad HTTP response '{}' while requesting file upload", _1)]
RequestStatus(StatusCode, String),
/// Decoding the upload response from the server.
/// Maybe the server responded with data from a newer API version.
#[fail(display = "Failed to decode upload response")]
Decode(#[cause] ReqwestError),
/// Failed to parse the retrieved URL from the upload response.
#[fail(display = "Failed to parse received URL")]
ParseUrl(#[cause] UrlParseError),
}
/// Reqwest status code extention, to easily retrieve an error message.
// TODO: implement this globally somewhere
trait StatusCodeExt {
/// Build a basic error message based on the status code.
fn err_text(&self) -> String;
}
impl StatusCodeExt for StatusCode {
fn err_text(&self) -> String {
self.canonical_reason()
.map(|text| text.to_owned())
.unwrap_or(format!("{}", self.as_u16()))
}
}

View file

@ -38,8 +38,8 @@ impl<'a> Upload<'a> {
let bar = Arc::new(Mutex::new(ProgressBar::new_upload())); let bar = Arc::new(Mutex::new(ProgressBar::new_upload()));
// Execute an upload action // Execute an upload action
// TODO: do not unwrap, but return an error let file = ApiUpload::new(host, path).invoke(&client, bar)
let file = ApiUpload::new(host, path).invoke(&client, bar).unwrap(); .map_err(|err| ActionError::Upload(err))?;
// Get the download URL, and report it in the console // Get the download URL, and report it in the console
let url = file.download_url(true); let url = file.download_url(true);

View file

@ -1,4 +1,5 @@
use ffsend_api::action::download::Error as DownloadError; use ffsend_api::action::download::Error as DownloadError;
use ffsend_api::action::upload::Error as UploadError;
#[derive(Fail, Debug)] #[derive(Fail, Debug)]
pub enum Error { pub enum Error {
@ -12,7 +13,7 @@ pub enum ActionError {
/// An error occurred while invoking the upload action. /// An error occurred while invoking the upload action.
// TODO: bind the upload cause here // TODO: bind the upload cause here
#[fail(display = "Failed to upload the specified file")] #[fail(display = "Failed to upload the specified file")]
Upload, Upload(#[cause] UploadError),
/// An error occurred while invoking the download action. /// An error occurred while invoking the download action.
#[fail(display = "Failed to download the requested file")] #[fail(display = "Failed to download the requested file")]