mirror of
https://github.com/timvisee/ffsend.git
synced 2025-10-06 02:29:57 +02:00
Start working on structs for download functionallity
This commit is contained in:
parent
b57e85a8ec
commit
3f3f12aa70
6 changed files with 513 additions and 2 deletions
314
api/src/action/download.rs
Normal file
314
api/src/action/download.rs
Normal file
|
@ -0,0 +1,314 @@
|
|||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use mime_guess::{get_mime_type, Mime};
|
||||
use openssl::symm::encrypt_aead;
|
||||
use reqwest::{
|
||||
Client,
|
||||
Error as ReqwestError,
|
||||
Request,
|
||||
};
|
||||
use reqwest::header::Authorization;
|
||||
use reqwest::mime::APPLICATION_OCTET_STREAM;
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use url::Url;
|
||||
|
||||
use crypto::key_set::KeySet;
|
||||
use reader::{
|
||||
EncryptedFileReaderTagged,
|
||||
ExactLengthReader,
|
||||
ProgressReader,
|
||||
ProgressReporter,
|
||||
};
|
||||
use file::file::File as SendFile;
|
||||
use file::metadata::{Metadata, XFileMetadata};
|
||||
|
||||
pub type Result<T> = ::std::result::Result<T, DownloadError>;
|
||||
|
||||
/// A file upload action to a Send server.
|
||||
pub struct Download {
|
||||
/// The Send host to upload the file to.
|
||||
host: Url,
|
||||
|
||||
/// The file to upload.
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl Download {
|
||||
/// Construct a new upload action.
|
||||
pub fn new(host: Url, path: PathBuf) -> Self {
|
||||
Self {
|
||||
host,
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Invoke the upload action.
|
||||
pub fn invoke(
|
||||
self,
|
||||
client: &Client,
|
||||
reporter: Arc<Mutex<ProgressReporter>>,
|
||||
) -> Result<SendFile> {
|
||||
// Create file data, generate a key
|
||||
let file = FileData::from(Box::new(&self.path))?;
|
||||
let key = KeySet::generate(true);
|
||||
|
||||
// Crpate metadata and a file reader
|
||||
let metadata = self.create_metadata(&key, &file)?;
|
||||
let reader = self.create_reader(&key, reporter.clone())?;
|
||||
let reader_len = reader.len().unwrap();
|
||||
|
||||
// Create the request to send
|
||||
let req = self.create_request(
|
||||
client,
|
||||
&key,
|
||||
metadata,
|
||||
reader,
|
||||
);
|
||||
|
||||
// Start the reporter
|
||||
reporter.lock()
|
||||
.expect("unable to start progress, failed to get lock")
|
||||
.start(reader_len);
|
||||
|
||||
// Execute the request
|
||||
let result = self.execute_request(req, client, &key);
|
||||
|
||||
// Mark the reporter as finished
|
||||
reporter.lock()
|
||||
.expect("unable to finish progress, failed to get lock")
|
||||
.finish();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Create a blob of encrypted metadata.
|
||||
fn create_metadata(&self, key: &KeySet, file: &FileData)
|
||||
-> Result<Vec<u8>>
|
||||
{
|
||||
// Construct the metadata
|
||||
let metadata = Metadata::from(
|
||||
key.iv(),
|
||||
file.name().to_owned(),
|
||||
file.mime().clone(),
|
||||
).to_json().into_bytes();
|
||||
|
||||
// Encrypt the metadata
|
||||
let mut metadata_tag = vec![0u8; 16];
|
||||
let mut metadata = match encrypt_aead(
|
||||
KeySet::cipher(),
|
||||
key.meta_key().unwrap(),
|
||||
Some(&[0u8; 12]),
|
||||
&[],
|
||||
&metadata,
|
||||
&mut metadata_tag,
|
||||
) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(_) => return Err(DownloadError::EncryptionError),
|
||||
};
|
||||
|
||||
// Append the encryption tag
|
||||
metadata.append(&mut metadata_tag);
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Create a reader that reads the file as encrypted stream.
|
||||
fn create_reader(
|
||||
&self,
|
||||
key: &KeySet,
|
||||
reporter: Arc<Mutex<ProgressReporter>>,
|
||||
) -> Result<EncryptedReader> {
|
||||
// Open the file
|
||||
let file = match File::open(self.path.as_path()) {
|
||||
Ok(file) => file,
|
||||
Err(_) => return Err(DownloadError::FileError),
|
||||
};
|
||||
|
||||
// Create an encrypted reader
|
||||
let reader = match EncryptedFileReaderTagged::new(
|
||||
file,
|
||||
KeySet::cipher(),
|
||||
key.file_key().unwrap(),
|
||||
key.iv(),
|
||||
) {
|
||||
Ok(reader) => reader,
|
||||
Err(_) => return Err(DownloadError::EncryptionError),
|
||||
};
|
||||
|
||||
// Buffer the encrypted reader
|
||||
let reader = BufReader::new(reader);
|
||||
|
||||
// Wrap into the encrypted reader
|
||||
let mut reader = ProgressReader::new(reader)
|
||||
.expect("failed to create progress reader");
|
||||
|
||||
// Initialize and attach the reporter
|
||||
reader.set_reporter(reporter);
|
||||
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
/// Build the request that will be send to the server.
|
||||
fn create_request(
|
||||
&self,
|
||||
client: &Client,
|
||||
key: &KeySet,
|
||||
metadata: Vec<u8>,
|
||||
reader: EncryptedReader,
|
||||
) -> Request {
|
||||
// Get the reader length
|
||||
let len = reader.len().expect("failed to get reader length");
|
||||
|
||||
// Configure a form to send
|
||||
let part = Part::reader_with_length(reader, len)
|
||||
// .file_name(file.name())
|
||||
.mime(APPLICATION_OCTET_STREAM);
|
||||
let form = Form::new()
|
||||
.part("data", part);
|
||||
|
||||
// Define the URL to call
|
||||
let url = self.host.join("api/upload").expect("invalid host");
|
||||
|
||||
// Build the request
|
||||
client.post(url.as_str())
|
||||
.header(Authorization(
|
||||
format!("send-v1 {}", key.auth_key_encoded().unwrap())
|
||||
))
|
||||
.header(XFileMetadata::from(&metadata))
|
||||
.multipart(form)
|
||||
.build()
|
||||
.expect("failed to build an API request")
|
||||
}
|
||||
|
||||
/// Execute the given request, and create a file object that represents the
|
||||
/// uploaded file.
|
||||
fn execute_request(&self, req: Request, client: &Client, key: &KeySet)
|
||||
-> Result<SendFile>
|
||||
{
|
||||
// Execute the request
|
||||
let mut res = match client.execute(req) {
|
||||
Ok(res) => res,
|
||||
Err(err) => return Err(DownloadError::RequestError(err)),
|
||||
};
|
||||
|
||||
// Decode the response
|
||||
let res: DownloadResponse = match res.json() {
|
||||
Ok(res) => res,
|
||||
Err(_) => return Err(DownloadError::DecodeError),
|
||||
};
|
||||
|
||||
// Transform the responce into a file object
|
||||
Ok(res.into_file(self.host.clone(), &key))
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that may occur in the upload action.
|
||||
#[derive(Debug)]
|
||||
pub enum DownloadError {
|
||||
/// The given file is not not an existing file.
|
||||
/// Maybe it is a directory, or maybe it doesn't exist.
|
||||
NotAFile,
|
||||
|
||||
/// An error occurred while opening or reading a file.
|
||||
FileError,
|
||||
|
||||
/// An error occurred while encrypting the file.
|
||||
EncryptionError,
|
||||
|
||||
/// An error occurred while while processing the request.
|
||||
/// This also covers things like HTTP 404 errors.
|
||||
RequestError(ReqwestError),
|
||||
|
||||
/// An error occurred while decoding the response data.
|
||||
DecodeError,
|
||||
}
|
||||
|
||||
/// The response from the server after a file has been uploaded.
|
||||
/// This response contains the file ID and owner key, to manage the file.
|
||||
///
|
||||
/// It also contains the download URL, although an additional secret is
|
||||
/// required.
|
||||
///
|
||||
/// The download URL can be generated using `download_url()` which will
|
||||
/// include the required secret in the URL.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DownloadResponse {
|
||||
/// The file ID.
|
||||
id: String,
|
||||
|
||||
/// The URL the file is reachable at.
|
||||
/// This includes the file ID, but does not include the secret.
|
||||
url: String,
|
||||
|
||||
/// The owner key, used to do further file modifications.
|
||||
owner: String,
|
||||
}
|
||||
|
||||
impl DownloadResponse {
|
||||
/// Convert this response into a file object.
|
||||
///
|
||||
/// The `host` and `key` must be given.
|
||||
pub fn into_file(self, host: Url, key: &KeySet) -> SendFile {
|
||||
SendFile::new_now(
|
||||
self.id,
|
||||
host,
|
||||
Url::parse(&self.url)
|
||||
.expect("upload response URL parse error"),
|
||||
key.secret().to_vec(),
|
||||
self.owner,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct that holds various file properties, such as it's name and it's
|
||||
/// mime type.
|
||||
struct FileData<'a> {
|
||||
/// The file name.
|
||||
name: &'a str,
|
||||
|
||||
/// The file mime type.
|
||||
mime: Mime,
|
||||
}
|
||||
|
||||
impl<'a> FileData<'a> {
|
||||
/// Create a file data object, from the file at the given path.
|
||||
pub fn from(path: Box<&'a Path>) -> Result<Self> {
|
||||
// Make sure the given path is a file
|
||||
if !path.is_file() {
|
||||
return Err(DownloadError::NotAFile);
|
||||
}
|
||||
|
||||
// Get the file name
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_str().expect("failed to convert string"),
|
||||
None => return Err(DownloadError::FileError),
|
||||
};
|
||||
|
||||
// Get the file extention
|
||||
// TODO: handle cases where the file doesn't have an extention
|
||||
let ext = match path.extension() {
|
||||
Some(ext) => ext.to_str().expect("failed to convert string"),
|
||||
None => return Err(DownloadError::FileError),
|
||||
};
|
||||
|
||||
Ok(
|
||||
Self {
|
||||
name,
|
||||
mime: get_mime_type(ext),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the file name.
|
||||
pub fn name(&self) -> &str {
|
||||
self.name
|
||||
}
|
||||
|
||||
/// Get the file mime type.
|
||||
pub fn mime(&self) -> &Mime {
|
||||
&self.mime
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
//pub mod download;
|
||||
pub mod upload;
|
||||
|
|
|
@ -1,10 +1,25 @@
|
|||
extern crate chrono;
|
||||
extern crate regex;
|
||||
|
||||
use url::Url;
|
||||
use url::{
|
||||
ParseError as UrlParseError,
|
||||
Url,
|
||||
};
|
||||
use self::chrono::{DateTime, Utc};
|
||||
use self::regex::Regex;
|
||||
|
||||
use crypto::b64;
|
||||
|
||||
/// A pattern for Send download URL paths, capturing the file ID.
|
||||
// TODO: match any sub-path?
|
||||
// TODO: match URL-safe base64 chars for the file ID?
|
||||
// TODO: constrain the ID length?
|
||||
const DOWNLOAD_PATH_PATTERN: &'static str = r"$/?download/([[:alnum:]]+={0,3})/?^";
|
||||
|
||||
/// A pattern for Send download URL fragments, capturing the file secret.
|
||||
// TODO: constrain the secret length?
|
||||
const DOWNLOAD_FRAGMENT_PATTERN: &'static str = r"$([a-zA-Z0-9-_+\/]+)?\s*^";
|
||||
|
||||
/// A struct representing an uploaded file on a Send host.
|
||||
///
|
||||
/// The struct contains the file ID, the file URL, the key that is required
|
||||
|
@ -87,3 +102,104 @@ impl File {
|
|||
url
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: merge this struct with `File`.
|
||||
pub struct DownloadFile {
|
||||
/// The ID of the file on that server.
|
||||
id: String,
|
||||
|
||||
/// The host the file was uploaded to.
|
||||
host: Url,
|
||||
|
||||
/// The file URL that was provided by the server.
|
||||
url: Url,
|
||||
|
||||
/// The secret key that is required to download the file.
|
||||
secret: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DownloadFile {
|
||||
/// Construct a new instance.
|
||||
pub fn new(
|
||||
id: String,
|
||||
host: Url,
|
||||
url: Url,
|
||||
secret: Vec<u8>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
host,
|
||||
url,
|
||||
secret,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to parse the given Send download URL.
|
||||
///
|
||||
/// The given URL is matched against a Send download URL pattern,
|
||||
/// this does not check whether the host is a valid and online Send host.
|
||||
///
|
||||
/// If the URL fragmet contains a file secret, it is also parsed.
|
||||
/// If it does not, the secret is left empty and must be specified manually.
|
||||
pub fn parse_url(url: String) -> Result<DownloadFile, FileParseError> {
|
||||
// Try to parse as an URL
|
||||
let url = Url::parse(&url)
|
||||
.map_err(|err| FileParseError::UrlFormatError(err))?;
|
||||
|
||||
// Build the host
|
||||
let mut host = url.clone();
|
||||
host.set_fragment(None);
|
||||
host.set_query(None);
|
||||
host.set_path("");
|
||||
|
||||
// Validate the path, get the file ID
|
||||
let re_path = Regex::new(DOWNLOAD_PATH_PATTERN).unwrap();
|
||||
let id = re_path.captures(url.path())
|
||||
.ok_or(FileParseError::InvalidDownloadUrl)?[1]
|
||||
.to_owned();
|
||||
|
||||
// Get the file secret
|
||||
let mut secret = Vec::new();
|
||||
if let Some(fragment) = url.fragment() {
|
||||
let re_fragment = Regex::new(DOWNLOAD_FRAGMENT_PATTERN).unwrap();
|
||||
if let Some(raw) = re_fragment.captures(fragment)
|
||||
.ok_or(FileParseError::InvalidSecret)?
|
||||
.get(1)
|
||||
{
|
||||
secret = b64::decode(raw.as_str())
|
||||
.map_err(|_| FileParseError::InvalidSecret)?
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the file
|
||||
Ok(Self::new(
|
||||
id,
|
||||
host,
|
||||
url,
|
||||
secret,
|
||||
))
|
||||
}
|
||||
|
||||
/// Check whether a file secret is set.
|
||||
/// This secret must be set to decrypt a downloaded Send file.
|
||||
pub fn has_secret(&self) -> bool {
|
||||
!self.secret.is_empty()
|
||||
}
|
||||
|
||||
/// Set the secret for this file.
|
||||
/// An empty vector will clear the secret.
|
||||
pub fn set_secret(&mut self, secret: Vec<u8>) {
|
||||
self.secret = secret;
|
||||
}
|
||||
}
|
||||
|
||||
pub enum FileParseError {
|
||||
/// An URL format error.
|
||||
UrlFormatError(UrlParseError),
|
||||
|
||||
/// An error for an invalid download URL format.
|
||||
InvalidDownloadUrl,
|
||||
|
||||
/// An error for an invalid secret format, if an URL fragmet exists.
|
||||
InvalidSecret,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue