فهرست منبع

Polish API upload action

timvisee 7 سال پیش
والد
کامیت
a66b9dd67f
1فایلهای تغییر یافته به همراه182 افزوده شده و 49 حذف شده
  1. 182 49
      api/src/action/upload.rs

+ 182 - 49
api/src/action/upload.rs

@@ -1,13 +1,17 @@
 use std::fs::File;
-use std::io::BufReader;
+use std::io::{BufReader, Read};
 use std::path::Path;
 
-use mime_guess::get_mime_type;
+use mime_guess::{get_mime_type, Mime};
 use openssl::symm::encrypt_aead;
-use reqwest;
+use reqwest::{
+    Client, 
+    Error as ReqwestError,
+    Request,
+};
 use reqwest::header::Authorization;
 use reqwest::mime::APPLICATION_OCTET_STREAM;
-use reqwest::multipart::Part;
+use reqwest::multipart::{Form, Part};
 use url::Url;
 
 use crypto::key_set::KeySet;
@@ -36,84 +40,163 @@ impl Upload {
     }
 
     /// Invoke the upload action.
-    pub fn invoke(self) -> Result<SendFile> {
-        // Make sure the given path is a file
-        if !self.path.is_file() {
-            return Err(UploadError::NotAFile);
-        }
+    pub fn invoke(self, client: &Client) -> Result<SendFile> {
+        // Create file data, generate a key
+        let file = FileData::from(Box::new(&self.path))?;
+        let key = KeySet::generate(true);
 
-        // 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);
+        // Create metadata and a file reader
+        let metadata = self.create_metadata(&key, &file)?;
+        let (reader, len) = self.create_reader(&key)?;
 
-        // Generate a key set
-        let key = KeySet::generate(true);
+        // 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.clone(), file_mime)
-            .to_json()
-            .into_bytes();
+        let metadata = Metadata::from(
+            key.iv(),
+            file.name().to_owned(),
+            file.mime().clone(),
+        ) .to_json().into_bytes();
 
-        // Encrypt the metadata, and append the tag to it
+        // Encrypt the metadata
         let mut metadata_tag = vec![0u8; 16];
-        let mut metadata = encrypt_aead(
+        let mut metadata = match encrypt_aead(
             KeySet::cipher(),
             key.meta_key().unwrap(),
             Some(&[0u8; 12]),
             &[],
             &metadata,
             &mut metadata_tag,
-        ).unwrap();
+        ) {
+            Ok(metadata) => metadata,
+            Err(_) => return Err(UploadError::EncryptionError),
+        };
+
+        // Append the encryption tag
         metadata.append(&mut metadata_tag);
 
-        // Open the file and create an encrypted file reader
-        let file = File::open(&self.path).unwrap();
-        let reader = EncryptedFileReaderTagged::new(
+        Ok(metadata)
+    }
+
+    /// 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,
             KeySet::cipher(),
             key.file_key().unwrap(),
             key.iv(),
-        ).unwrap();
+        ) {
+            Ok(reader) => reader,
+            Err(_) => return Err(UploadError::EncryptionError),
+        };
 
         // 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);
 
-        // Build the file part, configure the form to send
-        let part = Part::reader_with_length(reader, reader_len)
-            .file_name(file_name)
+        Ok((reader, len))
+    }
+
+    /// 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);
-        let form = reqwest::multipart::Form::new()
+        let form = Form::new()
             .part("data", part);
 
-        // Create a new reqwest client
-        let client = reqwest::Client::new();
-
-        // Make the request
-        // TODO: properly format an URL here
+        // Define the URL to call
         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))
             .multipart(form)
-            .send()
-            .unwrap();
+            .build()
+            .expect("failed to build an API request")
+    }
 
-        // Parse the response
-        let upload_res: UploadResponse = res.json().unwrap();
+    /// 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(UploadError::RequestError(err)),
+        };
 
-        // Print the response
-        Ok(
-            upload_res.into_file(self.host, key.secret().to_vec())
-        )
+        // Decode the response
+        let res: UploadResponse = match res.json() {
+            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 {
     /// 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.
@@ -125,7 +208,7 @@ pub enum UploadError {
 /// The download URL can be generated using `download_url()` which will
 /// include the required secret in the URL.
 #[derive(Debug, Deserialize)]
-pub struct UploadResponse {
+struct UploadResponse {
     /// The file ID.
     id: String,
 
@@ -140,14 +223,64 @@ pub struct UploadResponse {
 impl UploadResponse {
     /// Convert this response into a file object.
     ///
-    /// The `host` and `secret` must be given.
-    pub fn into_file(self, host: Url, secret: Vec<u8>) -> SendFile {
+    /// The `host` and `key` must be given.
+    pub fn into_file(self, host: Url, key: &KeySet) -> SendFile {
         SendFile::new_now(
             self.id,
             host,
             self.url,
-            secret,
+            key.secret().to_vec(),
             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
+    }
+}