ucare/
upload.rs

1//! Upload module contains all upload related API stuff.
2//!
3//! Upload API is an addition to the REST API. It provides several ways of uploading
4//! files to the Uploadcare servers.
5//! Every uploaded file is temporary and subject to be deleted within a 24-hour
6//! period. To make any file permanent, you should store or copy it.
7//!
8//! The package provides uploading files by making requests with payload to
9//! the Uploadcare API endpoints. There are two basic upload types:
10//!
11//! - Direct uploads, a regular upload mode that suits most files less than 100MB
12//! in size. You won’t be able to use this mode for larger files.
13//!
14//! - Multipart uploads, a more sophisticated upload mode supporting any files
15//! larger than 10MB and implementing accelerated uploads through
16//! a distributed network.
17
18use std::collections::HashMap;
19use std::fmt::{self, Debug, Display};
20
21use reqwest::{blocking::multipart::Form, Method, Url};
22use serde::Deserialize;
23
24use crate::file::{ImageInfo, VideoInfo};
25use crate::ucare::{upload::Client, upload::Fields, upload::Payload, Result};
26
27/// Service is used to make calls to file API.
28pub struct Service<'a> {
29    client: &'a Client,
30}
31
32/// creates new upload service instance
33pub fn new_svc(client: &Client) -> Service {
34    Service { client }
35}
36
37impl Service<'_> {
38    /// Uploads a file and return its unique id (uuid). Comply with the RFC7578 standard.
39    /// Resulting HashMap holds filenames as keys and their ids are values.
40    pub fn file(&self, params: FileParams) -> Result<HashMap<String, String>> {
41        let mut form = Form::new()
42            .file(params.name.to_string(), params.path.to_string())?
43            .text(
44                "UPLOADCARE_STORE",
45                if let Some(val) = params.to_store {
46                    val
47                } else {
48                    ToStore::False
49                }
50                .to_string(),
51            );
52        form = add_signature_expire(&(*self.client.auth_fields)(), form);
53
54        self.client.call::<String, HashMap<String, String>>(
55            Method::POST,
56            format!("/base/"),
57            None,
58            Some(Payload::Form(form)),
59        )
60    }
61
62    /// Uploads file by its public URL.
63    pub fn from_url(&self, params: FromUrlParams) -> Result<FromUrlData> {
64        let mut form = Form::new().text("source_url", params.source_url).text(
65            "store",
66            if let Some(val) = params.to_store {
67                val
68            } else {
69                ToStore::False
70            }
71            .to_string(),
72        );
73        if let Some(val) = params.filename {
74            form = form.text("filename", val);
75        }
76        if let Some(val) = params.check_url_duplicates {
77            form = form.text("check_URL_duplicates", val.to_string());
78        }
79        if let Some(val) = params.save_url_duplicates {
80            form = form.text("save_URL_duplicates", val.to_string());
81        }
82        form = add_signature_expire(&(*self.client.auth_fields)(), form);
83
84        self.client.call::<String, FromUrlData>(
85            Method::POST,
86            format!("/from_url/"),
87            None,
88            Some(Payload::Form(form)),
89        )
90    }
91
92    /// Check the status of a file uploaded from URL.
93    pub fn from_url_status(&self, token: &str) -> Result<FromUrlStatusData> {
94        self.client.call::<String, FromUrlStatusData>(
95            Method::GET,
96            format!("/from_url/status/?token={}", token),
97            None,
98            None,
99        )
100    }
101
102    /// Returns uploading file info.
103    pub fn file_info(&self, file_id: &str) -> Result<FileInfo> {
104        let fields = (*self.client.auth_fields)();
105        self.client.call::<String, FileInfo>(
106            Method::GET,
107            format!("/info/?pub_key={}&file_id={}", fields.pub_key, file_id),
108            None,
109            None,
110        )
111    }
112
113    /// Creates files group from a set of files by using their IDs with
114    /// or without applied CDN media processing operations.
115    ///
116    /// Example:
117    ///   [
118    ///      "d6d34fa9-addd-472c-868d-2e5c105f9fcd",
119    ///      "b1026315-8116-4632-8364-607e64fca723/-/resize/x800/",
120    ///   ]
121    pub fn create_group(&self, ids: &[&str]) -> Result<GroupInfo> {
122        let mut form = Form::new();
123        for (pos, id) in ids.iter().enumerate() {
124            form = form.text(
125                ("files[".to_string() + pos.to_string().as_str() + "]").to_string(),
126                id.to_string(),
127            );
128        }
129        form = add_signature_expire(&(*self.client.auth_fields)(), form);
130
131        self.client.call::<String, GroupInfo>(
132            Method::POST,
133            format!("/group/"),
134            None,
135            Some(Payload::Form(form)),
136        )
137    }
138
139    /// Returns group specific info.
140    ///
141    /// GroupID look like UUID~N, for example:
142    ///   "d52d7136-a2e5-4338-9f45-affbf83b857d~2"
143    pub fn group_info(&self, group_id: &str) -> Result<GroupInfo> {
144        let fields = (*self.client.auth_fields)();
145        self.client.call::<String, GroupInfo>(
146            Method::GET,
147            format!(
148                "/group/info/?pub_key={}&group_id={}",
149                fields.pub_key, group_id,
150            ),
151            None,
152            None,
153        )
154    }
155
156    /// Multipart upload is useful when you are dealing with file larger than
157    /// 100MB or explicitly want to use accelerated uploads.
158    /// Another benefit is your file will go straight to AWS S3 bypassing our upload
159    /// instances thus quickly becoming available for further use.
160    /// Note, there also exists a minimum file size to use with Multipart Uploads, 10MB.
161    /// Trying to use Multipart upload with a smaller file will result in an error.
162    pub fn multipart_start(&self, params: MultipartParams) -> Result<MultipartData> {
163        let mut form = Form::new()
164            .text("filename", params.filename)
165            .text(
166                "UPLOADCARE_STORE",
167                if let Some(val) = params.to_store {
168                    val
169                } else {
170                    ToStore::False
171                }
172                .to_string(),
173            )
174            .text("content_type", params.content_type)
175            .text("size", params.size.to_string());
176        form = add_signature_expire(&(*self.client.auth_fields)(), form);
177
178        self.client.call::<String, MultipartData>(
179            Method::POST,
180            format!("/multipart/start/"),
181            None,
182            Some(Payload::Form(form)),
183        )
184    }
185
186    /// The second phase is about uploading file parts to the provided URLs. Each uploaded part
187    /// should be 5MB (5242880 bytes) in size except for the last one that can be smaller. You
188    /// can upload file parts in parallel provided the byte order stays unchanged. Make sure to
189    /// define Content-Type header for your data.
190    pub fn upload_part(&self, url: &str, data: Vec<u8>) -> Result<()> {
191        self.client
192            .call_url::<()>(Method::PUT, Url::parse(url)?, Some(Payload::Raw(data)))
193    }
194
195    /// Complete multipart upload transaction when all file parts are uploaded
196    pub fn multipart_complete(&self, uuid: String) -> Result<FileInfo> {
197        let mut form = Form::new().text("uuid", uuid);
198        form = add_signature_expire(&(*self.client.auth_fields)(), form);
199
200        self.client.call::<String, FileInfo>(
201            Method::POST,
202            format!("/multipart/complete/"),
203            None,
204            Some(Payload::Form(form)),
205        )
206    }
207}
208
209/// Holds all possible params for the file upload
210#[derive(Default)]
211pub struct FileParams {
212    /// Path of the file to upload.
213    ///
214    /// It must be smaller than 100MB.
215    /// An attempt of reading a larger file raises a 413 error with the
216    /// respective description. If you want to upload larger files, please
217    /// use multipart upload API methods.
218    pub path: String,
219    /// Uploaded file name
220    pub name: String,
221    /// File storing behaviour.
222    pub to_store: Option<ToStore>,
223}
224
225/// Parameters for upload from public URL link
226pub struct FromUrlParams {
227    /// File URL, which should be a public HTTP or HTTPS link
228    pub source_url: String,
229    /// File storing behaviour.
230    pub to_store: Option<ToStore>,
231    /// The name for a file uploaded from URL. If not defined, the filename is obtained from
232    /// either response headers or a source URL
233    pub filename: Option<String>,
234    /// Specify to run the duplicate check and provide the immediate-download behavior
235    pub check_url_duplicates: Option<UrlDuplicates>,
236    /// Specify to run The save/update URL behavior. The parameter can be used if you believe a
237    /// `source_url` will be used more than once. If you don’t explicitly defined, it is by
238    /// default set to the value of `check_url_duplicates`.
239    pub save_url_duplicates: Option<UrlDuplicates>,
240}
241
242/// Holds data returned by `from_url`
243#[derive(Debug, Deserialize)]
244#[serde(untagged)]
245pub enum FromUrlData {
246    /// Token
247    #[serde(rename = "token")]
248    Token(FileToken),
249    /// File info
250    #[serde(rename = "file_info")]
251    FileInfo(FileInfo),
252}
253
254impl Default for FromUrlData {
255    fn default() -> Self {
256        FromUrlData::Token(FileToken::default())
257    }
258}
259
260/// Respose for the `FromUrlData::Token`
261#[derive(Debug, Deserialize, Default)]
262pub struct FileToken {
263    /// Value: "token"
264    #[serde(rename = "type")]
265    pub data_type: String,
266    /// A token to identify a file for the upload status request
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub token: Option<String>,
269}
270
271/// Holds the response returned by `from_url_status`
272#[derive(Debug, Deserialize)]
273#[serde(tag = "status")]
274pub enum FromUrlStatusData {
275    /// Success
276    #[serde(rename = "success")]
277    Success(FileInfo),
278    /// Still in progress
279    #[serde(rename = "progress")]
280    Progress {
281        /// Currently uploaded file size in bytes
282        done: u32,
283        /// Total file size in bytes
284        total: u32,
285    },
286    /// File upload error
287    #[serde(rename = "error")]
288    Error {
289        /// Error description
290        error: String,
291    },
292    /// Unknown
293    #[serde(rename = "unknown")]
294    Unknown,
295    /// Waiting
296    #[serde(rename = "waiting")]
297    Waiting,
298}
299
300impl Default for FromUrlStatusData {
301    fn default() -> Self {
302        FromUrlStatusData::Unknown
303    }
304}
305
306/// Holds file information in the upload context
307#[derive(Debug, Deserialize, Default)]
308pub struct FileInfo {
309    /// True if file is stored
310    pub is_stored: bool,
311    /// Denotes currently uploaded file size in bytes
312    pub done: u32,
313    /// Same as uuid
314    pub file_id: String,
315    /// Total is same as size
316    pub total: u32,
317    /// File size in bytes
318    pub size: u32,
319    /// File UUID
320    pub uuid: String,
321    /// If file is an image
322    pub is_image: bool,
323    /// Sanitized `original_filename
324    pub filename: String,
325    /// Video metadata
326    pub video_info: Option<VideoInfo>,
327    /// If file is ready to be used after upload
328    pub is_ready: bool,
329    /// Original file name taken from uploaded file
330    pub original_filename: String,
331    /// Image metadata
332    pub image_info: Option<ImageInfo>,
333    /// File MIME-type.
334    pub mime_type: String,
335    /// Your custom user bucket on which file are stored.
336    /// Only available of you setup foreign storage bucket for your project
337    pub s3_bucket: Option<String>,
338    /// CDN media transformations applied to the file when its group was created
339    pub default_effects: Option<String>,
340}
341
342/// Group information
343#[derive(Debug, Deserialize, Default)]
344pub struct GroupInfo {
345    /// When group was created
346    pub datetime_created: String,
347    /// When group was stored
348    pub datetime_stored: Option<String>,
349    /// Number of files in the group
350    #[serde(rename = "files_count")]
351    pub file_count: u32,
352    /// CDN URL of the group
353    pub cdn_url: String,
354    /// Files list
355    pub files: Option<Vec<FileInfo>>,
356    /// Group API url to get this info
357    pub url: String,
358    /// Group ID
359    pub id: String,
360}
361
362/// Params for starting multipart upload
363#[derive(Debug, Default)]
364pub struct MultipartParams {
365    /// Original file name
366    pub filename: String,
367    /// Precise file size in bytes. Should not exceed your project file size cap.
368    pub size: u32,
369    /// A file MIME-type
370    pub content_type: String,
371    /// File storing behaviour.
372    pub to_store: Option<ToStore>,
373}
374
375/// Response for starting multipart upload
376#[derive(Default, Debug, Deserialize)]
377pub struct MultipartData {
378    /// Array of presigned-url strings    
379    pub parts: Vec<String>,
380    /// Uploaded file UUID
381    pub uuid: String,
382}
383
384/// Upload status
385pub enum UploadStatus {
386    /// success
387    Success,
388    /// progress
389    InProgress,
390    /// error
391    Error,
392    /// waiting
393    Waiting,
394    /// unknown
395    Unknown,
396}
397
398impl Display for UploadStatus {
399    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
400        let val = match *self {
401            UploadStatus::Success => "success",
402            UploadStatus::InProgress => "progress",
403            UploadStatus::Error => "error",
404            UploadStatus::Waiting => "waiting",
405            UploadStatus::Unknown => "unknown",
406        };
407
408        write!(f, "{}", val)
409    }
410}
411
412/// Sets the file storing behaviour
413pub enum ToStore {
414    /// True
415    True,
416    /// False
417    False,
418    /// Auto
419    Auto,
420}
421
422impl Display for ToStore {
423    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
424        let val = match *self {
425            ToStore::True => "1",
426            ToStore::False => "0",
427            ToStore::Auto => "auto",
428        };
429
430        write!(f, "{}", val)
431    }
432}
433
434impl Debug for ToStore {
435    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
436        write!(f, "ToStore {}", self)
437    }
438}
439
440/// Used for FormUrlParams
441pub enum UrlDuplicates {
442    /// True
443    True,
444    /// False
445    False,
446}
447
448impl Display for UrlDuplicates {
449    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
450        let val = match *self {
451            UrlDuplicates::True => "1",
452            UrlDuplicates::False => "0",
453        };
454
455        write!(f, "{}", val)
456    }
457}
458
459fn add_signature_expire(auth_fields: &Fields, form: Form) -> Form {
460    let form = form
461        .text("UPLOADCARE_PUB_KEY", auth_fields.pub_key.to_string())
462        .text("pub_key", auth_fields.pub_key.to_string());
463    if let None = auth_fields.signature {
464        return form;
465    }
466    form.text(
467        "signature",
468        auth_fields.signature.as_ref().unwrap().to_string(),
469    )
470    .text("expire", auth_fields.expire.as_ref().unwrap().to_string())
471}