remoteit_api/
file_upload.rs1use bon::{bon, builder, Builder};
8use std::path::PathBuf;
9
10use crate::auth::{build_auth_header, get_date};
11
12#[derive(Debug, Clone, Builder)]
14pub struct FileUpload {
15 pub file_name: String,
17 #[builder(into)]
19 pub file_path: PathBuf,
20 pub executable: bool,
22 pub short_desc: Option<String>,
24 pub long_desc: Option<String>,
26}
27
28#[derive(serde::Deserialize, Clone, Debug)]
30#[serde(rename_all = "camelCase")]
31pub struct UploadFileResponse {
32 pub file_id: String,
34 pub file_version_id: String,
36 pub version: u32,
38 pub name: String,
40 pub executable: bool,
42 pub owner_id: String,
44 pub file_arguments: Vec<serde_json::Value>,
47}
48
49#[derive(serde::Deserialize, Clone, Debug)]
51pub struct ErrorResponse {
52 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 #[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 #[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}