Implement archive download extract feature

This commit is contained in:
timvisee 2018-05-17 21:09:52 +02:00
parent f2771f7953
commit 738ae46054
No known key found for this signature in database
GPG key ID: 109CBA0BF74036C2
15 changed files with 214 additions and 34 deletions

View file

@ -19,5 +19,6 @@ toolchain.
- `FFSEND_INCOGNITO`: incognito mode (present/boolean)
- `FFSEND_OPEN`: open an uploaded file (present/boolean)
- `FFSEND_ARCHIVE`: enable file archival (present/boolean)
- `FFSEND_EXTRACT`: enable file extraction (present/boolean)
- `FFSEND_COPY`: copy share URL to clipboard (present/boolean)
- `FFSEND_VERBOSE`: copy share URL to clipboard (present/boolean)

View file

@ -3,8 +3,6 @@ The first release used for gathering feedback on the application by selected
people.
Features:
- Make use of stdout and stderr consistent
- Allow file/directory archiving on upload
- Allow unarchiving on download
- Use clipboard through `xclip` on Linux if available for persistence
- Write complete README
@ -16,6 +14,7 @@ Features:
- Arch AUR package
- Windows, macOS and Redox support
- Implement verbose logging with `-v`
- Make use of stdout and stderr consistent
- Allow empty owner token for info command
- Check and validate all errors, are some too verbose?
@ -23,6 +22,7 @@ Features:
The first public release.
Features:
- Do not write archives to the disk (temporarily), stream their contents
- Implement error handling everywhere properly
- Extract utility module
- Embed/wrap request errors with failure
@ -41,6 +41,8 @@ Features:
- Host configuration file for host tags, to easily upload to other hosts
# Other ideas
- Check if extracting an archive overwrites files
- Flag to disable logging to stderr
- Rework encrypted reader/writer
- API actions contain duplicate code, create centralized functions
- Only allow file extension renaming on upload with `-f` flag

View file

@ -13,6 +13,9 @@ use serde_json;
use crypto::b64;
/// The MIME type string for a tar file.
const MIME_TAR: &str = "application/x-tar";
/// File metadata, which is send to the server.
#[derive(Debug, Serialize, Deserialize)]
pub struct Metadata {
@ -66,6 +69,14 @@ impl Metadata {
// Create a sized array
*array_ref!(decoded, 0, 12)
}
/**
* Check whether this MIME type is recognized as supported archive type.
* `true` is returned if it's an archive, `false` if not.
*/
pub fn is_archive(&self) -> bool {
self.mime.to_lowercase() == MIME_TAR.to_lowercase()
}
}
/// A X-File-Metadata header for reqwest, that is used to pass encrypted

View file

@ -14,7 +14,7 @@ use cmd::matcher::{
Matcher,
};
use error::ActionError;
use util::format_duration;
use util::{format_bool, format_duration};
/// A file debug action.
pub struct Debug<'a> {
@ -58,6 +58,12 @@ impl<'a> Debug<'a> {
Cell::new(&format_duration(Duration::seconds(SEND_DEFAULT_EXPIRE_TIME))),
]));
// Show whether verbose is used
table.add_row(Row::new(vec![
Cell::new("Verbose:"),
Cell::new(format_bool(matcher_main.verbose())),
]));
// Print the debug table
table.printstd();

View file

