From 8259106c170c6a6efb51ce08ba8abafcb93ace38 Mon Sep 17 00:00:00 2001 From: timvisee Date: Thu, 29 Mar 2018 22:50:43 +0200 Subject: [PATCH] Allow specifying an output file or directory when downloading [WIP] --- IDEAS.md | 15 +++++-- api/src/action/download.rs | 85 ++++++++++++++++++++++++++++++------- api/src/file/metadata.rs | 5 +++ cli/src/action/download.rs | 10 ++++- cli/src/cmd/cmd_download.rs | 16 +++++++ 5 files changed, 109 insertions(+), 22 deletions(-) diff --git a/IDEAS.md b/IDEAS.md index 2bc74d1..c7a7e97 100644 --- a/IDEAS.md +++ b/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 diff --git a/api/src/action/download.rs b/api/src/action/download.rs index 79a9d3f..cb28787 100644 --- a/api/src/action/download.rs +++ b/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, } impl<'a> Download<'a> { /// Construct a new download action for the given remote file. - pub fn new(file: &'a RemoteFile, password: Option) -> Self { + pub fn new( + file: &'a RemoteFile, + target: PathBuf, + password: Option, + ) -> 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, - ) -> Result, MetaError> { + ) -> Result<(Metadata, Vec), 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. /// diff --git a/api/src/file/metadata.rs b/api/src/file/metadata.rs index 1d77628..7903c6e 100644 --- a/api/src/file/metadata.rs +++ b/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] { diff --git a/cli/src/action/download.rs b/cli/src/action/download.rs index 9be775b..cc14042 100644 --- a/cli/src/action/download.rs +++ b/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 diff --git a/cli/src/cmd/cmd_download.rs b/cli/src/cmd/cmd_download.rs index 97d7de1..0b15f50 100644 --- a/cli/src/cmd/cmd_download.rs +++ b/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 {