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

This commit is contained in:
timvisee 2018-03-29 22:50:43 +02:00
parent 649190b17a
commit 8259106c17
No known key found for this signature in database
GPG key ID: 109CBA0BF74036C2
5 changed files with 109 additions and 22 deletions

View file

@ -1,12 +1,15 @@
# Ideas # 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 - Box errors
- Info endpoint, to view file info - Info endpoint, to view file info
- On download, mention a wrong or missing password with a HTTP 401 response - On download, mention a wrong or missing password with a HTTP 401 response
- Automatically get owner token, from file history when setting password - Automatically get owner token, from file history when setting password
- Implement error handling everywhere properly - Implement error handling everywhere properly
- `-y` flag for assume yes
- `-f` flag for forcing (no interact?)
- Quick upload/download without `upload` or `download` subcommands. - Quick upload/download without `upload` or `download` subcommands.
- Set file password - Set file password
- Set file download count - Set file download count
@ -33,4 +36,8 @@
- Ubuntu PPA package - Ubuntu PPA package
- Move API URL generator methods out of remote file class - Move API URL generator methods out of remote file class
- Prompt if a file download password is required - 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

View file

@ -6,6 +6,7 @@ use std::io::{
Error as IoError, Error as IoError,
Read, Read,
}; };
use std::path::PathBuf;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use failure::Error as FailureError; use failure::Error as FailureError;
@ -34,15 +35,23 @@ pub struct Download<'a> {
/// The remote file to download. /// The remote file to download.
file: &'a RemoteFile, file: &'a RemoteFile,
/// The target file or directory, to download the file to.
target: PathBuf,
/// An optional password to decrypt a protected file. /// An optional password to decrypt a protected file.
password: Option<String>, password: Option<String>,
} }
impl<'a> Download<'a> { impl<'a> Download<'a> {
/// Construct a new download action for the given remote file. /// 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 { Self {
file, file,
target,
password, password,
} }
} }
@ -59,15 +68,25 @@ impl<'a> Download<'a> {
// Fetch the authentication nonce // Fetch the authentication nonce
let auth_nonce = self.fetch_auth_nonce(client)?; let auth_nonce = self.fetch_auth_nonce(client)?;
// Fetch the meta nonce, set the input vector // Fetch the meta data, apply the derived input vector
let meta_nonce = self.fetch_meta_nonce(&client, &mut key, auth_nonce)?; 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 // Open the file we will write to
// TODO: this should become a temporary file first // TODO: this should become a temporary file first
// TODO: use the uploaded file name as default // TODO: use the uploaded file name as default
let path = "downloaded.zip";
let out = File::create(path) 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 // Create the file reader for downloading
let (reader, len) = self.create_file_reader(&key, meta_nonce, &client)?; let (reader, len) = self.create_file_reader(&key, meta_nonce, &client)?;
@ -78,7 +97,7 @@ impl<'a> Download<'a> {
len, len,
&key, &key,
reporter.clone(), reporter.clone(),
).map_err(|err| Error::File(path.into(), err))?; ).map_err(|err| Error::File(path_str.clone(), err))?;
// Download the file // Download the file
self.download(reader, writer, len, reporter)?; self.download(reader, writer, len, reporter)?;
@ -127,24 +146,29 @@ impl<'a> Download<'a> {
).map_err(|_| AuthError::MalformedNonce.into()) ).map_err(|_| AuthError::MalformedNonce.into())
} }
/// Fetch the metadata nonce.
/// This method also sets the input vector on the given key set, /// Create a metadata nonce, and fetch the metadata for the file from the
/// extracted from the metadata. /// server.
/// ///
/// The key set, along with the authentication nonce must be given. /// 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, &self,
client: &Client, client: &Client,
key: &mut KeySet, key: &mut KeySet,
auth_nonce: Vec<u8>, auth_nonce: Vec<u8>,
) -> Result<Vec<u8>, MetaError> { ) -> Result<(Metadata, Vec<u8>), MetaError> {
// Fetch the metadata and the nonce // 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 // Set the input vector bas
key.set_iv(metadata.iv()); key.set_iv(data.0.iv());
Ok(meta_nonce)
Ok(data)
} }
/// Create a metadata nonce, and fetch the metadata for the file from the /// 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 /// Make a download request, and create a reader that downloads the
/// encrypted file. /// encrypted file.
/// ///

View file

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

View file

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

View file

@ -1,3 +1,5 @@
use std::path::PathBuf;
use ffsend_api::url::{ParseError, Url}; use ffsend_api::url::{ParseError, Url};
use super::clap::{App, Arg, ArgMatches, SubCommand}; use super::clap::{App, Arg, ArgMatches, SubCommand};
@ -23,6 +25,11 @@ impl<'a: 'b, 'b> CmdDownload<'a> {
.help("The share URL") .help("The share URL")
.required(true) .required(true)
.multiple(false)) .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") .arg(Arg::with_name("password")
.long("password") .long("password")
.short("p") .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. /// Get the password.
/// `None` is returned if no password was specified. /// `None` is returned if no password was specified.
pub fn password(&'a self) -> Option<String> { pub fn password(&'a self) -> Option<String> {