1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
use std::cmp::max;

use reqwest::Error as ReqwestError;

use crate::api::data::{Error as DataError, OwnedData};
use crate::api::nonce::{request_nonce, NonceError};
use crate::api::request::{ensure_success, ResponseError};
use crate::api::url::UrlBuilder;
use crate::client::Client;
use crate::file::remote_file::RemoteFile;

/// An action to fetch info of a shared file.
///
/// This API specification for this action is compatible with both Firefox Send v2 and v3.
pub struct Info<'a> {
    /// The remote file to fetch the info for.
    file: &'a RemoteFile,

    /// 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_default(),
        }
    }

    /// Invoke the info action.
    pub fn invoke(mut self, client: &Client) -> Result<InfoResponse, Error> {
        // Fetch the authentication nonce if not set yet
        if self.nonce.is_empty() {
            self.nonce = self.fetch_auth_nonce(client)?;
        }

        // Create owned data, to send to the server for authentication
        let data = OwnedData::from(InfoData::new(), &self.file)
            .map_err(|err| -> PrepareError { err.into() })?;

        // Send the info request
        self.fetch_info(client, &data)
    }

    /// Fetch the authentication nonce for the file from the remote server.
    fn fetch_auth_nonce(&self, client: &Client) -> Result<Vec<u8>, Error> {
        request_nonce(client, UrlBuilder::download(self.file, false)).map_err(|err| err.into())
    }

    /// Send the request for fetching the remote file info.
    fn fetch_info(
        &self,
        client: &Client,
        data: &OwnedData<InfoData>,
    ) -> Result<InfoResponse, Error> {
        // Get the info URL, and send the request
        let url = UrlBuilder::api_info(self.file);
        let mut response = client
            .post(url)
            .json(&data)
            .send()
            .map_err(|_| InfoError::Request)?;

        // Ensure the response is successful
        ensure_success(&response)?;

        // Decode the JSON response
        let response: InfoResponse = match response.json() {
            Ok(response) => response,
            Err(err) => return Err(InfoError::Decode(err).into()),
        };

        Ok(response)
    }
}

/// The info data object.
/// This object is currently empty, as no additional data is sent to the
/// server.
#[derive(Debug, Serialize, Default)]
pub struct InfoData {}

impl InfoData {
    /// Constructor.
    pub fn new() -> Self {
        InfoData::default()
    }
}

/// 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 the action")]
    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 info request to the server.
    #[fail(display = "failed to send the file info request")]
    Info(#[cause] InfoError),
}

impl From<NonceError> for Error {
    fn from(err: NonceError) -> Error {
        match err {
            NonceError::Expired => Error::Expired,
            err => Error::Prepare(PrepareError::Auth(err)),
        }
    }
}

impl From<PrepareError> for Error {
    fn from(err: PrepareError) -> Error {
        Error::Prepare(err)
    }
}

impl From<ResponseError> for Error {
    fn from(err: ResponseError) -> Error {
        match err {
            ResponseError::Expired => Error::Expired,
            err => Error::Info(InfoError::Response(err)),
        }
    }
}

impl From<InfoError> for Error {
    fn from(err: InfoError) -> Error {
        Error::Info(err)
    }
}

#[derive(Debug, Fail)]
pub enum InfoDataError {
    /// Some error occurred while trying to wrap the info data in an
    /// owned object, which is required for authentication on the server.
    /// The wrapped error further described the problem.
    #[fail(display = "")]
    Owned(#[cause] DataError),
}

#[derive(Fail, Debug)]
pub enum PrepareError {
    /// Failed authenticating, needed to fetch the info
    #[fail(display = "failed to authenticate")]
    Auth(#[cause] NonceError),

    /// An error occurred while building the info data that will be
    /// send to the server.
    #[fail(display = "invalid parameters")]
    InfoData(#[cause] InfoDataError),
}

impl From<DataError> for PrepareError {
    fn from(err: DataError) -> PrepareError {
        PrepareError::InfoData(InfoDataError::Owned(err))
    }
}

#[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 server responded with an error while fetching the file info.
    #[fail(display = "bad response from server while fetching file info")]
    Response(#[cause] ResponseError),

    /// 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),
}