ffsend_api/action/download.rs
1use std::fs::File;
2use std::io::{self, Error as IoError, Read, Write};
3use std::path::PathBuf;
4use std::sync::{Arc, Mutex};
5
6use reqwest::{blocking::Response, header::AUTHORIZATION};
7use thiserror::Error;
8
9use super::metadata::{Error as MetadataError, Metadata as MetadataAction, MetadataResponse};
10use crate::api::request::{ensure_success, ResponseError};
11use crate::api::url::UrlBuilder;
12use crate::api::Version;
13use crate::client::Client;
14use crate::crypto::key_set::KeySet;
15use crate::crypto::sig::signature_encoded;
16use crate::file::remote_file::RemoteFile;
17#[cfg(feature = "send3")]
18use crate::pipe::crypto::EceCrypt;
19#[cfg(feature = "send2")]
20use crate::pipe::crypto::GcmCrypt;
21use crate::pipe::{
22 prelude::*,
23 progress::{ProgressPipe, ProgressReporter},
24};
25
26/// A file download action to a Send server.
27///
28/// This action is compatible with both Firefox Send v2 and v3, but the server API version to use
29/// must be explicitly given due to a version specific download method.
30pub struct Download<'a> {
31 /// The server API version to use when downloading the file.
32 version: Version,
33
34 /// The remote file to download.
35 file: &'a RemoteFile,
36
37 /// The target file or directory, to download the file to.
38 target: PathBuf,
39
40 /// An optional password to decrypt a protected file.
41 password: Option<String>,
42
43 /// Check whether the file exists (recommended).
44 check_exists: bool,
45
46 /// The metadata response to work with,
47 /// which will skip the internal metadata request.
48 metadata_response: Option<MetadataResponse>,
49}
50
51impl<'a> Download<'a> {
52 /// Construct a new download action for the given remote file.
53 /// It is recommended to check whether the file exists,
54 /// unless that is already done.
55 pub fn new(
56 version: Version,
57 file: &'a RemoteFile,
58 target: PathBuf,
59 password: Option<String>,
60 check_exists: bool,
61 metadata_response: Option<MetadataResponse>,
62 ) -> Self {
63 Self {
64 version,
65 file,
66 target,
67 password,
68 check_exists,
69 metadata_response,
70 }
71 }
72
73 /// Invoke the download action.
74 pub fn invoke(
75 mut self,
76 client: &Client,
77 reporter: Option<Arc<Mutex<dyn ProgressReporter>>>,
78 ) -> Result<(), Error> {
79 // Create a key set for the file
80 let mut key = KeySet::from(self.file, self.password.as_ref());
81
82 // Get the metadata, or fetch the file metadata,
83 // then update the input vector in the key set
84 let metadata: MetadataResponse = if self.metadata_response.is_some() {
85 self.metadata_response.take().unwrap()
86 } else {
87 MetadataAction::new(self.file, self.password.clone(), self.check_exists)
88 .invoke(&client)?
89 };
90
91 // Set the input vector if known, depending on the API version
92 if let Some(nonce) = metadata.metadata().iv() {
93 key.set_nonce(nonce);
94 }
95
96 // Decide what actual file target to use
97 let path = self.decide_path(metadata.metadata().name());
98 let path_str = path.to_str().unwrap_or("?").to_owned();
99
100 // Open the file we will write to
101 // TODO: this should become a temporary file first
102 // TODO: use the uploaded file name as default
103 let out = File::create(path)
104 .map_err(|err| Error::File(path_str.clone(), FileError::Create(err)))?;
105
106 // Create the file reader for downloading
107 let (reader, len) = self.create_file_reader(&key, &metadata, &client)?;
108
109 // Create the file writer
110 let writer = self
111 .create_writer(out, len, &key, reporter.clone())
112 .map_err(|err| Error::File(path_str.clone(), err))?;
113
114 // Download the file
115 self.download(reader, writer, len, reporter)?;
116
117 // TODO: return the file path
118 // TODO: return the new remote state (does it still exist remote)
119
120 Ok(())
121 }
122
123 /// Decide what path we will download the file to.
124 ///
125 /// A target file or directory, and a file name hint must be given.
126 /// The name hint can be derived from the retrieved metadata on this file.
127 ///
128 /// The name hint is used as file name, if a directory was given.
129 fn decide_path(&self, name_hint: &str) -> PathBuf {
130 // Return the target if it is an existing file
131 if self.target.is_file() {
132 return self.target.clone();
133 }
134
135 // Append the name hint if this is a directory
136 if self.target.is_dir() {
137 return self.target.join(name_hint);
138 }
139
140 // Return if the parent is an existing directory
141 if self.target.parent().map(|p| p.is_dir()).unwrap_or(false) {
142 return self.target.clone();
143 }
144
145 // TODO: are these todos below already implemented in CLI client?
146 // TODO: canonicalize the path when possible
147 // TODO: allow using `file.toml` as target without directory indication
148 // TODO: return a nice error here as the path may be invalid
149 // TODO: maybe prompt the user to create the directory
150 panic!("Invalid (non-existing) output path given, not yet supported");
151 }
152
153 /// Make a download request, and create a reader that downloads the
154 /// encrypted file.
155 ///
156 /// The response representing the file reader is returned along with the
157 /// length of the reader content.
158 fn create_file_reader(
159 &self,
160 key: &KeySet,
161 metadata: &MetadataResponse,
162 client: &Client,
163 ) -> Result<(Response, u64), DownloadError> {
164 // Compute the cryptographic signature
165 let sig = signature_encoded(key.auth_key().unwrap(), metadata.nonce())
166 .map_err(|_| DownloadError::ComputeSignature)?;
167
168 // Build and send the download request
169 let response = client
170 .get(UrlBuilder::api_download(self.file))
171 .header(AUTHORIZATION.as_str(), format!("send-v1 {}", sig))
172 .send()
173 .map_err(|_| DownloadError::Request)?;
174
175 // Ensure the response is successful
176 ensure_success(&response).map_err(DownloadError::Response)?;
177
178 // Get the content length
179 // TODO: make sure there is enough disk space
180 let len = metadata.size();
181
182 Ok((response, len))
183 }
184
185 /// Create a file writer.
186 ///
187 /// This writer will will decrypt the input on the fly, and writes the
188 /// decrypted data to the given file.
189 fn create_writer(
190 &self,
191 file: File,
192 len: u64,
193 key: &KeySet,
194 reporter: Option<Arc<Mutex<dyn ProgressReporter>>>,
195 ) -> Result<impl Write, FileError> {
196 // Build the decrypting file writer for the selected server API version
197 let writer: Box<dyn Write> = match self.version {
198 #[cfg(feature = "send2")]
199 Version::V2 => {
200 let decrypt = GcmCrypt::decrypt(len as usize, key.file_key().unwrap(), key.nonce());
201 let writer = decrypt.writer(Box::new(file));
202 Box::new(writer)
203 }
204 #[cfg(feature = "send3")]
205 Version::V3 => {
206 let ikm = key.secret().to_vec();
207 let decrypt = EceCrypt::decrypt(len as usize, ikm);
208 let writer = decrypt.writer(Box::new(file));
209 Box::new(writer)
210 }
211 };
212
213 // Build the progress pipe file writer
214 let progress = ProgressPipe::zero(len as u64, reporter);
215 let writer = progress.writer(writer);
216
217 Ok(writer)
218 }
219
220 /// Download the file from the reader, and write it to the writer.
221 /// The length of the file must also be given.
222 /// The status will be reported to the given progress reporter.
223 fn download<R, W>(
224 &self,
225 mut reader: R,
226 mut writer: W,
227 len: u64,
228 reporter: Option<Arc<Mutex<dyn ProgressReporter>>>,
229 ) -> Result<(), DownloadError>
230 where
231 R: Read,
232 W: Write,
233 {
234 // Start the writer
235 if let Some(reporter) = reporter.as_ref() {
236 reporter
237 .lock()
238 .map_err(|_| DownloadError::Progress)?
239 .start(len);
240 }
241
242 // Write to the output file
243 io::copy(&mut reader, &mut writer).map_err(|_| DownloadError::Download)?;
244
245 // Finish
246 if let Some(reporter) = reporter.as_ref() {
247 reporter
248 .lock()
249 .map_err(|_| DownloadError::Progress)?
250 .finish();
251 }
252
253 Ok(())
254 }
255}
256
257#[derive(Error, Debug)]
258pub enum Error {
259 /// An error occurred while fetching the metadata of the file.
260 /// This step is required in order to succsessfully decrypt the
261 /// file that will be downloaded.
262 #[error("failed to fetch file metadata")]
263 Meta(#[from] MetadataError),
264
265 /// The given Send file has expired, or did never exist in the first place.
266 /// Therefore the file could not be downloaded.
267 #[error("the file has expired or did never exist")]
268 Expired,
269
270 /// A password is required, but was not given.
271 #[error("missing password, password required")]
272 PasswordRequired,
273
274 /// An error occurred while downloading the file.
275 #[error("failed to download the file")]
276 Download(#[from] DownloadError),
277
278 /// An error occurred while decrypting the downloaded file.
279 #[error("failed to decrypt the downloaded file")]
280 Decrypt,
281
282 /// An error occurred while opening or writing to the target file.
283 // TODO: show what file this is about
284 #[error("couldn't use the target file at '{}'", _0)]
285 File(String, #[source] FileError),
286}
287
288#[derive(Error, Debug)]
289pub enum DownloadError {
290 /// An error occurred while computing the cryptographic signature used for
291 /// downloading the file.
292 #[error("failed to compute cryptographic signature")]
293 ComputeSignature,
294
295 /// Sending the request to download the file failed.
296 #[error("failed to request file download")]
297 Request,
298
299 /// The server responded with an error while requesting the file download.
300 #[error("bad response from server while requesting download")]
301 Response(#[from] ResponseError),
302
303 /// Failed to start or update the downloading progress, because of this the
304 /// download can't continue.
305 #[error("failed to update download progress")]
306 Progress,
307
308 /// The actual download and decryption process the server.
309 /// This covers reading the file from the server, decrypting the file,
310 /// and writing it to the file system.
311 #[error("failed to download the file")]
312 Download,
313 // /// Verifying the downloaded file failed.
314 // #[error("file verification failed")]
315 // Verify,
316}
317
318#[derive(Error, Debug)]
319pub enum FileError {
320 /// An error occurred while creating or opening the file to write to.
321 #[error("failed to create or replace the file")]
322 Create(#[from] IoError),
323
324 /// Failed to create an encrypted writer for the file, which is used to
325 /// decrypt the downloaded file.
326 #[error("failed to create file decryptor")]
327 EncryptedWriter,
328}