Explorar o código

Allow specifying an output file or directory when downloading [WIP]

timvisee %!s(int64=7) %!d(string=hai) anos
pai
achega
8259106c17
Modificáronse 5 ficheiros con 109 adicións e 22 borrados
  1. 11 4
      IDEAS.md
  2. 69 16
      api/src/action/download.rs
  3. 5 0
      api/src/file/metadata.rs
  4. 8 2
      cli/src/action/download.rs
  5. 16 0
      cli/src/cmd/cmd_download.rs

+ 11 - 4
IDEAS.md

@@ -1,12 +1,15 @@
 # Ideas
-- Rename DownloadFile to RemoteFile
+- custom file name when uploading
+- allow creating non existent directories with the `-f` flag 
+- only allow file extension renaming on upload with `-f` flag
+- no interact flag
+- `-y` flag for assume yes
+- `-f` flag for forcing (no interact?)
 - 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
-- `-f` flag for forcing (no interact?)
 - Quick upload/download without `upload` or `download` subcommands.
 - Set file password
 - Set file download count
@@ -33,4 +36,8 @@
 - Ubuntu PPA package
 - Move API URL generator methods out of remote file class
 - Prompt if a file download password is required
-- Do not allow empty passwords (must force with `-f`)
+- Do not allow empty passwords (must force with `-f`) (as not usable on web)
+- Must use `-f` to overwrite existing file
+- Rename host to server?
+- Read and write files from and to stdin and stdout with `-` as file
+- Ask to add MIME extension to downloaded files without one on Windows

+ 69 - 16
api/src/action/download.rs

@@ -6,6 +6,7 @@ use std::io::{
     Error as IoError,
     Read,
 };
+use std::path::PathBuf;
 use std::sync::{Arc, Mutex};
 
 use failure::Error as FailureError;
@@ -34,15 +35,23 @@ pub struct Download<'a> {
     /// The remote file to download.
     file: &'a RemoteFile,
 
+    /// The target file or directory, to download the file to.
+    target: PathBuf,
+
     /// An optional password to decrypt a protected file.
     password: Option<String>,
 }
 
 impl<'a> Download<'a> {
     /// Construct a new download action for the given remote file.
-    pub fn new(file: &'a RemoteFile, password: Option<String>) -> Self {
+    pub fn new(
+        file: &'a RemoteFile,
+        target: PathBuf,
+        password: Option<String>,
+    ) -> Self {
         Self {
             file,
+            target,
             password,
         }
     }
@@ -59,15 +68,25 @@ impl<'a> Download<'a> {
         // Fetch the authentication nonce
         let auth_nonce = self.fetch_auth_nonce(client)?;
 
-        // Fetch the meta nonce, set the input vector
-        let meta_nonce = self.fetch_meta_nonce(&client, &mut key, auth_nonce)?;
+        // Fetch the meta data, apply the derived input vector
+        let (metadata, meta_nonce) = self.fetch_metadata_apply_iv(
+            &client,
+            &mut key,
+            auth_nonce,
+        )?;
+
+        // Decide what actual file target to use
+        let path = self.decide_path(metadata.name());
+        let path_str = path.to_str().unwrap_or("?").to_owned();
 
         // Open the file we will write to
         // TODO: this should become a temporary file first
         // TODO: use the uploaded file name as default
-        let path = "downloaded.zip";
         let out = File::create(path)
-            .map_err(|err| Error::File(path.into(), FileError::Create(err)))?;
+            .map_err(|err| Error::File(
+                path_str.clone(),
+                FileError::Create(err),
+            ))?;
 
         // Create the file reader for downloading
         let (reader, len) = self.create_file_reader(&key, meta_nonce, &client)?;
@@ -78,7 +97,7 @@ impl<'a> Download<'a> {
             len,
             &key,
             reporter.clone(),
-        ).map_err(|err| Error::File(path.into(), err))?;
+        ).map_err(|err| Error::File(path_str.clone(), err))?;
 
         // Download the file
         self.download(reader, writer, len, reporter)?;
@@ -127,24 +146,29 @@ impl<'a> Download<'a> {
         ).map_err(|_| AuthError::MalformedNonce.into())
     }
 
-    /// Fetch the metadata nonce.
-    /// This method also sets the input vector on the given key set,
-    /// extracted from the metadata.
+
+    /// Create a metadata nonce, and fetch the metadata for the file from the
+    /// server.
     ///
     /// The key set, along with the authentication nonce must be given.
-    /// The meta nonce is returned.
-    fn fetch_meta_nonce(
+    ///
+    /// The metadata, with the meta nonce is returned.
+    ///
+    /// This method is similar to `fetch_metadata`, and additionally applies
+    /// the derived input vector to the given key set.
+    fn fetch_metadata_apply_iv(
         &self,
         client: &Client,
         key: &mut KeySet,
         auth_nonce: Vec<u8>,
-    ) -> Result<Vec<u8>, MetaError> {
+    ) -> Result<(Metadata, Vec<u8>), MetaError> {
         // Fetch the metadata and the nonce
-        let (metadata, meta_nonce) = self.fetch_metadata(client, key, auth_nonce)?;
+        let data = self.fetch_metadata(client, key, auth_nonce)?;
 
-        // Set the input vector, and return the nonce
-        key.set_iv(metadata.iv());
-        Ok(meta_nonce)
+        // Set the input vector bas
+        key.set_iv(data.0.iv());
+
+        Ok(data)
     }
 
     /// Create a metadata nonce, and fetch the metadata for the file from the
@@ -203,6 +227,35 @@ impl<'a> Download<'a> {
         ))
     }
 
