use std::fs::File;
use std::io::{self, Error as IoError, Read, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use reqwest::header::AUTHORIZATION;
use reqwest::{Client, Response};
use super::metadata::{Error as MetadataError, Metadata as MetadataAction, MetadataResponse};
use crate::api::request::{ensure_success, ResponseError};
use crate::api::url::UrlBuilder;
use crate::api::Version;
use crate::crypto::key_set::KeySet;
use crate::crypto::sig::signature_encoded;
use crate::file::remote_file::RemoteFile;
#[cfg(feature = "send3")]
use crate::pipe::crypto::EceCrypt;
#[cfg(feature = "send2")]
use crate::pipe::crypto::GcmCrypt;
use crate::pipe::{
progress::{ProgressPipe, ProgressReporter},
prelude::*,
};
pub struct Download<'a> {
version: Version,
file: &'a RemoteFile,
target: PathBuf,
password: Option<String>,
check_exists: bool,
metadata_response: Option<MetadataResponse>,
}
impl<'a> Download<'a> {
pub fn new(
version: Version,
file: &'a RemoteFile,
target: PathBuf,
password: Option<String>,
check_exists: bool,
metadata_response: Option<MetadataResponse>,
) -> Self {
Self {
version,
file,
target,
password,
check_exists,
metadata_response,
}
}
pub fn invoke(
mut self,
client: &Client,
reporter: Option<Arc<Mutex<ProgressReporter>>>,
) -> Result<(), Error> {
let mut key = KeySet::from(self.file, self.password.as_ref());
let metadata: MetadataResponse = if self.metadata_response.is_some() {
self.metadata_response.take().unwrap()
} else {
MetadataAction::new(self.file, self.password.clone(), self.check_exists)
.invoke(&client)?
};
if let Some(iv) = metadata.metadata().iv() {
key.set_iv(iv);
}
let path = self.decide_path(metadata.metadata().name());
let path_str = path.to_str().unwrap_or("?").to_owned();
let out = File::create(path)
.map_err(|err| Error::File(path_str.clone(), FileError::Create(err)))?;
let (reader, len) = self.create_file_reader(&key, &metadata, &client)?;
let writer = self
.create_writer(out, len, &key, reporter.clone())
.map_err(|err| Error::File(path_str.clone(), err))?;
self.download(reader, writer, len, reporter)?;
Ok(())
}
fn decide_path(&self, name_hint: &str) -> PathBuf {
if self.target.is_file() {
return self.target.clone();
}
if self.target.is_dir() {
return self.target.join(name_hint);
}
if self.target.parent().map(|p| p.is_dir()).unwrap_or(false) {
return self.target.clone();
}
panic!("Invalid (non-existing) output path given, not yet supported");
}
fn create_file_reader(
&self,
key: &KeySet,
metadata: &MetadataResponse,
client: &Client,
) -> Result<(Response, u64), DownloadError> {
let sig = signature_encoded(key.auth_key().unwrap(), metadata.nonce())
.map_err(|_| DownloadError::ComputeSignature)?;
let response = client
.get(UrlBuilder::api_download(self.file))
.header(AUTHORIZATION.as_str(), format!("send-v1 {}", sig))
.send()
.map_err(|_| DownloadError::Request)?;
ensure_success(&response).map_err(DownloadError::Response)?;
let len = metadata.size();
Ok((response, len))
}
fn create_writer(
&self,
file: File,
len: u64,
key: &KeySet,
reporter: Option<Arc<Mutex<ProgressReporter>>>,
) -> Result<impl Write, FileError> {
let writer: Box<dyn Write> = match self.version {
#[cfg(feature = "send2")]
Version::V2 => {
let decrypt = GcmCrypt::decrypt(len as usize, key.file_key().unwrap(), key.iv());
let writer = decrypt.writer(Box::new(file));
Box::new(writer)
}
#[cfg(feature = "send3")]
Version::V3 => {
let ikm = key.secret().to_vec();
let decrypt = EceCrypt::decrypt(len as usize, ikm);
let writer = decrypt.writer(Box::new(file));
Box::new(writer)
}
};
let progress = ProgressPipe::zero(len as u64, reporter);
let writer = progress.writer(writer);
Ok(writer)
}
fn download<R, W>(
&self,
mut reader: R,
mut writer: W,
len: u64,
reporter: Option<Arc<Mutex<ProgressReporter>>>,
) -> Result<(), DownloadError>
where R: Read,
W: Write,
{
if let Some(reporter) = reporter.as_ref() {
reporter
.lock()
.map_err(|_| DownloadError::Progress)?
.start(len);
}
io::copy(&mut reader, &mut writer).map_err(|_| DownloadError::Download)?;
if let Some(reporter) = reporter.as_ref() {
reporter
.lock()
.map_err(|_| DownloadError::Progress)?
.finish();
}
Ok(())
}
}
#[derive(Fail, Debug)]
pub enum Error {
#[fail(display = "failed to fetch file metadata")]
Meta(#[cause] MetadataError),
#[fail(display = "the file has expired or did never exist")]
Expired,
#[fail(display = "missing password, password required")]
PasswordRequired,
#[fail(display = "failed to download the file")]
Download(#[cause] DownloadError),
#[fail(display = "failed to decrypt the downloaded file")]
Decrypt,
#[fail(display = "couldn't use the target file at '{}'", _0)]
File(String, #[cause] FileError),
}
impl From<MetadataError> for Error {
fn from(err: MetadataError) -> Error {
match err {
MetadataError::Expired => Error::Expired,
MetadataError::PasswordRequired => Error::PasswordRequired,
err => Error::Meta(err),
}
}
}
impl From<DownloadError> for Error {
fn from(err: DownloadError) -> Error {
Error::Download(err)
}
}
#[derive(Fail, Debug)]
pub enum DownloadError {
#[fail(display = "failed to compute cryptographic signature")]
ComputeSignature,
#[fail(display = "failed to request file download")]
Request,
#[fail(display = "bad response from server while requesting download")]
Response(#[cause] ResponseError),
#[fail(display = "failed to update download progress")]
Progress,
#[fail(display = "failed to download the file")]
Download,
}
#[derive(Fail, Debug)]
pub enum FileError {
#[fail(display = "failed to create or replace the file")]
Create(#[cause] IoError),
#[fail(display = "failed to create file decryptor")]
EncryptedWriter,
}