Add file info action to API
This commit is contained in:
parent
14f24c9603
commit
f8e5dead9c
3 changed files with 263 additions and 0 deletions
252
api/src/action/info.rs
Normal file
252
api/src/action/info.rs
Normal file
|
@ -0,0 +1,252 @@
|
|||
// TODO: define redirect policy
|
||||
|
||||
use std::cmp::max;
|
||||
|
||||
use reqwest::{
|
||||
Client,
|
||||
Error as ReqwestError,
|
||||
StatusCode,
|
||||
};
|
||||
use reqwest::header::Authorization;
|
||||
|
||||
use crypto::b64;
|
||||
use crypto::key_set::KeySet;
|
||||
use crypto::sig::signature_encoded;
|
||||
use ext::status_code::StatusCodeExt;
|
||||
use file::remote_file::RemoteFile;
|
||||
|
||||
/// The name of the header that is used for the authentication nonce.
|
||||
const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate";
|
||||
|
||||
/// An action to fetch info of a shared file.
|
||||
pub struct Info<'a> {
|
||||
/// The remote file to fetch the info for.
|
||||
file: &'a RemoteFile,
|
||||
|
||||
// TODO: use this?
|
||||
/// The authentication nonce.
|
||||
/// May be an empty vector if the nonce is unknown.
|
||||
nonce: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a> Info<'a> {
|
||||
/// Construct a new info action for the given remote file.
|
||||
pub fn new(file: &'a RemoteFile, nonce: Option<Vec<u8>>) -> Self {
|
||||
Self {
|
||||
file,
|
||||
nonce: nonce.unwrap_or(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Invoke the info action.
|
||||
pub fn invoke(mut self, client: &Client) -> Result<InfoResponse, Error> {
|
||||
// Create a key set for the file
|
||||
let key = KeySet::from(self.file, None);
|
||||
|
||||
// Fetch the authentication nonce if not set yet
|
||||
if self.nonce.is_empty() {
|
||||
self.nonce = self.fetch_auth_nonce(client)?;
|
||||
}
|
||||
|
||||
// Compute a signature
|
||||
let sig = signature_encoded(key.auth_key().unwrap(), &self.nonce)
|
||||
.map_err(|_| PrepareError::ComputeSignature)?;
|
||||
|
||||
// Send the info request
|
||||
self.fetch_info(client, sig)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
/// Fetch the authentication nonce for the file from the remote server.
|
||||
fn fetch_auth_nonce(&self, client: &Client) -> Result<Vec<u8>, AuthError> {
|
||||
// Get the download URL, and parse the nonce
|
||||
let download_url = self.file.download_url(false);
|
||||
let response = client.get(download_url)
|
||||
.send()
|
||||
.map_err(|_| AuthError::NonceReq)?;
|
||||
|
||||
// Validate the status code
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
// TODO: should we check here whether a 404 is returned?
|
||||
// // Handle expired files
|
||||
// if status == FILE_EXPIRED_STATUS {
|
||||
// return Err(Error::Expired);
|
||||
// } else {
|
||||
return Err(AuthError::NonceReqStatus(status, status.err_text()).into());
|
||||
// }
|
||||
}
|
||||
|
||||
// Get the authentication nonce
|
||||
b64::decode(
|
||||
response.headers()
|
||||
.get_raw(HEADER_AUTH_NONCE)
|
||||
.ok_or(AuthError::NoNonceHeader)?
|
||||
.one()
|
||||
.ok_or(AuthError::MalformedNonce)
|
||||
.and_then(|line| String::from_utf8(line.to_vec())
|
||||
.map_err(|_| AuthError::MalformedNonce)
|
||||
)?
|
||||
.split_terminator(" ")
|
||||
.skip(1)
|
||||
.next()
|
||||
.ok_or(AuthError::MalformedNonce)?
|
||||
).map_err(|_| AuthError::MalformedNonce.into())
|
||||
}
|
||||
|
||||
/// Send the request for fetching the remote file info.
|
||||
fn fetch_info(&self, client: &Client, sig: String)
|
||||
-> Result<InfoResponse, InfoError>
|
||||
{
|
||||
// Get the params URL, and send the change
|
||||
let url = self.file.api_params_url();
|
||||
let mut response = client.get(url)
|
||||
// TODO: is this header required?
|
||||
.header(Authorization(
|
||||
format!("send-v1 {}", sig)
|
||||
))
|
||||
.send()
|
||||
.map_err(|_| InfoError::Request)?;
|
||||
|
||||
// Validate the status code
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(InfoError::RequestStatus(status, status.err_text()).into());
|
||||
}
|
||||
|
||||
// Decode the JSON response
|
||||
let response: InfoResponse = match response.json() {
|
||||
Ok(response) => response,
|
||||
Err(err) => return Err(InfoError::Decode(err)),
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// The file info response.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct InfoResponse {
|
||||
/// The download limit.
|
||||
#[serde(rename = "dlimit")]
|
||||
download_limit: usize,
|
||||
|
||||
/// The total number of times the file has been downloaded.
|
||||
#[serde(rename = "dtotal")]
|
||||
download_count: usize,
|
||||
|
||||
/// The time to live for this file in milliseconds.
|
||||
#[serde(rename = "ttl")]
|
||||
ttl: u64,
|
||||
}
|
||||
|
||||
impl InfoResponse {
|
||||
/// Get the number of times this file has been downloaded.
|
||||
pub fn download_count(&self) -> usize {
|
||||
self.download_count
|
||||
}
|
||||
|
||||
/// Get the maximum number of times the file may be downloaded.
|
||||
pub fn download_limit(&self) -> usize {
|
||||
self.download_limit
|
||||
}
|
||||
|
||||
/// Get the number of times this file may still be downloaded.
|
||||
pub fn download_left(&self) -> usize {
|
||||
max(self.download_limit() - self.download_count(), 0)
|
||||
}
|
||||
|
||||
/// Get the time to live for this file, in milliseconds from the time the
|
||||
/// request was made.
|
||||
pub fn ttl_millis(&self) -> u64 {
|
||||
self.ttl
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum Error {
|
||||
/// An error occurred while preparing the action.
|
||||
#[fail(display = "Failed to prepare setting the parameters")]
|
||||
Prepare(#[cause] PrepareError),
|
||||
|
||||
// /// The given Send file has expired, or did never exist in the first place.
|
||||
// /// Therefore the file could not be downloaded.
|
||||
// #[fail(display = "The file has expired or did never exist")]
|
||||
// Expired,
|
||||
|
||||
/// An error has occurred while sending the parameter change request to
|
||||
/// the server.
|
||||
#[fail(display = "Failed to send the parameter change request")]
|
||||
Info(#[cause] InfoError),
|
||||
}
|
||||
|
||||
impl From<PrepareError> for Error {
|
||||
fn from(err: PrepareError) -> Error {
|
||||
Error::Prepare(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AuthError> for Error {
|
||||
fn from(err: AuthError) -> Error {
|
||||
PrepareError::Auth(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InfoError> for Error {
|
||||
fn from(err: InfoError) -> Error {
|
||||
Error::Info(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum PrepareError {
|
||||
/// Failed authenticating, needed to change the parameters.
|
||||
#[fail(display = "Failed to authenticate")]
|
||||
Auth(#[cause] AuthError),
|
||||
|
||||
/// An error occurred while computing the cryptographic signature.
|
||||
#[fail(display = "Failed to compute cryptographic signature")]
|
||||
ComputeSignature,
|
||||
}
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum AuthError {
|
||||
/// Sending the request to gather the authentication encryption nonce
|
||||
/// failed.
|
||||
#[fail(display = "Failed to request authentication nonce")]
|
||||
NonceReq,
|
||||
|
||||
/// The response for fetching the authentication encryption nonce
|
||||
/// indicated an error and wasn't successful.
|
||||
#[fail(display = "Bad HTTP response '{}' while requesting authentication nonce", _1)]
|
||||
NonceReqStatus(StatusCode, String),
|
||||
|
||||
/// No authentication encryption nonce was included in the response
|
||||
/// from the server, it was missing.
|
||||
#[fail(display = "Missing authentication nonce in server response")]
|
||||
NoNonceHeader,
|
||||
|
||||
/// The authentication encryption nonce from the response malformed or
|
||||
/// empty.
|
||||
/// Maybe the server responded with a new format that isn't supported yet
|
||||
/// by this client.
|
||||
#[fail(display = "Received malformed authentication nonce")]
|
||||
MalformedNonce,
|
||||
}
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum InfoError {
|
||||
/// Sending the request to fetch the file info failed.
|
||||
#[fail(display = "Failed to send file info request")]
|
||||
Request,
|
||||
|
||||
/// The response fetching the file info indicated an error and wasn't
|
||||
/// successful.
|
||||
#[fail(display = "Bad HTTP response '{}' while fetching the file info", _1)]
|
||||
RequestStatus(StatusCode, String),
|
||||
|
||||
/// Failed to decode the info response from the server.
|
||||
/// Maybe the server responded with data from a newer API version.
|
||||
#[fail(display = "Failed to decode info response")]
|
||||
Decode(#[cause] ReqwestError),
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
pub mod download;
|
||||
pub mod info;
|
||||
pub mod params;
|
||||
pub mod password;
|
||||
pub mod upload;
|
||||
|
|
|
@ -219,6 +219,16 @@ impl RemoteFile {
|
|||
|
||||
url
|
||||
}
|
||||
|
||||
/// Get the API info URL of the file.
|
||||
pub fn api_info_url(&self) -> Url {
|
||||
// Get the share URL, and add the secret fragment
|
||||
let mut url = self.url.clone();
|
||||
url.set_path(format!("/api/info/{}", self.id).as_str());
|
||||
url.set_fragment(None);
|
||||
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
|
|
Loading…
Add table
Reference in a new issue