@ -1,5 +1,6 @@
use std::env::current_dir;
use std::fs::create_dir_all;
use std::io::Error as IoError;
use std::path::{self, PathBuf};
use std::sync::{Arc, Mutex};
@ -20,7 +21,13 @@ use ffsend_api::action::metadata::{
use ffsend_api::file::remote_file::{FileParseError, RemoteFile};
use ffsend_api::reader::ProgressReporter;
use ffsend_api::reqwest::Client;
use tempfile::{
Builder as TempBuilder,
NamedTempFile,
};
#[cfg(feature = "archive")]
use archive::archive::Archive;
use cmd::matcher::{
Matcher,
download::DownloadMatcher,
@ -92,12 +99,60 @@ impl<'a> Download<'a> {
false,
).invoke(&client)?;
// Prepare the output path to use
let target = Self::prepare_path(
// 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;
}
}
}
// Prepare the download target and output path to use
#[cfg(feature = "archive")]
let output_dir = !extract;
#[cfg(not(feature = "archive"))]
let output_dir = false;
let mut target = Self::prepare_path(
&target,
metadata.metadata().name(),
&matcher_main,
output_dir,
);
let output_path = target.clone();
#[cfg(feature = "archive")] {
// Allocate an archive file, and update the download and target paths
if extract {
// TODO: select the extention dynamically
let archive_extention = ".tar";
// Allocate a temporary file to download the archive to
tmp_archive = Some(
TempBuilder::new()
.prefix(&format!(".{}-archive-", crate_name!()))
.suffix(archive_extention)
.tempfile()
.map_err(ExtractError::TempFile)?
);
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
if !matcher_main.force() {
@ -117,6 +172,18 @@ impl<'a> Download<'a> {
Some(metadata),
).invoke(&client, &progress_reader)?;
// Extract the downloaded file if working with an archive
#[cfg(feature = "archive")] {
if extract {
eprintln!("Extracting...");
// Extract the downloaded file
Archive::new(tmp_archive.unwrap().into_file())
.extract(output_path)
.map_err(ExtractError::Extract)?;
}
}
// Add the file to the history
#[cfg(feature = "history")]
history_tool::add(&matcher_main, file, true);
@ -130,6 +197,8 @@ impl<'a> Download<'a> {
/// This methods prepares a full file path to use for the file to
/// download, based on the current directory, the original file name,
/// and the user input.
/// If `file` is set to false, 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.
///
@ -143,12 +212,18 @@ impl<'a> Download<'a> {
target: &PathBuf,
name_hint: &str,
main_matcher: &MainMatcher,
file: bool,
) -> PathBuf {
// Select the path to use
let 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 {
target = target.parent().unwrap().to_path_buf();
}
// Ask to overwrite
if target.exists() && !main_matcher.force() {
if file && target.exists() && !main_matcher.force() {
eprintln!(
"The path '{}' already exists",
target.to_str().unwrap_or("?"),
@ -159,14 +234,27 @@ impl<'a> Download<'a> {
}
}
// Validate the parent directory exists
match target.parent() {
Some(parent) => if !parent.is_dir() {
{
// Get the deepest directory, as we have to ensure it exists
let dir = if file {
match target.parent() {
Some(parent) => parent,
None => quit_error_msg(
"invalid output file path",
ErrorHints::default(),
),
}
} else {
&target
};
// Ensure the directory exists
if !dir.is_dir() {
// Prompt to create them if not forced
if !main_matcher.force() {
eprintln!(
"The directory '{}' doesn't exists",
parent.to_str().unwrap_or("?"),
dir.to_str().unwrap_or("?"),
);
if !prompt_yes("Create it?", Some(true), main_matcher) {
println!("Download cancelled");
@ -175,16 +263,12 @@ impl<'a> Download<'a> {
}
// Create the parent directories
if let Err(err) = create_dir_all(parent) {
if let Err(err) = create_dir_all(dir) {
quit_error(err.context(
"failed to create parent directories for output file",
), ErrorHints::default());
}
},
None => quit_error_msg(
"invalid output file path",
ErrorHints::default(),
),
}
}
target
@ -280,6 +364,10 @@ pub enum Error {
#[fail(display = "")]
Download(#[cause] DownloadError),
/// An error occurred while extracting the file.
#[fail(display = "failed the extraction procedure")]
Extract(#[cause] ExtractError),
/// The given Send file has expired, or did never exist in the first place.
#[fail(display = "the file has expired or did never exist")]
Expired,
@ -308,3 +396,20 @@ impl From<DownloadError> for Error {
Error::Download(err)
}
}
impl From<ExtractError> for Error {
fn from(err: ExtractError) -> Error {
Error::Extract(err)
}
}
#[derive(Debug, Fail)]
pub enum ExtractError {
/// An error occurred while creating the temporary archive file.
#[fail(display = "failed to create temporary archive file")]
TempFile(#[cause] IoError),
/// Failed to extract the file contents to the target directory.
#[fail(display = "failed to extract archive contents to target directory")]
Extract(#[cause] IoError),
}

View file

@ -1,5 +1,3 @@
extern crate tempfile;
use std::fs::File;
use std::io::Error as IoError;
use std::path::Path;
@ -21,7 +19,7 @@ use prettytable::{
row::Row,
Table,
};
use self::tempfile::{
use tempfile::{
Builder as TempBuilder,
NamedTempFile,
};
@ -137,8 +135,7 @@ impl<'a> Upload<'a> {
// The temporary file is stored here, to ensure it's lifetime exceeds the upload process
let mut tmp_archive: Option<NamedTempFile> = None;
#[cfg(feature = "archive")]
{
#[cfg(feature = "archive")] {
// Determine whether to archive, ask if a directory was selected
let mut archive = matcher_upload.archive();
if !archive && path.is_dir() {
@ -244,15 +241,13 @@ impl<'a> Upload<'a> {
}
// Copy the URL in the user's clipboard
#[cfg(feature = "clipboard")]
{
#[cfg(feature = "clipboard")] {
if matcher_upload.copy() && set_clipboard(url.as_str().to_owned()).is_err() {
print_error_msg("failed to copy the URL to the clipboard");
}
}
#[cfg(feature = "archive")]
{
#[cfg(feature = "archive")] {
// Close the temporary zip file, to ensure it's removed
if let Some(tmp_archive) = tmp_archive.take() {
if let Err(err) = tmp_archive.close() {

View file

@ -0,0 +1,28 @@
use std::io::{
Error as IoError,
Read,
};
use std::path::Path;
use super::tar::Archive as TarArchive;
pub type Result<T> = ::std::result::Result<T, IoError>;
pub struct Archive<R: Read> {
/// The tar archive
inner: TarArchive<R>,
}
impl<R: Read> Archive<R> {
/// Construct a new archive extractor.
pub fn new(reader: R) -> Archive<R> {
Archive {
inner: TarArchive::new(reader),
}
}
/// Extract the archive to the given destination.
pub fn extract<P: AsRef<Path>>(&mut self, destination: P) -> Result<()> {
self.inner.unpack(destination)
}
}

View file

@ -1,5 +1,3 @@
extern crate tar;
use std::fs::File;
use std::io::{
Error as IoError,
@ -7,7 +5,7 @@ use std::io::{
};
use std::path::Path;
use self::tar::Builder as TarBuilder;
use super::tar::Builder as TarBuilder;
pub type Result<T> = ::std::result::Result<T, IoError>;

View file

@ -1 +1,4 @@
extern crate tar;
pub mod archive;
pub mod archiver;

View file

@ -5,6 +5,7 @@ use ffsend_api::url::Url;
use cmd::arg::{ArgPassword, ArgUrl, CmdArgOption};
use super::Matcher;
use util::env_var_present;
/// The download command matcher.
pub struct DownloadMatcher<'a> {
@ -35,6 +36,12 @@ impl<'a: 'b, 'b> DownloadMatcher<'a> {
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("./"))
}
/// Check whether to extract an archived file.
#[cfg(feature = "archive")]
pub fn extract(&self) -> bool {
self.matches.is_present("extract") || env_var_present("FFSEND_EXTRACT")
}
}
impl<'a> Matcher<'a> for DownloadMatcher<'a> {

View file

@ -72,7 +72,6 @@ impl<'a: 'b, 'b> UploadMatcher<'a> {
}
/// Check whether to archive the file to upload.
/// TODO: infer to use this flag if a directory is selected
#[cfg(feature = "archive")]
pub fn archive(&self) -> bool {
self.matches.is_present("archive") || env_var_present("FFSEND_ARCHIVE")

View file

@ -7,7 +7,8 @@ pub struct CmdDownload;
impl CmdDownload {
pub fn build<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("download")
// Build the subcommand
let mut cmd = SubCommand::with_name("download")
.about("Download files")
.visible_alias("d")
.visible_alias("down")
@ -20,6 +21,19 @@ impl CmdDownload {
.alias("out")
.alias("file")
.value_name("PATH")
.help("The output file or directory"))
.help("The output file or directory"));
// Optional archive support
#[cfg(feature = "archive")] {
cmd = cmd.arg(Arg::with_name("extract")
.long("extract")
.short("e")
.alias("archive")
.alias("arch")
.alias("a")
.help("Extract an archived file"))
}
cmd
}
}

View file

@ -43,7 +43,7 @@ impl CmdUpload {
.long("archive")
.short("a")
.alias("arch")
.help("Package the file as an archive"))
.help("Archive the upload in a single file"))
}
// Optional clipboard support
@ -52,7 +52,8 @@ impl CmdUpload {
.long("copy")
.short("c")
.help("Copy the share link to your clipboard"));
}
}
cmd
}
}

View file

@ -16,6 +16,7 @@ extern crate serde;
#[cfg(feature = "history")]
#[macro_use]
extern crate serde_derive;
extern crate tempfile;
mod action;
#[cfg(feature = "archive")]

View file

@ -561,6 +561,15 @@ pub fn format_duration(duration: impl Borrow<Duration>) -> String {
components.join("")
}
/// Format the given boolean, as `yes` or `no`.
pub fn format_bool(b: bool) -> &'static str {
if b {
"yes"
} else {
"no"
}
}
/// Get the name of the executable that was invoked.
pub fn exe_name() -> String {
current_exe()