ffsend_api/file/
remote_file.rs

1extern crate chrono;
2extern crate regex;
3
4use self::chrono::{DateTime, Duration, Utc};
5use self::regex::Regex;
6use thiserror::Error;
7use url::{ParseError as UrlParseError, Url};
8
9use crate::api::url::UrlBuilder;
10use crate::config::SEND_DEFAULT_EXPIRE_TIME;
11use crate::crypto::b64;
12
13/// A pattern for share URL paths, capturing the file ID.
14// TODO: match any sub-path?
15// TODO: match URL-safe base64 chars for the file ID?
16// TODO: constrain the ID length?
17const SHARE_PATH_PATTERN: &str = r"^/?download/([[:alnum:]]{8,}={0,3})/?$";
18
19/// A pattern for share URL fragments, capturing the file secret.
20// TODO: constrain the secret length?
21const SHARE_FRAGMENT_PATTERN: &str = r"^([a-zA-Z0-9-_+/]+)?\s*$";
22
23/// A struct representing an uploaded file on a Send host.
24///
25/// The struct contains the file ID, the file URL, the key that is required
26/// in combination with the file, and the owner key.
27#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct RemoteFile {
29    /// The ID of the file on that server.
30    id: String,
31
32    /// The time the file was uploaded at, if known.
33    upload_at: Option<DateTime<Utc>>,
34
35    /// The time the file will expire at.
36    expire_at: DateTime<Utc>,
37
38    /// Define whether the expiry time is uncertain.
39    expire_uncertain: bool,
40
41    /// The host the file was uploaded to.
42    host: Url,
43
44    /// The file URL that was provided by the server.
45    url: Url,
46
47    /// The secret key that is required to download the file.
48    secret: Vec<u8>,
49
50    /// The owner key, that can be used to manage the file on the server.
51    owner_token: Option<String>,
52}
53
54impl RemoteFile {
55    /// Construct a new file.
56    pub fn new(
57        id: String,
58        upload_at: Option<DateTime<Utc>>,
59        expire_at: Option<DateTime<Utc>>,
60        host: Url,
61        url: Url,
62        secret: Vec<u8>,
63        owner_token: Option<String>,
64    ) -> Self {
65        // Assign the default expiry time if uncetain
66        let expire_uncertain = expire_at.is_none();
67        let expire_at =
68            expire_at.unwrap_or(Utc::now() + Duration::seconds(SEND_DEFAULT_EXPIRE_TIME as i64));
69
70        // Build the object
71        Self {
72            id,
73            upload_at,
74            expire_at,
75            expire_uncertain,
76            host,
77            url,
78            secret,
79            owner_token,
80        }
81    }
82
83    /// Construct a new file, that was created at this exact time.
84    /// This will set the file expiration time
85    pub fn new_now(
86        id: String,
87        host: Url,
88        url: Url,
89        secret: Vec<u8>,
90        owner_token: Option<String>,
91    ) -> Self {
92        // Get the current time
93        let now = Utc::now();
94        let expire_at = now + Duration::seconds(SEND_DEFAULT_EXPIRE_TIME as i64);
95
96        // Construct and return
97        Self::new(
98            id,
99            Some(now),
100            Some(expire_at),
101            host,
102            url,
103            secret,
104            owner_token,
105        )
106    }
107
108    /// Try to parse the given share URL.
109    ///
110    /// The given URL is matched against a share URL pattern,
111    /// this does not check whether the host is a valid and online host.
112    ///
113    /// If the URL fragmet contains a file secret, it is also parsed.
114    /// If it does not, the secret is left empty and must be specified
115    /// manually.
116    ///
117    /// An optional owner token may be given.
118    pub fn parse_url(url: Url, owner_token: Option<String>) -> Result<RemoteFile, FileParseError> {
119        // Build the host
120        let mut host = url.clone();
121        host.set_fragment(None);
122        host.set_query(None);
123        host.set_path("");
124
125        // Validate the path, get the file ID
126        let re_path = Regex::new(SHARE_PATH_PATTERN).unwrap();
127        let id = re_path
128            .captures(url.path())
129            .ok_or(FileParseError::InvalidUrl)?[1]
130            .trim()
131            .to_owned();
132
133        // Get the file secret
134        let mut secret = Vec::new();
135        if let Some(fragment) = url.fragment() {
136            let re_fragment = Regex::new(SHARE_FRAGMENT_PATTERN).unwrap();
137            if let Some(raw) = re_fragment
138                .captures(fragment)
139                .ok_or(FileParseError::InvalidSecret)?
140                .get(1)
141            {
142                secret =
143                    b64::decode(raw.as_str().trim()).map_err(|_| FileParseError::InvalidSecret)?
144            }
145        }
146
147        // Construct the file
148        Ok(Self::new(id, None, None, host, url, secret, owner_token))
149    }
150
151    /// Get the file ID.
152    pub fn id(&self) -> &str {
153        &self.id
154    }
155
156    /// Get the time the file will expire after.
157    /// Note that this time may not be correct as it may have been guessed,
158    /// see `expire_uncertain()`.
159    pub fn expire_at(&self) -> DateTime<Utc> {
160        self.expire_at
161    }
162
163    /// Get the duration the file will expire after.
164    /// Note that this time may not be correct as it may have been guessed,
165    /// see `expire_uncertain()`.
166    pub fn expire_duration(&self) -> Duration {
167        // Get the current time
168        let now = Utc::now();
169
170        // Return the duration if not expired, otherwise return zero
171        if self.expire_at > now {
172            self.expire_at - now
173        } else {
174            Duration::zero()
175        }
176    }
177
178    /// Set the time this file will expire at.
179    /// None may be given to assign the default expiry time with the
180    /// uncertainty flag set.
181    pub fn set_expire_at(&mut self, expire_at: Option<DateTime<Utc>>) {
182        if let Some(expire_at) = expire_at {
183            self.expire_at = expire_at;
184        } else {
185            self.expire_at = Utc::now() + Duration::seconds(SEND_DEFAULT_EXPIRE_TIME as i64);
186            self.expire_uncertain = true;
187        }
188    }
189
190    /// Set the time this file will expire at,
191    /// based on the given duration from now.
192    pub fn set_expire_duration(&mut self, duration: Duration) {
193        self.set_expire_at(Some(Utc::now() + duration));
194    }
195
196    /// Check whether this file has expired, based on it's expiry property.
197    pub fn has_expired(&self) -> bool {
198        self.expire_at < Utc::now()
199    }
200
201    /// Check whehter the set expiry time is uncertain.
202    /// If the expiry time of a file is unknown,
203    /// the default time is assigned from the first time
204    /// the file was used. Such time will be uncertain as it probably isn't
205    /// correct.
206    /// This time may be used however to check for expiry.
207    pub fn expire_uncertain(&self) -> bool {
208        self.expire_uncertain
209    }
210
211    /// Get the file URL, provided by the server.
212    pub fn url(&self) -> &Url {
213        &self.url
214    }
215
216    /// Get the raw secret.
217    // TODO: ensure whether the secret is set?
218    pub fn secret_raw(&self) -> &Vec<u8> {
219        &self.secret
220    }
221
222    /// Get the secret as base64 encoded string.
223    pub fn secret(&self) -> String {
224        b64::encode(self.secret_raw())
225    }
226
227    /// Set the secret for this file.
228    pub fn set_secret(&mut self, secret: Vec<u8>) {
229        self.secret = secret;
230    }
231
232    /// Check whether a file secret is set.
233    /// This secret must be set to decrypt a downloaded Send file.
234    pub fn has_secret(&self) -> bool {
235        !self.secret.is_empty()
236    }
237
238    /// Get the owner token if set.
239    pub fn owner_token(&self) -> Option<&String> {
240        self.owner_token.as_ref()
241    }
242
243    /// Get the owner token if set.
244    pub fn owner_token_mut(&mut self) -> &mut Option<String> {
245        &mut self.owner_token
246    }
247
248    /// Set the owner token, wrapped in an option.
249    /// If `None` is given, the owner token will be unset.
250    pub fn set_owner_token(&mut self, token: Option<String>) {
251        self.owner_token = token;
252    }
253
254    /// Check whether an owner token is set in this remote file.
255    pub fn has_owner_token(&self) -> bool {
256        self.owner_token
257            .clone()
258            .map(|t| !t.is_empty())
259            .unwrap_or(false)
260    }
261
262    /// Get the host URL for this remote file.
263    pub fn host(&self) -> Url {
264        self.host.clone()
265    }
266
267    /// Build the download URL of the given file.
268    /// This URL is identical to the share URL, a term used in this API.
269    /// Set `secret` to `true`, to include it in the URL if known.
270    pub fn download_url(&self, secret: bool) -> Url {
271        UrlBuilder::download(&self, secret)
272    }
273
274    /// Merge properties non-existant into this file, from the given other file.
275    /// This is ofcourse only done for properties that may be empty.
276    ///
277    /// The file IDs are not asserted for equality.
278    #[allow(unknown_lints)]
279    pub fn merge(&mut self, other: &RemoteFile, overwrite: bool) -> bool {
280        // Remember whether anything has changed
281        let mut changed = false;
282
283        // Set the upload time
284        if other.upload_at.is_some() && (self.upload_at.is_none() || overwrite) {
285            self.upload_at = other.upload_at;
286            changed = true;
287        }
288
289        // Set the expire time
290        if !other.expire_uncertain() && (self.expire_uncertain() || overwrite) {
291            self.expire_at = other.expire_at;
292            self.expire_uncertain = other.expire_uncertain();
293            changed = true;
294        }
295
296        // Set the secret
297        if other.has_secret() && (!self.has_secret() || overwrite) {
298            self.secret = other.secret_raw().clone();
299            changed = true;
300        }
301
302        // Set the owner token
303        if other.owner_token.is_some() && (self.owner_token.is_none() || overwrite) {
304            self.owner_token = other.owner_token.clone();
305            changed = true;
306        }
307
308        changed
309    }
310}
311
312#[derive(Debug, Error)]
313pub enum FileParseError {
314    /// An URL format error.
315    #[error("failed to parse remote file, invalid URL format")]
316    UrlFormatError(#[from] UrlParseError),
317
318    /// An error for an invalid share URL format.
319    #[error("failed to parse remote file, invalid URL")]
320    InvalidUrl,
321
322    /// An error for an invalid secret format, if an URL fragmet exists.
323    #[error("failed to parse remote file, invalid secret in URL")]
324    InvalidSecret,
325}