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}