+    /// Decide what path we will download the file to.
+    ///
+    /// A target file or directory, and a file name hint must be given.
+    /// The name hint can be derived from the retrieved metadata on this file.
+    ///
+    /// The name hint is used as file name, if a directory was given.
+    fn decide_path(&self, name_hint: &str) -> PathBuf {
+        // Return the target if it is an existing file
+        if self.target.is_file() {
+            return self.target.clone();
+        }
+
+        // Append the name hint if this is a directory
+        if self.target.is_dir() {
+            return self.target.join(name_hint);
+        }
+
+        // Return if the parent is an existing directory
+        if self.target.parent().map(|p| p.is_dir()).unwrap_or(false) {
+            return self.target.clone();
+        }
+
+        // TODO: canonicalize the path when possible
+        // TODO: allow using `file.toml` as target without directory indication
+        // TODO: return a nice error here as the path may be invalid
+        // TODO: maybe prompt the user to create the directory
+        panic!("Invalid (non-existing) output path given, not yet supported");
+    }
+
     /// Make a download request, and create a reader that downloads the
     /// encrypted file.
     ///

+ 5 - 0
api/src/file/metadata.rs

@@ -47,6 +47,11 @@ impl Metadata {
         serde_json::to_string(&self).unwrap()
     }
 
+    /// Get the file name.
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+
     /// Get the input vector
     // TODO: use an input vector length from a constant
     pub fn iv(&self) -> [u8; 12] {

+ 8 - 2
cli/src/action/download.rs

@@ -34,12 +34,18 @@ impl<'a> Download<'a> {
         // TODO: handle error here
         let file = RemoteFile::parse_url(url, None)?;
 
+        // Get the target file or directory
+        let target = self.cmd.file();
+
         // Create a progress bar reporter
         let bar = Arc::new(Mutex::new(ProgressBar::new_download()));
 
         // Execute an download action
-        ApiDownload::new(&file, self.cmd.password())
-            .invoke(&client, bar)?;
+        ApiDownload::new(
+            &file,
+            target,
+            self.cmd.password(),
+        ).invoke(&client, bar)?;
 
         // TODO: open the file, or it's location
         // TODO: copy the file location

+ 16 - 0
cli/src/cmd/cmd_download.rs

@@ -1,3 +1,5 @@
+use std::path::PathBuf;
+
 use ffsend_api::url::{ParseError, Url};
 
 use super::clap::{App, Arg, ArgMatches, SubCommand};
@@ -23,6 +25,11 @@ impl<'a: 'b, 'b> CmdDownload<'a> {
                 .help("The share URL")
                 .required(true)
                 .multiple(false))
+            .arg(Arg::with_name("file")
+                .long("file")
+                .short("f")
+                .value_name("PATH")
+                .help("The output file or directory"))
             .arg(Arg::with_name("password")
                 .long("password")
                 .short("p")
@@ -71,6 +78,15 @@ impl<'a: 'b, 'b> CmdDownload<'a> {
         }
     }
 
+    /// The target file or directory to download the file to.
+    /// If a directory is given, the file name of the original uploaded file
+    /// will be used.
+    pub fn file(&'a self) -> PathBuf {
+        self.matches.value_of("file")
+            .map(|path| PathBuf::from(path))
+            .unwrap_or(PathBuf::from("./"))
+    }
+
     /// Get the password.
     /// `None` is returned if no password was specified.
     pub fn password(&'a self) -> Option<String> {