Polish API upload action

This commit is contained in:
timvisee 2018-03-09 00:25:34 +01:00
parent c32a55c82f
commit a66b9dd67f
No known key found for this signature in database
GPG key ID: 109CBA0BF74036C2

View file

@ -1,13 +1,17 @@
use std::fs::File; use std::fs::File;
use std::io::BufReader; use std::io::{BufReader, Read};
use std::path::Path; use std::path::Path;
use mime_guess::get_mime_type; use mime_guess::{get_mime_type, Mime};
use openssl::symm::encrypt_aead; use openssl::symm::encrypt_aead;
use reqwest; use reqwest::{
Client,
Error as ReqwestError,
Request,
};
use reqwest::header::Authorization; use reqwest::header::Authorization;
use reqwest::mime::APPLICATION_OCTET_STREAM; use reqwest::mime::APPLICATION_OCTET_STREAM;
use reqwest::multipart::Part; use reqwest::multipart::{Form, Part};
use url::Url; use url::Url;
use crypto::key_set::KeySet; use crypto::key_set::KeySet;
@ -36,84 +40,163 @@ impl Upload {
} }
/// Invoke the upload action. /// Invoke the upload action.
pub fn invoke(self) -> Result<SendFile> { pub fn invoke(self, client: &Client) -> Result<SendFile> {
// Make sure the given path is a file // Create file data, generate a key
if !self.path.is_file() { let file = FileData::from(Box::new(&self.path))?;
return Err(UploadError::NotAFile);
}
// Grab some file details
let file_ext = self.path.extension().unwrap().to_str().unwrap();
let file_name = self.path.file_name().unwrap().to_str().unwrap().to_owned();
let file_mime = get_mime_type(file_ext);
// Generate a key set
let key = KeySet::generate(true); let key = KeySet::generate(true);
// Construct the metadata // Create metadata and a file reader
let metadata = Metadata::from(key.iv(), file_name.clone(), file_mime) let metadata = self.create_metadata(&key, &file)?;
.to_json() let (reader, len) = self.create_reader(&key)?;
.into_bytes();
// Encrypt the metadata, and append the tag to it // Create the request to send
let req = self.create_request(
client,
&key,
metadata,
reader,
len,
);
// Execute the request
self.execute_request(req, client, &key)
}
/// 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_tag = vec![0u8; 16];
let mut metadata = encrypt_aead( let mut metadata = match encrypt_aead(
KeySet::cipher(), KeySet::cipher(),
key.meta_key().unwrap(), key.meta_key().unwrap(),
Some(&[0u8; 12]), Some(&[0u8; 12]),
&[], &[],
&metadata, &metadata,
&mut metadata_tag, &mut metadata_tag,
).unwrap(); ) {
Ok(metadata) => metadata,
Err(_) => return Err(UploadError::EncryptionError),
};
// Append the encryption tag
metadata.append(&mut metadata_tag); metadata.append(&mut metadata_tag);
// Open the file and create an encrypted file reader Ok(metadata)
let file = File::open(&self.path).unwrap(); }
let reader = EncryptedFileReaderTagged::new(
/// Create a reader that reads the file as encrypted stream.
fn create_reader(&self, key: &KeySet)
-> Result<(BufReader<EncryptedFileReaderTagged>, u64)>
{
// Open the file
let file = match File::open(&self.path) {
Ok(file) => file,
Err(_) => return Err(UploadError::FileError),
};
// Create an encrypted reader
let reader = match EncryptedFileReaderTagged::new(
file, file,
KeySet::cipher(), KeySet::cipher(),
key.file_key().unwrap(), key.file_key().unwrap(),
key.iv(), key.iv(),
).unwrap(); ) {
Ok(reader) => reader,
Err(_) => return Err(UploadError::EncryptionError),
};
// Buffer the encrypted reader, and determine the length // Buffer the encrypted reader, and determine the length
let reader_len = reader.len().unwrap(); let len = match reader.len() {
Ok(len) => len,
Err(_) => return Err(UploadError::FileError),
};
let reader = BufReader::new(reader); let reader = BufReader::new(reader);
// Build the file part, configure the form to send Ok((reader, len))
let part = Part::reader_with_length(reader, reader_len) }
.file_name(file_name)
/// Build the request that will be send to the server.
fn create_request<R>(
&self,
client: &Client,
key: &KeySet,
metadata: Vec<u8>,
reader: R,
len: u64,
) -> Request
where
R: Read + Send + 'static
{
// Configure a form to send
let part = Part::reader_with_length(reader, len)
// .file_name(file.name())
.mime(APPLICATION_OCTET_STREAM); .mime(APPLICATION_OCTET_STREAM);
let form = reqwest::multipart::Form::new() let form = Form::new()
.part("data", part); .part("data", part);
// Create a new reqwest client // Define the URL to call
let client = reqwest::Client::new();
// Make the request
// TODO: properly format an URL here
let url = self.host.join("api/upload").expect("invalid host"); let url = self.host.join("api/upload").expect("invalid host");
let mut res = client.post(url.as_str())
.header(Authorization(format!("send-v1 {}", key.auth_key_encoded().unwrap()))) // Build the request
client.post(url.as_str())
.header(Authorization(
format!("send-v1 {}", key.auth_key_encoded().unwrap())
))
.header(XFileMetadata::from(&metadata)) .header(XFileMetadata::from(&metadata))
.multipart(form) .multipart(form)
.send() .build()
.unwrap(); .expect("failed to build an API request")
}
// Parse the response /// Execute the given request, and create a file object that represents the
let upload_res: UploadResponse = res.json().unwrap(); /// 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(UploadError::RequestError(err)),
};
// Print the response // Decode the response
Ok( let res: UploadResponse = match res.json() {
upload_res.into_file(self.host, key.secret().to_vec()) Ok(res) => res,
) Err(_) => return Err(UploadError::DecodeError),
};
// Transform the responce into a file object
Ok(res.into_file(self.host.clone(), &key))
} }
} }
/// Errors that may occur in the upload action.
pub enum UploadError { pub enum UploadError {
/// The given file is not not an existing file. /// The given file is not not an existing file.
/// Maybe it is a directory, or maybe it doesn't exist. /// Maybe it is a directory, or maybe it doesn't exist.
NotAFile, 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.
@ -125,7 +208,7 @@ pub enum UploadError {
/// The download URL can be generated using `download_url()` which will /// The download URL can be generated using `download_url()` which will
/// include the required secret in the URL. /// include the required secret in the URL.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct UploadResponse { struct UploadResponse {
/// The file ID. /// The file ID.
id: String, id: String,
@ -140,14 +223,64 @@ pub struct UploadResponse {
impl UploadResponse { impl UploadResponse {
/// Convert this response into a file object. /// Convert this response into a file object.
/// ///
/// The `host` and `secret` must be given. /// The `host` and `key` must be given.
pub fn into_file(self, host: Url, secret: Vec<u8>) -> SendFile { pub fn into_file(self, host: Url, key: &KeySet) -> SendFile {
SendFile::new_now( SendFile::new_now(
self.id, self.id,
host, host,
self.url, self.url,
secret, key.secret().to_vec(),
self.owner, self.owner,
) )
} }
} }
/// A struct that holds various file properties, such as it's name and it's
/// mime type.
struct FileData<'a> {
/// The file name.
name: &'a str,
/// The file mime type.
mime: Mime,
}
impl<'a> FileData<'a> {
/// Create a file data object, from the file at the given path.
pub fn from(path: Box<&'a Path>) -> Result<Self> {
// Make sure the given path is a file
if !path.is_file() {
return Err(UploadError::NotAFile);
}
// Get the file name
let name = match path.file_name() {
Some(name) => name.to_str().expect("failed to convert string"),
None => return Err(UploadError::FileError),
};
// Get the file extention
// TODO: handle cases where the file doesn't have an extention
let ext = match path.extension() {
Some(ext) => ext.to_str().expect("failed to convert string"),
None => return Err(UploadError::FileError),
};
Ok(
Self {
name,
mime: get_mime_type(ext),
}
)
}
/// Get the file name.
pub fn name(&self) -> &str {
self.name
}
/// Get the file mime type.
pub fn mime(&self) -> &Mime {
&self.mime
}
}