Переглянути джерело

Add file info action to API

timvisee 7 роки тому
батько
коміт
f8e5dead9c
3 змінених файлів з 263 додано та 0 видалено
  1. 252 0
      api/src/action/info.rs
  2. 1 0
      api/src/action/mod.rs
  3. 10 0
      api/src/file/remote_file.rs

+ 252 - 0
api/src/action/info.rs

@@ -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 - 0
api/src/action/mod.rs

@@ -1,4 +1,5 @@
 pub mod download;
+pub mod info;
 pub mod params;
 pub mod password;
 pub mod upload;

+ 10 - 0
api/src/file/remote_file.rs

@@ -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)]