Merge branch '30-allow-multi-file-uploads' into 'master'
Support multiple files for uploading through archive See merge request timvisee/ffsend!29
This commit is contained in:
commit
02559bc6ce
6 changed files with 274 additions and 85 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -646,6 +646,7 @@ dependencies = [
|
|||
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"open 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pathdiff 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pbr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"prettytable-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"qr2term 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -1251,6 +1252,11 @@ dependencies = [
|
|||
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "pbr"
|
||||
version = "1.0.1"
|
||||
|
@ -2433,6 +2439,7 @@ dependencies = [
|
|||
"checksum owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13"
|
||||
"checksum parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ab41b4aed082705d1056416ae4468b6ea99d52599ecf3169b00088d43113e337"
|
||||
"checksum parking_lot_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94c8c7923936b28d546dfd14d4472eaf34c99b14e1c973a32b3e6d4eb04298c9"
|
||||
"checksum pathdiff 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a3bf70094d203e07844da868b634207e71bfab254fe713171fae9a6e751ccf31"
|
||||
"checksum pbr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "deb73390ab68d81992bd994d145f697451bb0b54fd39738e72eef32458ad6907"
|
||||
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
|
||||
"checksum phf 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18"
|
||||
|
|
|
@ -108,6 +108,7 @@ fs2 = "0.4"
|
|||
lazy_static = "1.0"
|
||||
open = "1"
|
||||
openssl-probe = "0.1"
|
||||
pathdiff = "0.1"
|
||||
pbr = "1"
|
||||
prettytable-rs = "0.8"
|
||||
qr2term = { version = "0.1", optional = true }
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
use std::env::current_dir;
|
||||
#[cfg(feature = "archive")]
|
||||
use std::io::Error as IoError;
|
||||
use std::path::Path;
|
||||
#[cfg(feature = "archive")]
|
||||
use std::path::PathBuf;
|
||||
#[cfg(feature = "archive")]
|
||||
use std::process::exit;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use clap::ArgMatches;
|
||||
|
@ -10,6 +15,7 @@ use ffsend_api::action::upload::{Error as UploadError, Upload as ApiUpload};
|
|||
use ffsend_api::action::version::Error as VersionError;
|
||||
use ffsend_api::config::{upload_size_max, UPLOAD_SIZE_MAX_RECOMMENDED};
|
||||
use ffsend_api::pipe::ProgressReporter;
|
||||
use pathdiff::diff_paths;
|
||||
use prettytable::{format::FormatBuilder, Cell, Row, Table};
|
||||
#[cfg(feature = "qrcode")]
|
||||
use qr2term::print_qr;
|
||||
|
@ -53,9 +59,183 @@ impl<'a> Upload<'a> {
|
|||
|
||||
// Get API parameters
|
||||
#[allow(unused_mut)]
|
||||
let mut path = Path::new(matcher_upload.file()).to_path_buf();
|
||||
let mut paths: Vec<_> = matcher_upload
|
||||
.files()
|
||||
.into_iter()
|
||||
.map(|p| Path::new(p).to_path_buf())
|
||||
.collect();
|
||||
let mut path = Path::new(paths.first().unwrap()).to_path_buf();
|
||||
let host = matcher_upload.host();
|
||||
|
||||
// The file name to use
|
||||
#[allow(unused_mut)]
|
||||
let mut file_name = matcher_upload.name().map(|s| s.to_owned());
|
||||
|
||||
// All paths must exist
|
||||
// TODO: ensure the file exists and is accessible
|
||||
for path in &paths {
|
||||
if !path.exists() {
|
||||
quit_error_msg(
|
||||
format!("the path '{}' does not exist", path.to_str().unwrap_or("?")),
|
||||
ErrorHintsBuilder::default().build().unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A temporary archive file, only used when archiving
|
||||
// The temporary file is stored here, to ensure it's lifetime exceeds the upload process
|
||||
#[allow(unused_mut)]
|
||||
#[cfg(feature = "archive")]
|
||||
let mut tmp_archive: Option<NamedTempFile> = None;
|
||||
|
||||
#[cfg(feature = "archive")]
|
||||
{
|
||||
// Determine whether to archive, we must archive for multiple files/directory
|
||||
let mut archive = matcher_upload.archive();
|
||||
if !archive {
|
||||
if paths.len() > 1 {
|
||||
if prompt_yes(
|
||||
"You've selected multiple files, only a single file may be uploaded.\n\
|
||||
Archive the files into a single file?",
|
||||
Some(true),
|
||||
&matcher_main,
|
||||
) {
|
||||
archive = true;
|
||||
} else {
|
||||
exit(1);
|
||||
}
|
||||
} else if path.is_dir() {
|
||||
if prompt_yes(
|
||||
"You've selected a directory, only a single file may be uploaded.\n\
|
||||
Archive the directory into a single file?",
|
||||
Some(true),
|
||||
&matcher_main,
|
||||
) {
|
||||
archive = true;
|
||||
} else {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Archive the selected file or directory
|
||||
if archive {
|
||||
eprintln!("Archiving...");
|
||||
let archive_extention = ".tar";
|
||||
|
||||
// Create a new temporary file to write the archive to
|
||||
tmp_archive = Some(
|
||||
TempBuilder::new()
|
||||
.prefix(&format!(".{}-archive-", crate_name!()))
|
||||
.suffix(archive_extention)
|
||||
.tempfile()
|
||||
.map_err(ArchiveError::TempFile)?,
|
||||
);
|
||||
if let Some(tmp_archive) = &tmp_archive {
|
||||
// Get the path, and the actual file
|
||||
let archive_path = tmp_archive.path().to_path_buf();
|
||||
let archive_file = tmp_archive
|
||||
.as_file()
|
||||
.try_clone()
|
||||
.map_err(ArchiveError::CloneHandle)?;
|
||||
|
||||
// Select the file name to use if not set
|
||||
if file_name.is_none() {
|
||||
// Require user to specify name if multiple files are given
|
||||
if paths.len() > 1 {
|
||||
quit_error_msg(
|
||||
"you must specify a file name for the archive",
|
||||
ErrorHintsBuilder::default()
|
||||
.name(true)
|
||||
.verbose(false)
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// Derive name from given file
|
||||
file_name = Some(
|
||||
path.canonicalize()
|
||||
.map_err(|err| ArchiveError::FileName(Some(err)))?
|
||||
.file_name()
|
||||
.ok_or(ArchiveError::FileName(None))?
|
||||
.to_str()
|
||||
.map(|s| s.to_owned())
|
||||
.ok_or(ArchiveError::FileName(None))?,
|
||||
);
|
||||
}
|
||||
|
||||
// Get the current working directory, including working directory as highest possible root, canonicalize it
|
||||
let working_dir =
|
||||
current_dir().expect("failed to get current working directory");
|
||||
let shared_dir = {
|
||||
let mut paths = paths.clone();
|
||||
paths.push(working_dir.clone());
|
||||
match shared_dir(paths) {
|
||||
Some(p) => p,
|
||||
None => quit_error_msg(
|
||||
"when archiving, all files must be within a same directory",
|
||||
ErrorHintsBuilder::default().verbose(false).build().unwrap(),
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
// Build an archiver, append each file
|
||||
let mut archiver = Archiver::new(archive_file);
|
||||
for path in &paths {
|
||||
// Canonicalize the path
|
||||
let mut path = Path::new(path).to_path_buf();
|
||||
if let Ok(p) = path.canonicalize() {
|
||||
path = p;
|
||||
}
|
||||
|
||||
// Find relative name to share dir, used to derive name from
|
||||
let name = diff_paths(&path, &shared_dir)
|
||||
.expect("failed to determine relative path of file to archive");
|
||||
let name = name.to_str().expect("failed to get file path");
|
||||
|
||||
// Add file to archiver
|
||||
archiver
|
||||
.append_path(name, &path)
|
||||
.map_err(ArchiveError::AddFile)?;
|
||||
}
|
||||
|
||||
// Finish the archival process, writes the archive file
|
||||
archiver.finish().map_err(ArchiveError::Write)?;
|
||||
|
||||
// Append archive extention to name, set to upload archived file
|
||||
if let Some(ref mut file_name) = file_name {
|
||||
file_name.push_str(archive_extention);
|
||||
}
|
||||
path = archive_path;
|
||||
paths.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Quit with error when uploading multiple files or directory, if we cannot archive
|
||||
#[cfg(not(feature = "archive"))]
|
||||
{
|
||||
if paths.len() > 1 {
|
||||
quit_error_msg(
|
||||
"uploading multiple files is not supported, ffsend must be compiled with 'archive' feature for this",
|
||||
ErrorHintsBuilder::default()
|
||||
.verbose(false)
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
if path.is_dir() {
|
||||
quit_error_msg(
|
||||
"uploading a directory is not supported, ffsend must be compiled with 'archive' feature for this",
|
||||
ErrorHintsBuilder::default()
|
||||
.verbose(false)
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a reqwest client capable for uploading files
|
||||
let client_config = create_config(&matcher_main);
|
||||
let client = client_config.clone().client(false);
|
||||
|
@ -65,8 +245,6 @@ impl<'a> Upload<'a> {
|
|||
select_api_version(&client, host.clone(), &mut desired_version)?;
|
||||
let api_version = desired_version.version().unwrap();
|
||||
|
||||
// TODO: ensure the file exists and is accessible
|
||||
|
||||
// We do not authenticate for now
|
||||
let auth = false;
|
||||
|
||||
|
@ -155,83 +333,6 @@ impl<'a> Upload<'a> {
|
|||
}
|
||||
};
|
||||
|
||||
// The file name to use
|
||||
#[allow(unused_mut)]
|
||||
let mut file_name = matcher_upload.name().map(|s| s.to_owned());
|
||||
|
||||
// A temporary archive file, only used when archiving
|
||||
// The temporary file is stored here, to ensure it's lifetime exceeds the upload process
|
||||
#[allow(unused_mut)]
|
||||
#[cfg(feature = "archive")]
|
||||
let mut tmp_archive: Option<NamedTempFile> = None;
|
||||
|
||||
#[cfg(feature = "archive")]
|
||||
{
|
||||
// Determine whether to archive, ask if a directory was selected
|
||||
let mut archive = matcher_upload.archive();
|
||||
if !archive && path.is_dir() {
|
||||
if prompt_yes(
|
||||
"You've selected a directory, only a single file may be uploaded.\n\
|
||||
Archive the directory into a single file?",
|
||||
Some(true),
|
||||
&matcher_main,
|
||||
) {
|
||||
archive = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Archive the selected file or directory
|
||||
if archive {
|
||||
eprintln!("Archiving...");
|
||||
let archive_extention = ".tar";
|
||||
|
||||
// Create a new temporary file to write the archive to
|
||||
tmp_archive = Some(
|
||||
TempBuilder::new()
|
||||
.prefix(&format!(".{}-archive-", crate_name!()))
|
||||
.suffix(archive_extention)
|
||||
.tempfile()
|
||||
.map_err(ArchiveError::TempFile)?,
|
||||
);
|
||||
if let Some(tmp_archive) = &tmp_archive {
|
||||
// Get the path, and the actual file
|
||||
let archive_path = tmp_archive.path().to_path_buf();
|
||||
let archive_file = tmp_archive
|
||||
.as_file()
|
||||
.try_clone()
|
||||
.map_err(ArchiveError::CloneHandle)?;
|
||||
|
||||
// Select the file name to use if not set
|
||||
if file_name.is_none() {
|
||||
file_name = Some(
|
||||
path.canonicalize()
|
||||
.map_err(|err| ArchiveError::FileName(Some(err)))?
|
||||
.file_name()
|
||||
.ok_or(ArchiveError::FileName(None))?
|
||||
.to_str()
|
||||
.map(|s| s.to_owned())
|
||||
.ok_or(ArchiveError::FileName(None))?,
|
||||
);
|
||||
}
|
||||
|
||||
// Build an archiver and append the file
|
||||
let mut archiver = Archiver::new(archive_file);
|
||||
archiver
|
||||
.append_path(file_name.as_ref().unwrap(), &path)
|
||||
.map_err(ArchiveError::AddFile)?;
|
||||
|
||||
// Finish the archival process, writes the archive file
|
||||
archiver.finish().map_err(ArchiveError::Write)?;
|
||||
|
||||
// Append archive extention to name, set to upload archived file
|
||||
if let Some(ref mut file_name) = file_name {
|
||||
file_name.push_str(archive_extention);
|
||||
}
|
||||
path = archive_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the progress reporter
|
||||
let progress_reporter: Arc<Mutex<ProgressReporter>> = progress_bar;
|
||||
|
||||
|
@ -376,6 +477,74 @@ impl<'a> Upload<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Find the deepest directory all given paths share.
|
||||
///
|
||||
/// This function canonicalizes the paths, make sure the paths exist.
|
||||
///
|
||||
/// Returns `None` if paths are using a different root.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// If the following paths are given:
|
||||
///
|
||||
/// - `/home/user/git/ffsend/src`
|
||||
/// - `/home/user/git/ffsend/src/main.rs`
|
||||
/// - `/home/user/git/ffsend/Cargo.toml`
|
||||
///
|
||||
/// The following is returned:
|
||||
///
|
||||
/// `/home/user/git/ffsend`
|
||||
#[cfg(feature = "archive")]
|
||||
fn shared_dir(paths: Vec<PathBuf>) -> Option<PathBuf> {
|
||||
// Any path must be given
|
||||
if paths.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Build vector
|
||||
let c: Vec<Vec<PathBuf>> = paths
|
||||
.into_iter()
|
||||
.map(|p| p.canonicalize().expect("failed to canonicalize path"))
|
||||
.map(|mut p| {
|
||||
// Start with parent if current path is file
|
||||
if p.is_file() {
|
||||
p = match p.parent() {
|
||||
Some(p) => p.to_path_buf(),
|
||||
None => return vec![],
|
||||
};
|
||||
}
|
||||
|
||||
// Build list of path buffers for each path component
|
||||
let mut items = vec![p];
|
||||
#[allow(mutable_borrow_reservation_conflict)]
|
||||
while let Some(item) = items.last().unwrap().parent() {
|
||||
items.push(item.to_path_buf());
|
||||
}
|
||||
|
||||
// Reverse as we built it in the wrong order
|
||||
items.reverse();
|
||||
items
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Find the index at which the paths are last shared at by walking through indices
|
||||
let i = (0..)
|
||||
.take_while(|i| {
|
||||
// Get path for first item, stop if none
|
||||
let base = &c[0].get(*i);
|
||||
if base.is_none() {
|
||||
return false;
|
||||
};
|
||||
|
||||
// All other paths must equal at this index
|
||||
c.iter().skip(1).all(|p| &p.get(*i) == base)
|
||||
})
|
||||
.last();
|
||||
|
||||
// Find the shared path
|
||||
i.map(|i| c[0][i].to_path_buf())
|
||||
}
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
/// Selecting the API version to use failed.
|
||||
|
|
|
@ -18,10 +18,11 @@ pub struct UploadMatcher<'a> {
|
|||
impl<'a: 'b, 'b> UploadMatcher<'a> {
|
||||
/// Get the selected file to upload.
|
||||
// TODO: maybe return a file or path instance here
|
||||
pub fn file(&'a self) -> &'a str {
|
||||
pub fn files(&'a self) -> Vec<&'a str> {
|
||||
self.matches
|
||||
.value_of("FILE")
|
||||
.values_of("FILE")
|
||||
.expect("no file specified to upload")
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The the name to use for the uploaded file.
|
||||
|
|
|
@ -16,9 +16,9 @@ impl CmdUpload {
|
|||
.visible_alias("up")
|
||||
.arg(
|
||||
Arg::with_name("FILE")
|
||||
.help("The file to upload")
|
||||
.help("The file(s) to upload")
|
||||
.required(true)
|
||||
.multiple(false),
|
||||
.multiple(true),
|
||||
)
|
||||
.arg(ArgPassword::build().help("Protect the file with a password"))
|
||||
.arg(ArgGenPassphrase::build())
|
||||
|
|
13
src/util.rs
13
src/util.rs
|
@ -132,6 +132,9 @@ pub struct ErrorHints {
|
|||
/// A list of info messages to print along with the error.
|
||||
info: Vec<String>,
|
||||
|
||||
/// Show about the name option.
|
||||
name: bool,
|
||||
|
||||
/// Show about the password option.
|
||||
password: bool,
|
||||
|
||||
|
@ -157,7 +160,8 @@ impl ErrorHints {
|
|||
pub fn any(&self) -> bool {
|
||||
// Determine the result
|
||||
#[allow(unused_mut)]
|
||||
let mut result = self.password || self.owner || self.force || self.verbose || self.help;
|
||||
let mut result =
|
||||
self.name || self.password || self.owner || self.force || self.verbose || self.help;
|
||||
|
||||
// Factor in the history hint when enabled
|
||||
#[cfg(feature = "history")]
|
||||
|
@ -189,6 +193,12 @@ impl ErrorHints {
|
|||
highlight("--api <VERSION>")
|
||||
);
|
||||
}
|
||||
if self.name {
|
||||
eprintln!(
|
||||
"Use '{}' to specify a file name",
|
||||
highlight("--name <NAME>")
|
||||
);
|
||||
}
|
||||
if self.password {
|
||||
eprintln!(
|
||||
"Use '{}' to specify a password",
|
||||
|
@ -230,6 +240,7 @@ impl Default for ErrorHints {
|
|||
ErrorHints {
|
||||
api: false,
|
||||
info: Vec::new(),
|
||||
name: false,
|
||||
password: false,
|
||||
owner: false,
|
||||
#[cfg(feature = "history")]
|
||||
|
|
Loading…
Reference in a new issue