|
@@ -1,7 +1,7 @@
|
|
use std::env::current_dir;
|
|
use std::env::current_dir;
|
|
-use std::fs::create_dir_all;
|
|
|
|
-#[cfg(feature = "archive")]
|
|
|
|
|
|
+use std::fs::{create_dir_all, File};
|
|
use std::io::Error as IoError;
|
|
use std::io::Error as IoError;
|
|
|
|
+use std::io::{self, BufReader, Read};
|
|
use std::path::{self, PathBuf};
|
|
use std::path::{self, PathBuf};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
|
|
@@ -11,10 +11,10 @@ use ffsend_api::action::download::{Download as ApiDownload, Error as DownloadErr
|
|
use ffsend_api::action::exists::{Error as ExistsError, Exists as ApiExists};
|
|
use ffsend_api::action::exists::{Error as ExistsError, Exists as ApiExists};
|
|
use ffsend_api::action::metadata::{Error as MetadataError, Metadata as ApiMetadata};
|
|
use ffsend_api::action::metadata::{Error as MetadataError, Metadata as ApiMetadata};
|
|
use ffsend_api::action::version::Error as VersionError;
|
|
use ffsend_api::action::version::Error as VersionError;
|
|
|
|
+use ffsend_api::file::metadata::{ManifestFile, Metadata};
|
|
use ffsend_api::file::remote_file::{FileParseError, RemoteFile};
|
|
use ffsend_api::file::remote_file::{FileParseError, RemoteFile};
|
|
use ffsend_api::pipe::ProgressReporter;
|
|
use ffsend_api::pipe::ProgressReporter;
|
|
-#[cfg(feature = "archive")]
|
|
|
|
-use tempfile::{Builder as TempBuilder, NamedTempFile};
|
|
|
|
|
|
+use tempfile::Builder as TempBuilder;
|
|
|
|
|
|
use super::select_api_version;
|
|
use super::select_api_version;
|
|
#[cfg(feature = "archive")]
|
|
#[cfg(feature = "archive")]
|
|
@@ -34,6 +34,71 @@ pub struct Download<'a> {
|
|
cmd_matches: &'a ArgMatches<'a>,
|
|
cmd_matches: &'a ArgMatches<'a>,
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+/// Strategy after the download action
|
|
|
|
+enum InvokeStrategy {
|
|
|
|
+ /// Just download the single normal file
|
|
|
|
+ Normal,
|
|
|
|
+ /// Download multiple files at once
|
|
|
|
+ Multi { files: Vec<ManifestFile> },
|
|
|
|
+ /// Download a single archive and extract it
|
|
|
|
+ #[cfg(feature = "archive")]
|
|
|
|
+ Extract,
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+impl InvokeStrategy {
|
|
|
|
+ /// Determine strategy from metadata and CLI input
|
|
|
|
+ pub fn build(
|
|
|
|
+ metadata: &Metadata,
|
|
|
|
+ matcher_download: &DownloadMatcher,
|
|
|
|
+ matcher_main: &MainMatcher,
|
|
|
|
+ ) -> InvokeStrategy {
|
|
|
|
+ // Check whether to extract
|
|
|
|
+ #[cfg(feature = "archive")]
|
|
|
|
+ {
|
|
|
|
+ if matcher_download.extract() {
|
|
|
|
+ return InvokeStrategy::Extract;
|
|
|
|
+ }
|
|
|
|
+ if metadata.is_archive() {
|
|
|
|
+ if prompt_yes(
|
|
|
|
+ "You're downloading an archive, extract it into the selected directory?",
|
|
|
|
+ Some(true),
|
|
|
|
+ &matcher_main,
|
|
|
|
+ ) {
|
|
|
|
+ return InvokeStrategy::Extract;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Check whether multiple files or not
|
|
|
|
+ if metadata.mime() == "send-archive" {
|
|
|
|
+ if let Some(manifest) = metadata.manifest() {
|
|
|
|
+ InvokeStrategy::Multi {
|
|
|
|
+ files: manifest.files().clone(),
|
|
|
|
+ }
|
|
|
|
+ // `files` will be used after the action, but `ApiDownload::new` consumes metadata.
|
|
|
|
+ // Therefore, we should clone in advance.
|
|
|
|
+ } else {
|
|
|
|
+ quit_error_msg(
|
|
|
|
+ "invalid metadata for downloading multiple files, manifest unknown",
|
|
|
|
+ ErrorHints::default(),
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ InvokeStrategy::Normal
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /// Whether the strategy will finally output multiple files
|
|
|
|
+ pub fn has_multi_outs(&self) -> bool {
|
|
|
|
+ match self {
|
|
|
|
+ InvokeStrategy::Normal => false,
|
|
|
|
+ InvokeStrategy::Multi { .. } => true,
|
|
|
|
+ #[cfg(feature = "archive")]
|
|
|
|
+ InvokeStrategy::Extract => true,
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
impl<'a> Download<'a> {
|
|
impl<'a> Download<'a> {
|
|
/// Construct a new download action.
|
|
/// Construct a new download action.
|
|
pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self {
|
|
pub fn new(cmd_matches: &'a ArgMatches<'a>) -> Self {
|
|
@@ -97,63 +162,46 @@ impl<'a> Download<'a> {
|
|
// Fetch the file metadata
|
|
// Fetch the file metadata
|
|
let metadata = ApiMetadata::new(&file, password.clone(), false).invoke(&client)?;
|
|
let metadata = ApiMetadata::new(&file, password.clone(), false).invoke(&client)?;
|
|
|
|
|
|
- // A temporary archive file, only used when archiving
|
|
|
|
- // The temporary file is stored here, to ensure it's lifetime exceeds the upload process
|
|
|
|
- #[cfg(feature = "archive")]
|
|
|
|
- let mut tmp_archive: Option<NamedTempFile> = None;
|
|
|
|
-
|
|
|
|
- // Check whether to extract
|
|
|
|
- #[cfg(feature = "archive")]
|
|
|
|
- let mut extract = matcher_download.extract();
|
|
|
|
-
|
|
|
|
- #[cfg(feature = "archive")]
|
|
|
|
- {
|
|
|
|
- // Ask to extract if downloading an archive
|
|
|
|
- if !extract && metadata.metadata().is_archive() {
|
|
|
|
- if prompt_yes(
|
|
|
|
- "You're downloading an archive, extract it into the selected directory?",
|
|
|
|
- Some(true),
|
|
|
|
- &matcher_main,
|
|
|
|
- ) {
|
|
|
|
- extract = true;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
|
|
+ // Determine strategy
|
|
|
|
+ let strategy = InvokeStrategy::build(metadata.metadata(), &matcher_download, &matcher_main);
|
|
|
|
|
|
// Prepare the download target and output path to use
|
|
// Prepare the download target and output path to use
|
|
- #[cfg(feature = "archive")]
|
|
|
|
- let output_dir = !extract;
|
|
|
|
- #[cfg(not(feature = "archive"))]
|
|
|
|
- let output_dir = false;
|
|
|
|
#[allow(unused_mut)]
|
|
#[allow(unused_mut)]
|
|
let mut target = Self::prepare_path(
|
|
let mut target = Self::prepare_path(
|
|
&target,
|
|
&target,
|
|
metadata.metadata().name(),
|
|
metadata.metadata().name(),
|
|
&matcher_main,
|
|
&matcher_main,
|
|
- output_dir,
|
|
|
|
|
|
+ &strategy,
|
|
);
|
|
);
|
|
#[cfg(feature = "archive")]
|
|
#[cfg(feature = "archive")]
|
|
let output_path = target.clone();
|
|
let output_path = target.clone();
|
|
|
|
|
|
- #[cfg(feature = "archive")]
|
|
|
|
- {
|
|
|
|
- // Allocate an archive file, and update the download and target paths
|
|
|
|
- if extract {
|
|
|
|
|
|
+ // Allocate an archive file if there will be multiple outputs…
|
|
|
|
+ let tmp_archive = match &strategy {
|
|
|
|
+ InvokeStrategy::Normal => None,
|
|
|
|
+ InvokeStrategy::Multi { .. } => Some(
|
|
|
|
+ TempBuilder::new()
|
|
|
|
+ .prefix(&format!(".{}-archive-", crate_name!()))
|
|
|
|
+ .tempfile()
|
|
|
|
+ .map_err(SplitError::TempFile)?,
|
|
|
|
+ ),
|
|
|
|
+ #[cfg(feature = "archive")]
|
|
|
|
+ InvokeStrategy::Extract => {
|
|
// TODO: select the extension dynamically
|
|
// TODO: select the extension dynamically
|
|
- let archive_extention = ".tar";
|
|
|
|
|
|
+ let archive_extension = ".tar";
|
|
|
|
|
|
- // Allocate a temporary file to download the archive to
|
|
|
|
- tmp_archive = Some(
|
|
|
|
|
|
+ Some(
|
|
TempBuilder::new()
|
|
TempBuilder::new()
|
|
.prefix(&format!(".{}-archive-", crate_name!()))
|
|
.prefix(&format!(".{}-archive-", crate_name!()))
|
|
- .suffix(archive_extention)
|
|
|
|
|
|
+ .suffix(archive_extension)
|
|
.tempfile()
|
|
.tempfile()
|
|
.map_err(ExtractError::TempFile)?,
|
|
.map_err(ExtractError::TempFile)?,
|
|
- );
|
|
|
|
- if let Some(tmp_archive) = &tmp_archive {
|
|
|
|
- target = tmp_archive.path().to_path_buf();
|
|
|
|
- }
|
|
|
|
|
|
+ )
|
|
}
|
|
}
|
|
|
|
+ };
|
|
|
|
+ // …and update the download and target paths
|
|
|
|
+ if let Some(tmp_archive) = &tmp_archive {
|
|
|
|
+ target = tmp_archive.path().to_path_buf();
|
|
}
|
|
}
|
|
|
|
|
|
// Ensure there is enough disk space available when not being forced
|
|
// Ensure there is enough disk space available when not being forced
|
|
@@ -177,10 +225,18 @@ impl<'a> Download<'a> {
|
|
ApiDownload::new(api_version, &file, target, password, false, Some(metadata))
|
|
ApiDownload::new(api_version, &file, target, password, false, Some(metadata))
|
|
.invoke(&transfer_client, progress)?;
|
|
.invoke(&transfer_client, progress)?;
|
|
|
|
|
|
- // Extract the downloaded file if working with an archive
|
|
|
|
- #[cfg(feature = "archive")]
|
|
|
|
- {
|
|
|
|
- if extract {
|
|
|
|
|
|
+ // Post process
|
|
|
|
+ match strategy {
|
|
|
|
+ InvokeStrategy::Multi { files } => {
|
|
|
|
+ eprintln!("Splitting...");
|
|
|
|
+
|
|
|
|
+ Self::split(tmp_archive.unwrap().into_file(), files).map_err(SplitError::Split)?;
|
|
|
|
+ }
|
|
|
|
+ InvokeStrategy::Normal => {
|
|
|
|
+ // No need to post process
|
|
|
|
+ }
|
|
|
|
+ #[cfg(feature = "archive")]
|
|
|
|
+ InvokeStrategy::Extract => {
|
|
eprintln!("Extracting...");
|
|
eprintln!("Extracting...");
|
|
|
|
|
|
// Extract the downloaded file
|
|
// Extract the downloaded file
|
|
@@ -203,8 +259,8 @@ impl<'a> Download<'a> {
|
|
/// This methods prepares a full file path to use for the file to
|
|
/// This methods prepares a full file path to use for the file to
|
|
/// download, based on the current directory, the original file name,
|
|
/// download, based on the current directory, the original file name,
|
|
/// and the user input.
|
|
/// and the user input.
|
|
- /// If `file` is set to false, no file name is included and the path
|
|
|
|
- /// will point to a directory.
|
|
|
|
|
|
+ /// If `strategy` will generate multiple outputs, no file name is included
|
|
|
|
+ /// and the path will point to a directory.
|
|
///
|
|
///
|
|
/// If no file name was given, the original file name is used.
|
|
/// If no file name was given, the original file name is used.
|
|
///
|
|
///
|
|
@@ -218,31 +274,53 @@ impl<'a> Download<'a> {
|
|
target: &PathBuf,
|
|
target: &PathBuf,
|
|
name_hint: &str,
|
|
name_hint: &str,
|
|
main_matcher: &MainMatcher,
|
|
main_matcher: &MainMatcher,
|
|
- file: bool,
|
|
|
|
|
|
+ strategy: &InvokeStrategy,
|
|
) -> PathBuf {
|
|
) -> PathBuf {
|
|
// Select the path to use
|
|
// Select the path to use
|
|
let mut target = Self::select_path(&target, name_hint);
|
|
let mut target = Self::select_path(&target, name_hint);
|
|
|
|
|
|
- // Use the parent directory, if we don't want a file
|
|
|
|
- if !file {
|
|
|
|
|
|
+ // Use the parent directory, if there will be multiple outputs
|
|
|
|
+ if strategy.has_multi_outs() {
|
|
target = target.parent().unwrap().to_path_buf();
|
|
target = target.parent().unwrap().to_path_buf();
|
|
}
|
|
}
|
|
|
|
|
|
- // Ask to overwrite
|
|
|
|
- if file && target.exists() && !main_matcher.force() {
|
|
|
|
- eprintln!(
|
|
|
|
- "The path '{}' already exists",
|
|
|
|
- target.to_str().unwrap_or("?"),
|
|
|
|
- );
|
|
|
|
- if !prompt_yes("Overwrite?", None, main_matcher) {
|
|
|
|
- println!("Download cancelled");
|
|
|
|
- quit();
|
|
|
|
|
|
+ // Ask to overwrite if any file already exists
|
|
|
|
+ if !main_matcher.force() {
|
|
|
|
+ match strategy {
|
|
|
|
+ InvokeStrategy::Normal => {
|
|
|
|
+ if target.exists() {
|
|
|
|
+ eprintln!(
|
|
|
|
+ "The path '{}' already exists",
|
|
|
|
+ target.to_str().unwrap_or("?"),
|
|
|
|
+ );
|
|
|
|
+ if !prompt_yes("Overwrite?", None, main_matcher) {
|
|
|
|
+ println!("Download cancelled");
|
|
|
|
+ quit();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ InvokeStrategy::Multi { files } => {
|
|
|
|
+ for ManifestFile { name, .. } in files {
|
|
|
|
+ let path = Self::select_path(&target, name);
|
|
|
|
+ if path.exists() {
|
|
|
|
+ eprintln!("The path '{}' already exists", path.to_str().unwrap_or("?"),);
|
|
|
|
+ if !prompt_yes("Overwrite?", None, main_matcher) {
|
|
|
|
+ println!("Download cancelled");
|
|
|
|
+ quit();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ InvokeStrategy::Extract => {
|
|
|
|
+ // We can't determine what will be extracted now,
|
|
|
|
+ // so let it go.
|
|
|
|
+ }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
{
|
|
{
|
|
// Get the deepest directory, as we have to ensure it exists
|
|
// Get the deepest directory, as we have to ensure it exists
|
|
- let dir = if file {
|
|
|
|
|
|
+ let dir = if !strategy.has_multi_outs() {
|
|
match target.parent() {
|
|
match target.parent() {
|
|
Some(parent) => parent,
|
|
Some(parent) => parent,
|
|
None => quit_error_msg("invalid output file path", ErrorHints::default()),
|
|
None => quit_error_msg("invalid output file path", ErrorHints::default()),
|
|
@@ -347,6 +425,18 @@ impl<'a> Download<'a> {
|
|
|
|
|
|
target
|
|
target
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ /// Split the downloaded send-archive into multiple files
|
|
|
|
+ fn split(archive: File, files: Vec<ManifestFile>) -> Result<(), IoError> {
|
|
|
|
+ let mut reader = BufReader::new(archive);
|
|
|
|
+
|
|
|
|
+ for ManifestFile { name, size, .. } in files {
|
|
|
|
+ let mut writer = File::create(name)?;
|
|
|
|
+ io::copy(&mut reader.by_ref().take(size), &mut writer)?;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Ok(())
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Debug, Fail)]
|
|
#[derive(Debug, Fail)]
|
|
@@ -378,6 +468,10 @@ pub enum Error {
|
|
#[fail(display = "failed the extraction procedure")]
|
|
#[fail(display = "failed the extraction procedure")]
|
|
Extract(#[cause] ExtractError),
|
|
Extract(#[cause] ExtractError),
|
|
|
|
|
|
|
|
+ /// An error occurred while splitting send-archive into multiple files.
|
|
|
|
+ #[fail(display = "failed the split procedure")]
|
|
|
|
+ Split(#[cause] SplitError),
|
|
|
|
+
|
|
/// The given Send file has expired, or did never exist in the first place.
|
|
/// The given Send file has expired, or did never exist in the first place.
|
|
#[fail(display = "the file has expired or did never exist")]
|
|
#[fail(display = "the file has expired or did never exist")]
|
|
Expired,
|
|
Expired,
|
|
@@ -420,6 +514,12 @@ impl From<ExtractError> for Error {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+impl From<SplitError> for Error {
|
|
|
|
+ fn from(err: SplitError) -> Error {
|
|
|
|
+ Error::Split(err)
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
#[cfg(feature = "archive")]
|
|
#[cfg(feature = "archive")]
|
|
#[derive(Debug, Fail)]
|
|
#[derive(Debug, Fail)]
|
|
pub enum ExtractError {
|
|
pub enum ExtractError {
|
|
@@ -431,3 +531,14 @@ pub enum ExtractError {
|
|
#[fail(display = "failed to extract archive contents to target directory")]
|
|
#[fail(display = "failed to extract archive contents to target directory")]
|
|
Extract(#[cause] IoError),
|
|
Extract(#[cause] IoError),
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+#[derive(Debug, Fail)]
|
|
|
|
+pub enum SplitError {
|
|
|
|
+ /// An error occurred while creating the temporary file.
|
|
|
|
+ #[fail(display = "failed to create temporary file")]
|
|
|
|
+ TempFile(#[cause] IoError),
|
|
|
|
+
|
|
|
|
+ /// Failed to split send-archive into multiple files.
|
|
|
|
+ #[fail(display = "failed to split the file into multiple files")]
|
|
|
|
+ Split(#[cause] IoError),
|
|
|
|
+}
|