remoteit_api/
file_upload.rs

1//! Enabled by the `file_upload` feature. Contains structs and impl blocks related to uploading files to remote.it.
2//!
3//! On the docs page of this module, you can only see the builder structs for the functions.
4//!
5//! Please see [`R3Client`](crate::R3Client) for the actual functions you can call.
6
7use bon::{bon, builder, Builder};
8use std::path::PathBuf;
9
10use crate::auth::{build_auth_header, get_date};
11
12/// Struct to hold the details of a file to be uploaded to remote.it.
13#[derive(Debug, Clone, Builder)]
14pub struct FileUpload {
15    /// The name of the file. This is what the file will be called in the remote.it system.
16    pub file_name: String,
17    /// The path to the file on the local filesystem.
18    #[builder(into)]
19    pub file_path: PathBuf,
20    /// Whether the file is an executable script or an asset.
21    pub executable: bool,
22    /// A short description of the file.
23    pub short_desc: Option<String>,
24    /// A long description of the file.
25    pub long_desc: Option<String>,
26}
27
28/// The positive response from the remote.it API when uploading a file.
29#[derive(serde::Deserialize, Clone, Debug)]
30#[serde(rename_all = "camelCase")]
31pub struct UploadFileResponse {
32    /// The ID of the file. You can use this to reference the file in other API calls.
33    pub file_id: String,
34    /// The ID of the version of the file. You can use this to reference the version in other API calls.
35    pub file_version_id: String,
36    /// The version of the file. When you upload a file with the same name as one that already exists, the version will be incremented.
37    pub version: u32,
38    /// The name of the file.
39    pub name: String,
40    /// Whether the file is an executable script or an asset.
41    pub executable: bool,
42    /// The User ID of the owner of the file.
43    pub owner_id: String,
44    /// The available arguments for this file, if it is an executable script.
45    /// See <https://docs.remote.it/developer-tools/device-scripting#creating-scripts> for more information.
46    pub file_arguments: Vec<serde_json::Value>,
47}
48
49/// The negative response from the remote.it API when uploading a file.
50#[derive(serde::Deserialize, Clone, Debug)]
51pub struct ErrorResponse {
52    /// The error message returned by the API.
53    pub message: String,
54}
55
56#[derive(thiserror::Error, Debug)]
57#[allow(missing_docs)]
58pub enum UploadFileError {
59    #[error("IO error while uploading file: {0}")]
60    IO(#[from] std::io::Error),
61    #[error("Failed to send upload file request: {0}")]
62    Reqwest(#[from] reqwest::Error),
63    #[error("Failed to parse response JSON: {0}")]
64    ParseJson(reqwest::Error),
65    #[error("The API returned an error: {0:?}")]
66    ApiError(ErrorResponse),
67}
68
69#[cfg(feature = "blocking")]
70#[bon]
71impl crate::R3Client {
72    /// Upload a file to remote.it.
73    /// The file could be an executable script, or any other file to be used as a resource in scripts.
74    ///
75    /// Note: This is not GraphQL, it is a multipart form upload
76    ///
77    /// # Returns
78    /// The response from the remote.it API. Contains the ID of the file and the version among other things. See [`UploadFileResponse`] for more details.
79    ///
80    /// # Errors
81    /// - [`UploadFileError::IO`] if there is an error reading the file.
82    /// - [`UploadFileError::Reqwest`] if there is an error sending the request.
83    /// - [`UploadFileError::ApiError`] if the remote.it API returns an error response.
84    /// - [`UploadFileError::ParseJson`] if there is an error parsing the response.
85    #[builder]
86    pub fn upload_file(
87        &self,
88        file_upload: FileUpload,
89    ) -> Result<UploadFileResponse, UploadFileError> {
90        use crate::BASE_URL;
91        use crate::FILE_UPLOAD_PATH;
92
93        let client = reqwest::blocking::Client::new();
94        let mut form = reqwest::blocking::multipart::Form::new()
95            .file(file_upload.file_name, file_upload.file_path)?
96            .text("executable", file_upload.executable.to_string());
97
98        if let Some(short_descr) = file_upload.short_desc {
99            form = form.text("shortDesc", short_descr);
100        }
101        if let Some(long_descr) = file_upload.long_desc {
102            form = form.text("longDesc", long_descr);
103        }
104
105        #[cfg(debug_assertions)]
106        dbg!(&form);
107
108        let content_type = format!("multipart/form-data; boundary={}", form.boundary());
109        let date = get_date();
110        let auth_header = build_auth_header()
111            .key_id(self.credentials.access_key_id())
112            .key(self.credentials.key())
113            .content_type(&content_type)
114            .method(&reqwest::Method::POST)
115            .path(FILE_UPLOAD_PATH)
116            .date(&date)
117            .call();
118
119        let response = client
120            .post(format!("{BASE_URL}{FILE_UPLOAD_PATH}"))
121            .header("Date", date)
122            .header("Authorization", auth_header)
123            .header("Content-Type", content_type)
124            .multipart(form)
125            .send()?;
126
127        if response.status().is_success() {
128            let file_upload_response = response
129                .json::<UploadFileResponse>()
130                .map_err(UploadFileError::ParseJson)?;
131            Ok(file_upload_response)
132        } else {
133            let response: ErrorResponse =
134                response.json().map_err(UploadFileError::ParseJson)?;
135            Err(UploadFileError::ApiError(response))
136        }
137    }
138}
139
140#[cfg(feature = "async")]
141#[bon]
142impl crate::R3Client {
143    /// Upload a file to remote.it.
144    /// The file could be an executable script, or any other file to be used as a resource in scripts.
145    ///
146    /// Note: This is not GraphQL, it is a multipart form upload
147    ///
148    /// # Returns
149    /// The response from the remote.it API. Contains the ID of the file and the version among other things. See [`UploadFileResponse`] for more details.
150    ///
151    /// # Errors
152    /// - [`UploadFileError::IO`] if there is an error reading the file.
153    /// - [`UploadFileError::Reqwest`] if there is an error sending the request.
154    /// - [`UploadFileError::ApiError`] if the remote.it API returns an error response.
155    /// - [`UploadFileError::ParseJson`] if there is an error parsing the response.
156    #[builder]
157    pub async fn upload_file_async(
158        &self,
159        file_upload: FileUpload,
160    ) -> Result<UploadFileResponse, UploadFileError> {
161        use crate::BASE_URL;
162        use crate::FILE_UPLOAD_PATH;
163
164        let client = reqwest::Client::new();
165
166        let file_name = file_upload
167            .file_path
168            .file_name()
169            .map(|val| val.to_string_lossy().to_string())
170            .unwrap_or_default();
171
172        let file = tokio::fs::File::open(&file_upload.file_name).await?;
173
174        let reader = reqwest::Body::wrap_stream(tokio_util::codec::FramedRead::new(
175            file,
176            tokio_util::codec::BytesCodec::new(),
177        ));
178        let mut form = reqwest::multipart::Form::new()
179            .part(
180                file_upload.file_name,
181                reqwest::multipart::Part::stream(reader).file_name(file_name),
182            )
183            .text("executable", file_upload.executable.to_string());
184
185        if let Some(short_descr) = file_upload.short_desc {
186            form = form.text("shortDesc", short_descr);
187        }
188        if let Some(long_descr) = file_upload.long_desc {
189            form = form.text("longDesc", long_descr);
190        }
191
192        #[cfg(debug_assertions)]
193        dbg!(&form);
194
195        let content_type = format!("multipart/form-data; boundary={}", form.boundary());
196        let date = get_date();
197        let auth_header = build_auth_header()
198            .key_id(self.credentials.access_key_id())
199            .key(self.credentials.key())
200            .content_type(&content_type)
201            .method(&reqwest::Method::POST)
202            .path(FILE_UPLOAD_PATH)
203            .date(&date)
204            .call();
205
206        let response = client
207            .post(format!("{BASE_URL}{FILE_UPLOAD_PATH}"))
208            .header("Date", date)
209            .header("Authorization", auth_header)
210            .header("Content-Type", content_type)
211            .multipart(form)
212            .send()
213            .await?;
214
215        if response.status().is_success() {
216            let file_upload_response = response
217                .json::<UploadFileResponse>()
218                .await
219                .map_err(UploadFileError::ParseJson)?;
220            Ok(file_upload_response)
221        } else {
222            let response: ErrorResponse = response
223                .json()
224                .await
225                .map_err(UploadFileError::ParseJson)?;
226            Err(UploadFileError::ApiError(response))
227        }
228    }
229}