Procházet zdrojové kódy

Start implementing an upload progress bar (WIP)

timvisee před 7 roky
rodič
revize
030454cc10
7 změnil soubory, kde provedl 242 přidání a 34 odebrání
  1. 13 0
      Cargo.lock
  2. 44 24
      api/src/action/upload.rs
  3. 129 9
      api/src/reader.rs
  4. 1 0
      cli/Cargo.toml
  5. 6 1
      cli/src/action/upload.rs
  6. 1 0
      cli/src/main.rs
  7. 48 0
      cli/src/progress.rs

+ 13 - 0
Cargo.lock

@@ -256,6 +256,7 @@ dependencies = [
  "clipboard 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "ffsend-api 0.1.0",
  "open 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pbr 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -644,6 +645,17 @@ dependencies = [
  "vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "pbr"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "percent-encoding"
 version = "1.0.1"
@@ -1228,6 +1240,7 @@ dependencies = [
 "checksum openssl 0.10.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1636c9f1d78af9cbcc50e523bfff4a30274108aad5e86761afd4d31e4e184fa7"
 "checksum openssl 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)" = "a3605c298474a3aa69de92d21139fb5e2a81688d308262359d85cdd0d12a7985"
 "checksum openssl-sys 0.9.27 (registry+https://github.com/rust-lang/crates.io-index)" = "d6fdc5c4a02e69ce65046f1763a0181107038e02176233acb0b3351d7cc588f9"
+"checksum pbr 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e048e3afebb6c454bb1c5d0fe73fda54698b4715d78ed8e7302447c37736d23a"
 "checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
 "checksum phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "cb325642290f28ee14d8c6201159949a872f220c62af6e110a56ea914fbe42fc"
 "checksum phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d62594c0bb54c464f633175d502038177e90309daf2e0158be42ed5f023ce88f"

+ 44 - 24
api/src/action/upload.rs

@@ -1,5 +1,5 @@
 use std::fs::File;
-use std::io::{BufReader, Read};
+use std::io::BufReader;
 use std::path::{Path, PathBuf};
 
 use mime_guess::{get_mime_type, Mime};
@@ -15,10 +15,17 @@ use reqwest::multipart::{Form, Part};
 use url::Url;
 
 use crypto::key_set::KeySet;
-use reader::EncryptedFileReaderTagged;
+use reader::{
+    EncryptedFileReaderTagged,
+    ExactLengthReader,
+    ProgressReader,
+    ProgressReporter,
+};
 use file::file::File as SendFile;
 use file::metadata::{Metadata, XFileMetadata};
 
+type EncryptedReader =
+    ProgressReader<'static, BufReader<EncryptedFileReaderTagged>>;
 pub type Result<T> = ::std::result::Result<T, UploadError>;
 
 /// A file upload action to a Send server.
@@ -40,14 +47,19 @@ impl Upload {
     }
 
     /// Invoke the upload action.
-    pub fn invoke(self, client: &Client) -> Result<SendFile> {
+    pub fn invoke(
+        self,
+        client: &Client,
+        reporter: Box<ProgressReporter + 'static>,
+    ) -> Result<SendFile> {
         // Create file data, generate a key
         let file = FileData::from(Box::new(&self.path))?;
         let key = KeySet::generate(true);
 
-        // Create metadata and a file reader
+        // Crpate metadata and a file reader
         let metadata = self.create_metadata(&key, &file)?;
-        let (reader, len) = self.create_reader(&key)?;
+        // TODO: do not use leak, as it might cause memory leaks
+        let reader = self.create_reader(&key, Box::leak(reporter))?;
 
         // Create the request to send
         let req = self.create_request(
@@ -55,11 +67,14 @@ impl Upload {
             &key,
             metadata,
             reader,
-            len,
         );
 
         // Execute the request
-        self.execute_request(req, client, &key)
+        let result = self.execute_request(req, client, &key);
+
+        // TODO: finish the progress bar
+
+        result
     }
 
     /// Create a blob of encrypted metadata.
@@ -71,7 +86,7 @@ impl Upload {
             key.iv(),
             file.name().to_owned(),
             file.mime().clone(),
-        ) .to_json().into_bytes();
+        ).to_json().into_bytes();
 
         // Encrypt the metadata
         let mut metadata_tag = vec![0u8; 16];
@@ -94,9 +109,11 @@ impl Upload {
     }
 
     /// Create a reader that reads the file as encrypted stream.
-    fn create_reader(&self, key: &KeySet)
-        -> Result<(BufReader<EncryptedFileReaderTagged>, u64)>
-    {
+    fn create_reader(
+        &self,
+        key: &KeySet,
+        reporter: &'static mut ProgressReporter,
+    ) -> Result<EncryptedReader> {
         // Open the file
         let file = match File::open(self.path.as_path()) {
             Ok(file) => file,
@@ -114,28 +131,31 @@ impl Upload {
             Err(_) => return Err(UploadError::EncryptionError),
         };
 
-        // Buffer the encrypted reader, and determine the length
-        let len = match reader.len() {
-            Ok(len) => len,
-            Err(_) => return Err(UploadError::FileError),
-        };
+        // Buffer the encrypted reader
         let reader = BufReader::new(reader);
 
-        Ok((reader, len))
+        // Wrap into the encrypted reader
+        let mut reader = ProgressReader::new(reader)
+            .expect("failed to create progress reader");
+
+        // Initialize and attach the reporter
+        reporter.start(reader.len().unwrap());
+        reader.set_reporter(&mut *reporter);
+
+        Ok(reader)
     }
 
     /// Build the request that will be send to the server.
-    fn create_request<R>(
+    fn create_request(
         &self,
         client: &Client,
         key: &KeySet,
         metadata: Vec<u8>,
-        reader: R,
-        len: u64,
-    ) -> Request
-        where
-            R: Read + Send + 'static
-    {
+        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())

+ 129 - 9
api/src/reader.rs

@@ -1,6 +1,12 @@
 use std::cmp::min;
 use std::fs::File;
-use std::io::{self, Cursor, Read};
+use std::io::{
+    self,
+    BufReader,
+    Cursor,
+    Error as IoError,
+    Read,
+};
 
 use openssl::symm::{
     Cipher,
@@ -75,14 +81,6 @@ impl EncryptedFileReaderTagged {
         )
     }
 
-    /// Calculate the total length of the encrypted file with the appended
-    /// tag.
-    /// Useful in combination with some progress monitor, to determine how much
-    /// of the file is read or for example; sent over the network.
-    pub fn len(&self) -> Result<u64, io::Error> {
-        Ok(self.file.metadata()?.len() + TAG_LEN as u64)
-    }
-
     /// Read data from the internal buffer if there is any data in it, into
     /// the given `buf`.
     ///
@@ -181,6 +179,16 @@ impl EncryptedFileReaderTagged {
     }
 }
 
+impl ExactLengthReader for EncryptedFileReaderTagged {
+    /// Calculate the total length of the encrypted file with the appended
+    /// tag.
+    /// Useful in combination with some progress monitor, to determine how much
+    /// of the file is read or for example; sent over the network.
+    fn len(&self) -> Result<u64, io::Error> {
+        Ok(self.file.metadata()?.len() + TAG_LEN as u64)
+    }
+}
+
 /// The reader trait implementation.
 impl Read for EncryptedFileReaderTagged {
     /// Read from the encrypted file, and then the encryption tag.
@@ -218,3 +226,115 @@ impl Read for EncryptedFileReaderTagged {
 
 // TODO: implement this some other way
 unsafe impl Send for EncryptedFileReaderTagged {}
+
+/// A reader wrapper, that measures the reading process for a reader with a
+/// known length.
+///
+/// If the reader exceeds the initially specified length,
+/// the reader will continue to allow reads.
+/// The length property will grow accordingly.
+///
+/// The reader will only start producing `None` if the wrapped reader is doing
+/// so.
+pub struct ProgressReader<'a, R> {
+    /// The wrapped reader.
+    inner: R,
+
+    /// The total length of the reader.
+    len: u64,
+
+    /// The current reading progress.
+    progress: u64,
+
+    /// A reporter, to report the progress status to.
+    reporter: Option<&'a mut ProgressReporter>,
+}
+
+impl<'a, R: Read> ProgressReader<'a, R> {
+    /// Wrap the given reader with an exact length, in a progress reader.
+    pub fn new(inner: R) -> Result<Self, IoError>
+        where
+            R: ExactLengthReader
+    {
+        Ok(
+            Self {
+                len: inner.len()?,
+                inner,
+                progress: 0,
+                reporter: None,
+            }
+        )
+    }
+
+    /// Wrap the given reader with the given length in a progress reader.
+    pub fn from(inner: R, len: u64) -> Self {
+        Self {
+            inner,
+            len,
+            progress: 0,
+            reporter: None,
+        }
+    }
+
+    /// Set the reporter to report the status to.
+    pub fn set_reporter(&mut self, reporter: &'a mut ProgressReporter) {
+        self.reporter = Some(reporter);
+    }
+
+    /// Get the current progress.
+    pub fn progress(&self) -> u64 {
+        self.progress
+    }
+}
+
+impl<'a, R: Read> Read for ProgressReader<'a, R> {
+    /// Read from the encrypted file, and then the encryption tag.
+    fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> {
+        // Read from the wrapped reader, increase the progress
+        let len = self.inner.read(buf)?;
+        self.progress += len as u64;
+
+        // Keep the specified length in-bound
+        if self.progress > self.len {
+            self.len = self.progress;
+        }
+
+        // Report
+        if let Some(reporter) = self.reporter.as_mut() {
+            reporter.progress(self.progress);
+        }
+
+        Ok(len)
+    }
+}
+
+impl<'a, R: Read> ExactLengthReader for ProgressReader<'a, R> {
+    // Return the specified length.
+    fn len(&self) -> Result<u64, io::Error> {
+        Ok(self.len)
+    }
+}
+
+/// A progress reporter.
+pub trait ProgressReporter: Send {
+    /// Start the progress with the given total.
+    fn start(&mut self, total: u64);
+
+    /// A progress update.
+    fn progress(&mut self, progress: u64);
+
+    /// Finish the progress.
+    fn finish(&mut self);
+}
+
+/// A trait for readers, to get the exact length of a reader.
+pub trait ExactLengthReader: Read {
+    /// Get the exact length of the reader in bytes.
+    fn len(&self) -> Result<u64, io::Error>;
+}
+
+impl<R: ExactLengthReader> ExactLengthReader for BufReader<R> {
+    fn len(&self) -> Result<u64, io::Error> {
+        self.get_ref().len()
+    }
+}

+ 1 - 0
cli/Cargo.toml

@@ -16,3 +16,4 @@ clap = "2.31"
 clipboard = { version = "0.4", optional = true }
 ffsend-api = { version = "*", path = "../api" }
 open = "1"
+pbr = "1"

+ 6 - 1
cli/src/action/upload.rs

@@ -1,9 +1,11 @@
 use std::path::Path;
+use std::sync::Arc;
 
 use ffsend_api::action::upload::Upload as ApiUpload;
 use ffsend_api::reqwest::Client;
 
 use cmd::cmd_upload::CmdUpload;
+use progress::ProgressBar;
 use util::open_url;
 #[cfg(feature = "clipboard")]
 use util::set_clipboard;
@@ -31,9 +33,12 @@ impl<'a> Upload<'a> {
         // Create a reqwest client
         let client = Client::new();
 
+        // Create a progress bar reporter
+        let bar = Box::new(ProgressBar::new());
+
         // Execute an upload action
         // TODO: do not unwrap, but return an error
-        let file = ApiUpload::new(host, path).invoke(&client).unwrap();
+        let file = ApiUpload::new(host, path).invoke(&client, bar).unwrap();
 
         // Get the download URL, and report it in the console
         let url = file.download_url();

+ 1 - 0
cli/src/main.rs

@@ -3,6 +3,7 @@ extern crate ffsend_api;
 mod action;
 mod app;
 mod cmd;
+mod progress;
 mod util;
 
 use action::upload::Upload;

+ 48 - 0
cli/src/progress.rs

@@ -0,0 +1,48 @@
+extern crate pbr;
+
+use std::io::Stdout;
+
+use ffsend_api::reader::ProgressReporter;
+use self::pbr::{
+    ProgressBar as Pbr,
+    Units,
+};
+
+/// A progress bar reporter.
+pub struct ProgressBar {
+    bar: Option<Pbr<Stdout>>,
+}
+
+impl ProgressBar {
+    pub fn new() -> Self {
+        Self {
+            bar: None,
+        }
+    }
+}
+
+impl ProgressReporter for ProgressBar {
+    /// Start the progress with the given total.
+    fn start(&mut self, total: u64) {
+        // Initialize the progress bar
+        let mut bar = Pbr::new(total);
+        bar.set_units(Units::Bytes);
+
+        self.bar = Some(bar);
+    }
+
+    /// A progress update.
+    fn progress(&mut self, progress: u64) {
+        self.bar.as_mut()
+            .expect("progress bar not yet instantiated, cannot set progress")
+            .set(progress);
+    }
+
+    /// Finish the progress.
+    fn finish(&mut self) {
+        self.bar.as_mut()
+            .expect("progress bar not yet instantiated")
+            // TODO: print a proper message here
+            .finish_print("DONE");
+    }
+}