fundamentum_sdk_mqtt/models/actions/
file_transfer_action.rs1use std::path::PathBuf;
2
3use crate::error::Error;
4use fundamentum_iot_mqtt_proto::com::fundamentum::actions::v1::{
5 self as cloud_proto, file_transfer_action::TransferType,
6};
7use url::Url;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum FileTransferAction {
12 Download(CloudToDeviceRequest),
14 Upload(DeviceToCloudRequest),
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub struct CloudToDeviceRequest {
21 pub file_path: PathBuf,
23 pub url: Url,
25 pub size: u64,
27 pub hash: String,
31 pub overwrite: bool,
33 pub metadata: Option<String>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub struct DeviceToCloudRequest {
41 pub file_path: PathBuf,
43 pub url: Url,
45}
46
47impl TryFrom<cloud_proto::FileTransferAction> for FileTransferAction {
48 type Error = Error;
49
50 fn try_from(value: cloud_proto::FileTransferAction) -> Result<Self, Self::Error> {
51 match value.transfer_type {
52 Some(TransferType::Download(download)) => {
53 let url =
54 Url::parse(&download.url).map_err(|e| Error::InvalidRequest(e.to_string()))?;
55 if url.scheme() != "https" {
56 return Err(Error::InvalidRequest(
57 "URL must be with a HTTPS scheme".to_owned(),
58 ));
59 }
60
61 if download.file_path.is_empty() {
62 return Err(Error::InvalidRequest("Path cannot be empty".to_owned()));
63 }
64 if download.size == 0 {
65 return Err(Error::InvalidRequest("File size cannot be 0".to_owned()));
66 }
67 if download.hash.is_empty() {
68 return Err(Error::InvalidRequest("Hash cannot be empty".to_owned()));
69 }
70
71 Ok(Self::Download(CloudToDeviceRequest {
72 file_path: PathBuf::from(download.file_path),
73 url,
74 size: download.size,
75 hash: download.hash,
76 overwrite: download.overwrite,
77 metadata: (!download.metadata.is_empty()).then_some(download.metadata),
78 }))
79 }
80
81 Some(TransferType::Upload(upload)) => {
82 let url =
83 Url::parse(&upload.url).map_err(|e| Error::InvalidRequest(e.to_string()))?;
84 if url.scheme() != "https" {
85 return Err(Error::InvalidRequest(
86 "URL must be with a HTTPS scheme".to_owned(),
87 ));
88 }
89
90 if upload.file_path.is_empty() {
91 return Err(Error::InvalidRequest("Path cannot be empty".to_owned()));
92 }
93
94 Ok(Self::Upload(DeviceToCloudRequest {
95 file_path: PathBuf::from(upload.file_path),
96 url,
97 }))
98 }
99 None => Err(Error::InvalidRequest(
100 "There is no transfer type in the FileTransferAction".to_owned(),
101 )),
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 use fundamentum_iot_mqtt_proto::com::fundamentum::actions::v1 as cloud_proto;
111
112 const ANY_FILE_PATH: &str = "/any/path";
113 const ANY_URL: &str = "https://aws.amazon.com/file?a=42&b=42";
114 const ANY_SIZE: u64 = 42;
115 const ANY_HASH: &str =
116 "sha384-MBO5IDfYaE6c6Aao94oZrIOiC6CGiSN2n4QUbHNPhzk5Xhm0djZLQqTpL0HzTUxk";
117 const ANY_METADATA: &str = "metadata";
118
119 #[test]
120 fn given_no_transfer_type_when_try_into_then_error() {
121 let incoming_transfer = cloud_proto::FileTransferAction {
122 transfer_type: None,
123 };
124
125 let result: Result<FileTransferAction, Error> = incoming_transfer.try_into();
126
127 assert!(result.is_err());
128 }
129
130 fn given_incoming_download(
133 request: cloud_proto::file_transfer_action::CloudToDeviceRequest,
134 ) -> cloud_proto::FileTransferAction {
135 cloud_proto::FileTransferAction {
136 transfer_type: Some(TransferType::Download(request)),
137 }
138 }
139
140 fn new_cloud_to_device_request() -> cloud_proto::file_transfer_action::CloudToDeviceRequest {
141 cloud_proto::file_transfer_action::CloudToDeviceRequest {
142 file_path: ANY_FILE_PATH.to_owned(),
143 url: ANY_URL.to_owned(),
144 size: ANY_SIZE,
145 hash: ANY_HASH.to_owned(),
146 overwrite: false,
147 metadata: ANY_METADATA.to_owned(),
148 }
149 }
150
151 #[test]
152 fn given_download_file_transfer_action_when_try_into_then_valid_transfer_action() {
153 let incoming_transfer = given_incoming_download(new_cloud_to_device_request());
154
155 let download: FileTransferAction =
156 incoming_transfer.try_into().expect("All fields are valid");
157
158 assert_eq!(
159 download,
160 FileTransferAction::Download(CloudToDeviceRequest {
161 file_path: PathBuf::from(ANY_FILE_PATH),
162 url: Url::parse(ANY_URL).expect("ANY_URL must be valid"),
163 size: ANY_SIZE,
164 hash: ANY_HASH.to_owned(),
165 overwrite: false,
166 metadata: Some(ANY_METADATA.to_owned())
167 })
168 );
169 }
170
171 #[test]
172 fn given_empty_path_when_try_into_then_error() {
173 let incoming_transfer =
174 given_incoming_download(cloud_proto::file_transfer_action::CloudToDeviceRequest {
175 file_path: String::new(),
176 ..new_cloud_to_device_request()
177 });
178
179 let result: Result<FileTransferAction, Error> = incoming_transfer.try_into();
180
181 assert!(result.is_err());
182 }
183
184 #[test]
185 fn given_non_https_url_when_try_into_then_error() {
186 let incoming_transfer =
187 given_incoming_download(cloud_proto::file_transfer_action::CloudToDeviceRequest {
188 url: "mqtts://aws.amazon.com".to_owned(),
189 ..new_cloud_to_device_request()
190 });
191
192 let result: Result<FileTransferAction, Error> = incoming_transfer.try_into();
193
194 assert!(result.is_err());
195 }
196
197 #[test]
198 fn given_empty_file_when_try_into_then_error() {
199 let incoming_transfer =
200 given_incoming_download(cloud_proto::file_transfer_action::CloudToDeviceRequest {
201 size: 0,
202 ..new_cloud_to_device_request()
203 });
204
205 let result: Result<FileTransferAction, Error> = incoming_transfer.try_into();
206
207 assert!(result.is_err());
208 }
209
210 #[test]
211 fn given_empty_hash_when_try_into_then_error() {
212 let incoming_transfer =
213 given_incoming_download(cloud_proto::file_transfer_action::CloudToDeviceRequest {
214 hash: String::new(),
215 ..new_cloud_to_device_request()
216 });
217
218 let result: Result<FileTransferAction, Error> = incoming_transfer.try_into();
219
220 assert!(result.is_err());
221 }
222
223 #[test]
224 fn given_empty_metadata_when_try_into_then_no_metadata() {
225 let incoming_transfer =
226 given_incoming_download(cloud_proto::file_transfer_action::CloudToDeviceRequest {
227 metadata: String::new(),
228 ..new_cloud_to_device_request()
229 });
230
231 let action: FileTransferAction =
232 incoming_transfer.try_into().expect("All fields are valid");
233
234 let is_none = match action {
235 FileTransferAction::Download(cloud_to_device_request) => {
236 cloud_to_device_request.metadata.is_none()
237 }
238 FileTransferAction::Upload(_) => false,
239 };
240 assert!(is_none);
241 }
242
243 #[test]
246 fn given_upload_file_transfer_action_when_try_into_then_valid_transfer_action() {
247 let incoming_transfer = cloud_proto::FileTransferAction {
248 transfer_type: Some(TransferType::Upload(
249 cloud_proto::file_transfer_action::DeviceToCloudRequest {
250 file_path: ANY_FILE_PATH.to_owned(),
251 url: ANY_URL.to_owned(),
252 },
253 )),
254 };
255
256 let upload: FileTransferAction =
257 incoming_transfer.try_into().expect("All fields are valid");
258
259 assert_eq!(
260 upload,
261 FileTransferAction::Upload(DeviceToCloudRequest {
262 file_path: PathBuf::from(ANY_FILE_PATH),
263 url: Url::parse(ANY_URL).expect("ANY_URL must be valid"),
264 })
265 );
266 }
267
268 #[test]
269 fn given_empty_path2_when_try_into_then_error() {
270 let incoming_transfer = cloud_proto::FileTransferAction {
271 transfer_type: Some(TransferType::Upload(
272 cloud_proto::file_transfer_action::DeviceToCloudRequest {
273 file_path: String::new(),
274 url: ANY_URL.to_owned(),
275 },
276 )),
277 };
278
279 let result: Result<FileTransferAction, Error> = incoming_transfer.try_into();
280
281 assert!(result.is_err());
282 }
283
284 #[test]
285 fn given_non_https_url2_when_try_into_then_error() {
286 let incoming_transfer = cloud_proto::FileTransferAction {
287 transfer_type: Some(TransferType::Upload(
288 cloud_proto::file_transfer_action::DeviceToCloudRequest {
289 file_path: ANY_FILE_PATH.to_owned(),
290 url: "mqtts://aws.amazon.com".to_owned(),
291 },
292 )),
293 };
294
295 let result: Result<FileTransferAction, Error> = incoming_transfer.try_into();
296
297 assert!(result.is_err());
298 }
299}