tus_client_extra/
lib.rs

1//! # tus_client
2//!
3//! A Rust native client library to interact with *tus* enabled endpoints.
4//!
5//! ## `reqwest` implementation
6//!
7//! `tus_client` requires a "handler" which implements the `HttpHandler` trait. To include a default implementation of this trait for [`reqwest`](https://crates.io/crates/reqwest), specify the `reqwest` feature when including `tus_client` as a dependency.
8//!
9//! ```toml
10//! # Other parts of Cargo.toml omitted for brevity
11//! [dependencies]
12//! tus_client = {version = "x.x.x", features = ["reqwest"]}
13//! ```
14//!
15//! ## Usage
16//!
17//! ```rust
18//! use tus_client::Client;
19//! use reqwest;
20//!
21//! // Create an instance of the `tus_client::Client` struct.
22//! // Assumes "reqwest" feature is enabled (see above)
23//! let client = Client::new(reqwest::Client::new());
24//!
25//! // You'll need an upload URL to be able to upload a files.
26//! // This may be provided to you (through a separate API, for example),
27//! // or you might need to create the file through the *tus* protocol.
28//! // If an upload URL is provided for you, you can skip this step.
29//!
30//! let upload_url = client
31//! .create("https://my.tus.server/files/", "/path/to/file")
32//! .expect("Failed to create file on server");
33//!
34//! // Next, you can start uploading the file by calling `upload`.
35//! // The file will be uploaded in 5 MiB chunks by default.
36//! // To customize the chunk size, use `upload_with_chunk_size` instead of `upload`.
37//!
38//! client
39//! .upload(&upload_url, "/path/to/file")
40//! .expect("Failed to upload file to server");
41//! ```
42//!
43//! `upload` (and `upload_with_chunk_size`) will automatically resume the upload from where it left off, if the upload transfer is interrupted.
44#![doc(html_root_url = "https://docs.rs/tus_client/0.1.1")]
45use crate::http::{default_headers, Headers, HttpHandler, HttpMethod, HttpRequest};
46use std::collections::HashMap;
47use std::collections::hash_map::RandomState;
48use std::error::Error as StdError;
49use std::fmt::{Display, Formatter};
50use std::fs::File;
51use std::io;
52use std::io::{BufReader, Read, Seek, SeekFrom};
53use std::num::ParseIntError;
54use std::ops::Deref;
55use std::path::Path;
56use std::str::FromStr;
57
58mod headers;
59/// Contains the `HttpHandler` trait and related structs. This module is only relevant when implement `HttpHandler` manually.
60pub mod http;
61
62#[cfg(feature = "reqwest")]
63mod reqwest;
64
65const DEFAULT_CHUNK_SIZE: usize = 5 * 1024 * 1024;
66
67/// Used to interact with a [tus](https://tus.io) endpoint.
68pub struct Client<'a> {
69    use_method_override: bool,
70    http_handler: Box<dyn HttpHandler + 'a>,
71}
72
73pub struct CreateResponse{
74    pub upload_url: String,
75    pub headers: HashMap<String, String, RandomState>
76}
77
78impl<'a> Client<'a> {
79    /// Instantiates a new instance of `Client`. `http_handler` needs to implement the `HttpHandler` trait.
80    /// A default implementation of this trait for the `reqwest` library is available by enabling the `reqwest` feature.
81    pub fn new(http_handler: impl HttpHandler + 'a) -> Self {
82        Client {
83            use_method_override: false,
84            http_handler: Box::new(http_handler),
85        }
86    }
87
88    /// Some environments might not support using the HTTP methods `PATCH` and `DELETE`. Use this method to create a `Client` which uses the `X-HTTP-METHOD-OVERRIDE` header to specify these methods instead.
89    pub fn with_method_override(http_handler: impl HttpHandler + 'a) -> Self {
90        Client {
91            use_method_override: true,
92            http_handler: Box::new(http_handler),
93        }
94    }
95
96    /// Get info about a file on the server.
97    pub fn get_info(&self, url: &str) -> Result<UploadInfo, Error> {
98        let req = self.create_request(HttpMethod::Head, url, None, Some(default_headers()));
99
100        let response = self.http_handler.deref().handle_request(req)?;
101
102        let bytes_uploaded = response.headers.get_by_key(headers::UPLOAD_OFFSET);
103        let total_size = response
104            .headers
105            .get_by_key(headers::UPLOAD_LENGTH)
106            .and_then(|l| l.parse::<usize>().ok());
107        let metadata = response
108            .headers
109            .get_by_key(headers::UPLOAD_METADATA)
110            .and_then(|data| base64::decode(data).ok())
111            .map(|decoded| {
112                String::from_utf8(decoded).unwrap().split(';').fold(
113                    HashMap::new(),
114                    |mut acc, key_val| {
115                        let mut parts = key_val.splitn(2, ':');
116                        if let Some(key) = parts.next() {
117                            acc.insert(
118                                String::from(key),
119                                String::from(parts.next().unwrap_or_default()),
120                            );
121                        }
122                        acc
123                    },
124                )
125            });
126
127        if response.status_code.to_string().starts_with('4') || bytes_uploaded.is_none() {
128            return Err(Error::NotFoundError);
129        }
130
131        let bytes_uploaded = bytes_uploaded.unwrap().parse()?;
132
133        Ok(UploadInfo {
134            bytes_uploaded,
135            total_size,
136            metadata,
137        })
138    }
139
140    /// Upload a file to the specified upload URL.
141    pub fn upload(&self, url: &str, path: &Path) -> Result<(), Error> {
142        self.upload_with_chunk_size(url, path, DEFAULT_CHUNK_SIZE)
143    }
144
145    /// Upload a file to the specified upload URL with the given chunk size.
146    pub fn upload_with_chunk_size(
147        &self,
148        url: &str,
149        path: &Path,
150        chunk_size: usize,
151    ) -> Result<(), Error> {
152        let info = self.get_info(url)?;
153        let file = File::open(path)?;
154        let file_len = file.metadata()?.len();
155
156        if let Some(total_size) = info.total_size {
157            if file_len as usize != total_size {
158                return Err(Error::UnequalSizeError);
159            }
160        }
161
162        let mut reader = BufReader::new(&file);
163        let mut buffer = vec![0; chunk_size];
164        let mut progress = info.bytes_uploaded;
165
166        reader.seek(SeekFrom::Start(progress as u64))?;
167
168        loop {
169            let bytes_read = reader.read(&mut buffer)?;
170            if bytes_read == 0 {
171                return Err(Error::FileReadError);
172            }
173
174            let req = self.create_request(
175                HttpMethod::Patch,
176                url,
177                Some(&buffer[..bytes_read]),
178                Some(create_upload_headers(progress)),
179            );
180
181            let response = self.http_handler.deref().handle_request(req)?;
182
183            if response.status_code == 409 {
184                return Err(Error::WrongUploadOffsetError);
185            }
186
187            if response.status_code == 404 {
188                return Err(Error::NotFoundError);
189            }
190
191            if response.status_code != 204 {
192                return Err(Error::UnexpectedStatusCode(response.status_code));
193            }
194
195            let upload_offset = match response.headers.get_by_key(headers::UPLOAD_OFFSET) {
196                Some(offset) => Ok(offset),
197                None => Err(Error::MissingHeader(headers::UPLOAD_OFFSET.to_owned())),
198            }?;
199
200            progress = upload_offset.parse()?;
201
202            if progress >= file_len as usize {
203                break;
204            }
205        }
206
207        Ok(())
208    }
209
210    /// Get information about the tus server
211    pub fn get_server_info(&self, url: &str) -> Result<ServerInfo, Error> {
212        let req = self.create_request(HttpMethod::Options, url, None, None);
213
214        let response = self.http_handler.deref().handle_request(req)?;
215
216        if ![200_usize, 204].contains(&response.status_code) {
217            return Err(Error::UnexpectedStatusCode(response.status_code));
218        }
219
220        let supported_versions: Vec<String> = response
221            .headers
222            .get_by_key(headers::TUS_VERSION)
223            .unwrap()
224            .split(',')
225            .map(String::from)
226            .collect();
227        let extensions: Vec<TusExtension> =
228            if let Some(ext) = response.headers.get_by_key(headers::TUS_EXTENSION) {
229                ext.split(',')
230                    .map(str::parse)
231                    .filter(Result::is_ok)
232                    .map(Result::unwrap)
233                    .collect()
234            } else {
235                Vec::new()
236            };
237        let max_upload_size = response
238            .headers
239            .get_by_key(headers::TUS_MAX_SIZE)
240            .and_then(|h| h.parse::<usize>().ok());
241
242        Ok(ServerInfo {
243            supported_versions,
244            extensions,
245            max_upload_size,
246        })
247    }
248
249    /// Create a file on the server, receiving the upload URL of the file.
250    pub fn create(&self, url: &str, path: &Path) -> Result<CreateResponse, Error> {
251        self.create_with_metadata(url, path, HashMap::new())
252    }
253
254    /// Create a file on the server including the specified metadata, receiving the upload URL of the file.
255    pub fn create_with_metadata(
256        &self,
257        url: &str,
258        path: &Path,
259        metadata: HashMap<String, String>,
260    ) -> Result<CreateResponse, Error> {
261        let mut headers = default_headers();
262        headers.insert(
263            headers::UPLOAD_LENGTH.to_owned(),
264            path.metadata()?.len().to_string(),
265        );
266        if !metadata.is_empty() {
267            let data = metadata
268                .iter()
269                .map(|(key, value)| format!("{} {}", key, base64::encode(value)))
270                .collect::<Vec<_>>()
271                .join(",");
272            headers.insert(headers::UPLOAD_METADATA.to_owned(), data);
273        }
274
275        let req = self.create_request(HttpMethod::Post, url, None, Some(headers));
276
277        let response = self.http_handler.deref().handle_request(req)?;
278
279        if response.status_code == 413 {
280            return Err(Error::FileTooLarge);
281        }
282
283        if response.status_code != 201 {
284            return Err(Error::UnexpectedStatusCode(response.status_code));
285        }
286
287        let location = response.headers.get_by_key(headers::LOCATION);
288
289        if location.is_none() {
290            return Err(Error::MissingHeader(headers::LOCATION.to_owned()));
291        }
292
293        Ok(CreateResponse {
294            upload_url: location.unwrap().to_string(),
295            headers: response.headers,
296        })
297    }
298
299    /// Delete a file on the server.
300    pub fn delete(&self, url: &str) -> Result<(), Error> {
301        let req = self.create_request(HttpMethod::Delete, url, None, Some(default_headers()));
302
303        let response = self.http_handler.deref().handle_request(req)?;
304
305        if response.status_code != 204 {
306            return Err(Error::UnexpectedStatusCode(response.status_code));
307        }
308
309        Ok(())
310    }
311
312    fn create_request<'b>(
313        &self,
314        method: HttpMethod,
315        url: &str,
316        body: Option<&'b [u8]>,
317        headers: Option<Headers>,
318    ) -> HttpRequest<'b> {
319        let mut headers = headers.unwrap_or_default();
320
321        let method = if self.use_method_override {
322            headers.insert(
323                headers::X_HTTP_METHOD_OVERRIDE.to_owned(),
324                method.to_string(),
325            );
326            HttpMethod::Post
327        } else {
328            method
329        };
330
331        HttpRequest {
332            method,
333            url: String::from(url),
334            body,
335            headers,
336        }
337    }
338}
339
340/// Describes a file on the server.
341#[derive(Debug)]
342pub struct UploadInfo {
343    /// How many bytes have been uploaded.
344    pub bytes_uploaded: usize,
345    /// The total size of the file.
346    pub total_size: Option<usize>,
347    /// Metadata supplied when the file was created.
348    pub metadata: Option<HashMap<String, String>>,
349}
350
351/// Describes the tus enabled server.
352#[derive(Debug)]
353pub struct ServerInfo {
354    /// The different versions of the tus protocol supported by the server, ordered by preference.
355    pub supported_versions: Vec<String>,
356    /// The extensions to the protocol supported by the server.
357    pub extensions: Vec<TusExtension>,
358    /// The maximum supported total size of a file.
359    pub max_upload_size: Option<usize>,
360}
361
362/// Enumerates the extensions to the tus protocol.
363#[derive(Debug, PartialEq)]
364pub enum TusExtension {
365    /// The server supports creating files.
366    Creation,
367    //// The server supports setting expiration time on files and uploads.
368    Expiration,
369    /// The server supports verifying checksums of uploaded chunks.
370    Checksum,
371    /// The server supports deleting files.
372    Termination,
373    /// The server supports parallel uploads of a single file.
374    Concatenation,
375}
376
377impl FromStr for TusExtension {
378    type Err = ();
379
380    fn from_str(s: &str) -> Result<Self, Self::Err> {
381        match s.trim().to_lowercase().as_str() {
382            "creation" => Ok(TusExtension::Creation),
383            "expiration" => Ok(TusExtension::Expiration),
384            "checksum" => Ok(TusExtension::Checksum),
385            "termination" => Ok(TusExtension::Termination),
386            "concatenation" => Ok(TusExtension::Concatenation),
387            _ => Err(()),
388        }
389    }
390}
391
392/// Enumerates the errors which can occur during operation
393#[derive(Debug)]
394pub enum Error {
395    /// The status code returned by the server was not one of the expected ones.
396    UnexpectedStatusCode(usize),
397    /// The file specified was not found by the server.
398    NotFoundError,
399    /// A required header was missing from the server response.
400    MissingHeader(String),
401    /// An error occurred while doing disk IO. This may be while reading a file, or during a network call.
402    IoError(io::Error),
403    /// Unable to parse a value, which should be an integer.
404    ParsingError(ParseIntError),
405    /// The size of the specified file, and the file size reported by the server do not match.
406    UnequalSizeError,
407    /// Unable to read the file specified.
408    FileReadError,
409    /// The `Client` tried to upload the file with an incorrect offset.
410    WrongUploadOffsetError,
411    /// The specified file is larger that what is supported by the server.
412    FileTooLarge,
413    /// An error occurred in the HTTP handler.
414    HttpHandlerError(String),
415}
416
417impl Display for Error {
418    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
419        let message = match self {
420            Error::UnexpectedStatusCode(status_code) => format!("The status code returned by the server was not one of the expected ones: {}", status_code),
421            Error::NotFoundError => "The file specified was not found by the server".to_string(),
422            Error::MissingHeader(header_name) => format!("The '{}' header was missing from the server response", header_name),
423            Error::IoError(error) => format!("An error occurred while doing disk IO. This may be while reading a file, or during a network call: {}", error),
424            Error::ParsingError(error) => format!("Unable to parse a value, which should be an integer: {}", error),
425            Error::UnequalSizeError => "The size of the specified file, and the file size reported by the server do not match".to_string(),
426            Error::FileReadError => "Unable to read the specified file".to_string(),
427            Error::WrongUploadOffsetError => "The client tried to upload the file with an incorrect offset".to_string(),
428            Error::FileTooLarge => "The specified file is larger that what is supported by the server".to_string(),
429            Error::HttpHandlerError(message) => format!("An error occurred in the HTTP handler: {}", message),
430        };
431
432        write!(f, "{}", message)?;
433
434        Ok(())
435    }
436}
437
438impl StdError for Error {}
439
440impl From<io::Error> for Error {
441    fn from(e: io::Error) -> Self {
442        Error::IoError(e)
443    }
444}
445
446impl From<ParseIntError> for Error {
447    fn from(e: ParseIntError) -> Self {
448        Error::ParsingError(e)
449    }
450}
451
452trait HeaderMap {
453    fn get_by_key(&self, key: &str) -> Option<&String>;
454}
455
456impl HeaderMap for HashMap<String, String> {
457    fn get_by_key(&self, key: &str) -> Option<&String> {
458        self.keys()
459            .find(|k| k.to_lowercase().as_str() == key)
460            .and_then(|k| self.get(k))
461    }
462}
463
464fn create_upload_headers(progress: usize) -> Headers {
465    let mut headers = default_headers();
466    headers.insert(
467        headers::CONTENT_TYPE.to_owned(),
468        "application/offset+octet-stream".to_owned(),
469    );
470    headers.insert(headers::UPLOAD_OFFSET.to_owned(), progress.to_string());
471    headers
472}