graph_http/traits/response_blocking_ext.rs
1use crate::blocking::UploadSessionBlocking;
2use crate::internal::{
3 copy, create_dir, parse_content_disposition, FileConfig, HttpResponseBuilderExt, RangeIter,
4 UploadSessionLink, MAX_FILE_NAME_LEN,
5};
6use graph_error::download::BlockingDownloadError;
7use graph_error::{ErrorMessage, ErrorType, GraphFailure, GraphResult};
8use std::io::Read;
9use std::path::PathBuf;
10
11pub trait ResponseBlockingExt {
12 fn job_status(&self) -> Option<GraphResult<reqwest::blocking::Response>>;
13
14 /// # Begin an upload session using any [`std::io::Reader`].<br>
15 ///
16 /// Converts the current request object into an upload session object for uploading large
17 /// files to OneDrive or SharePoint.<br>
18 ///
19 /// This method takes a `reader` object that implements the [std::io::Read] and [Send] traits,
20 /// and returns a [GraphResult] containing an [UploadSession] object.<br>
21 ///
22 /// The [UploadSession] object contains the upload URL for the file, as well as a [RangeIter] iterator
23 /// that can be used to send the file contents to the server in multiple chunks (or "ranges").
24 /// If the upload URL is not found in the response body, this method returns a `GraphFailure`
25 /// with an error message indicating that no upload URL was found.<br>
26 ///
27 ///
28 /// ## Requires reqwest::blocking::Response body to be valid JSON
29 /// The body of the reqwest::blocking::Response must be valid JSON with an
30 /// [uploadUrl] field.
31 ///
32 /// # Example
33 /// ```rust,ignore
34 /// use graph_rs_sdk::http::{ResponseBlockingExt};
35 /// use graph_rs_sdk::*;
36 ///
37 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
38 ///
39 /// // Put the path to your file and the file name itself that
40 /// // you want to upload to one drive.
41 /// static LOCAL_FILE_PATH: &str = "/path/to/file/file.txt";
42 ///
43 /// // Parent folder id of where to store this file.
44 /// static DRIVE_PARENT_ID: &str = "PARENT_ID";
45 ///
46 /// // The conflict behavior can be one of: fail, rename, or replace.
47 /// static CONFLICT_BEHAVIOR: &str = "rename";
48 ///
49 /// #[tokio::main]
50 /// async fn main() -> GraphResult<()> {
51 /// let client = Graph::new(ACCESS_TOKEN);
52 ///
53 /// let conflict_behavior = CONFLICT_BEHAVIOR.to_string();
54 /// let upload = serde_json::json!({
55 /// "@microsoft.graph.conflictBehavior": Some(conflict_behavior)
56 /// });
57 ///
58 /// let response = client
59 /// .me()
60 /// .drive()
61 /// .item_by_path(PATH_IN_ONE_DRIVE)
62 /// .create_upload_session(&upload)
63 /// .send()
64 /// .unwrap();
65 ///
66 /// let file = std::fs::File::open(PATH_IN_ONE_DRIVE)?;
67 ///
68 /// let upload_session_task = response.into_upload_session(file)?;
69 ///
70 /// for result in upload_session_task {
71 /// let response = result?;
72 /// println!("{:#?}", response);
73 /// let body: serde_json::Value = response.json().unwrap();
74 /// println!("{:#?}", body);
75 /// }
76 ///
77 ///
78 /// Ok(())
79 /// }
80 /// ```
81 fn into_upload_session(
82 self,
83 reader: impl std::io::Read + Send,
84 ) -> GraphResult<UploadSessionBlocking>;
85
86 /// # Downloads the content of the HTTP response and saves it to a file.<br>
87 ///
88 /// This method takes a `file_config` object containing various parameters that control how the
89 /// file is downloaded and saved. The `file_config` object includes the file path, file name,
90 /// whether to create the directory recursively, whether to overwrite existing files, and the
91 /// desired file extension.<br><br>
92 ///
93 /// If `create_dir_all` is set to true (default is true), this method will create the directory at the specified
94 /// path if it doesn't exist yet. If it is set to false and the target directory doesn't exist,
95 /// this method will return an [BlockingDownloadError] with an error message indicating that the
96 /// target directory does not exist.<br><br>
97 ///
98 /// The [`FileConfig::file_name`] parameter can be used to specify a custom file name for the downloaded file.
99 /// If it is not provided, the method will attempt to parse the `Content-Disposition` header to extract the file name.
100 /// If no file name can be obtained from the header, this method will return an [BlockingDownloadError::NoFileName]
101 /// with an error message indicating that no file name was found.<br><br>
102 ///
103 /// If the [`FileConfig::extension`] parameter is set to a non-empty string,
104 /// this method will set the file extension of the downloaded file to the specified value. <br><br>
105 ///
106 /// If the target file already exists and [`overwrite_existing_file`] is set to false,
107 /// this method will return an [BlockingDownloadError::FileExists] with an error message
108 /// indicating that the file already exists and cannot be overwritten. <br><br>
109 ///
110 /// If the file is downloaded and saved successfully, this method returns a [`http::Response<PathBuf>`] object
111 /// containing the path to the downloaded file.
112 ///
113 ///
114 /// # Example
115 ///
116 /// ```rust,ignore
117 /// use graph_rs_sdk::http::{BodyRead, FileConfig};
118 /// use graph_rs_sdk::*;
119 ///
120 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
121 ///
122 /// static ITEM_ID: &str = "ITEM_ID";
123 ///
124 /// static FORMAT: &str = "pdf";
125 ///
126 /// static DOWNLOAD_DIRECTORY: &str = "./examples";
127 ///
128 /// #[tokio::main]
129 /// async fn main() -> GraphResult<()> {
130 /// let client = Graph::new(ACCESS_TOKEN);
131 ///
132 /// let response = client
133 /// .me()
134 /// .drive()
135 /// .item(ITEM_ID)
136 /// .get_items_content()
137 /// .format(FORMAT)
138 /// .send()?;
139 ///
140 /// println!("{response:#?}");
141 ///
142 /// let response2 = response.download(&FileConfig::new(DOWNLOAD_DIRECTORY))
143 /// .send()?;
144 ///
145 /// let path_buf = response2.body();
146 /// println!("{:#?}", path_buf.metadata());
147 ///
148 /// Ok(())
149 /// }
150 /// ```
151 /// <br><br>
152 /// # Example format and rename
153 ///
154 /// ```rust,ignore
155 /// use graph_rs_sdk::http::{BodyRead, FileConfig};
156 /// use graph_rs_sdk::*;
157 /// use std::ffi::OsStr;
158 ///
159 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
160 ///
161 /// static ITEM_ID: &str = "ITEM_ID";
162 ///
163 /// static FORMAT: &str = "pdf";
164 ///
165 /// static DOWNLOAD_DIRECTORY: &str = "./examples";
166 ///
167 /// static FILE_NAME: &str = "new_file_name.pdf";
168 ///
169 /// #[tokio::main]
170 /// async fn main() -> GraphResult<()> {
171 /// let client = Graph::new(ACCESS_TOKEN);
172 ///
173 /// let response = client
174 /// .me()
175 /// .drive()
176 /// .item(ITEM_ID)
177 /// .get_items_content()
178 /// .format(FORMAT)
179 /// .send()?;
180 ///
181 /// println!("{response:#?}");
182 ///
183 /// let file_config = FileConfig::new(DOWNLOAD_DIRECTORY)
184 /// .file_name(OsStr::new(FILE_NAME));
185 ///
186 /// let response2 = response.download(file_config)
187 /// .send()?;
188 ///
189 /// let path_buf = response2.body();
190 /// println!("{:#?}", path_buf.metadata());
191 ///
192 /// Ok(())
193 /// }
194 /// ```
195 fn download(
196 self,
197 file_config: &FileConfig,
198 ) -> Result<http::Response<PathBuf>, BlockingDownloadError>;
199
200 /// If the response is a server error then Microsoft Graph will return
201 /// an error in the response body. The [`ErrorMessage`] type maps to these
202 /// errors and this method deserializes to this type.
203 ///
204 /// Microsoft Graph does not return this error message in all situations so it
205 /// make sure to handle cases where the body could not be deserialized properly.
206 /// ```rust,ignore
207 /// let status = response.status();
208 ///
209 /// if status.is_server_error() || status.is_client_error() {
210 /// let error_message = response.into_error_message().unwrap();
211 /// println!("{error_message:#?}");
212 ///
213 /// // This is the same thing as doing
214 /// let error_message: ErrorMessage = response.json().unwrap();
215 /// }
216 /// ```
217 fn into_graph_error_message(self) -> Result<ErrorMessage, reqwest::Error>;
218
219 /// Microsoft Graph specific status code errors mapped from the response [StatusCode].
220 /// Not all status codes map to a Microsoft Graph error.
221 ///
222 /// Use [`ErrorType::as_str`] to get a short description of the Microsoft Graph specific error.
223 /// ```rust,ignore
224 /// let error_type = response.graph_error_type().unwrap();
225 /// println!("{:#?}", error_type.as_str());
226 /// ```
227 fn graph_error_type(&self) -> Option<ErrorType>;
228}
229
230impl ResponseBlockingExt for reqwest::blocking::Response {
231 fn job_status(&self) -> Option<GraphResult<reqwest::blocking::Response>> {
232 let url = self
233 .headers()
234 .get(reqwest::header::LOCATION)?
235 .to_str()
236 .ok()?;
237 let result = reqwest::blocking::Client::new()
238 .get(url)
239 .send()
240 .map_err(GraphFailure::from);
241
242 Some(result)
243 }
244
245 /// # Begin an upload session using any [`std::io::Reader`].<br>
246 ///
247 /// Converts the current request object into an upload session object for uploading large
248 /// files to OneDrive or SharePoint.<br>
249 ///
250 /// This method takes a `reader` object that implements the [std::io::Read] and [Send] traits,
251 /// and returns a [GraphResult] containing an [UploadSession] object.<br>
252 ///
253 /// The [UploadSession] object contains the upload URL for the file, as well as a [RangeIter] iterator
254 /// that can be used to send the file contents to the server in multiple chunks (or "ranges").
255 /// If the upload URL is not found in the response body, this method returns a `GraphFailure`
256 /// with an error message indicating that no upload URL was found.<br>
257 ///
258 ///
259 /// ## Requires reqwest::blocking::Response body to be valid JSON
260 /// The body of the reqwest::blocking::Response must be valid JSON with an
261 /// [uploadUrl] field.
262 ///
263 /// # Example
264 /// ```rust,ignore
265 /// use graph_rs_sdk::http::{ResponseBlockingExt};
266 /// use graph_rs_sdk::*;
267 ///
268 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
269 ///
270 /// // Put the path to your file and the file name itself that
271 /// // you want to upload to one drive.
272 /// static LOCAL_FILE_PATH: &str = "/path/to/file/file.txt";
273 ///
274 /// // Parent folder id of where to store this file.
275 /// static DRIVE_PARENT_ID: &str = "PARENT_ID";
276 ///
277 /// // The conflict behavior can be one of: fail, rename, or replace.
278 /// static CONFLICT_BEHAVIOR: &str = "rename";
279 ///
280 /// #[tokio::main]
281 /// async fn main() -> GraphResult<()> {
282 /// let client = Graph::new(ACCESS_TOKEN);
283 ///
284 /// let conflict_behavior = CONFLICT_BEHAVIOR.to_string();
285 /// let upload = serde_json::json!({
286 /// "@microsoft.graph.conflictBehavior": Some(conflict_behavior)
287 /// });
288 ///
289 /// let response = client
290 /// .me()
291 /// .drive()
292 /// .item_by_path(PATH_IN_ONE_DRIVE)
293 /// .create_upload_session(&upload)
294 /// .send()
295 /// .unwrap();
296 ///
297 /// let file = std::fs::File::open(PATH_IN_ONE_DRIVE)?;
298 ///
299 /// let upload_session_task = response.into_upload_session(file)?;
300 ///
301 /// for result in upload_session_task {
302 /// let response = result?;
303 /// println!("{:#?}", response);
304 /// let body: serde_json::Value = response.json().unwrap();
305 /// println!("{:#?}", body);
306 /// }
307 ///
308 ///
309 /// Ok(())
310 /// }
311 /// ```
312 fn into_upload_session(self, reader: impl Read + Send) -> GraphResult<UploadSessionBlocking> {
313 let body: serde_json::Value = self.json()?;
314 let url = body
315 .upload_session_link()
316 .ok_or_else(|| GraphFailure::not_found("No uploadUrl found in response body"))?;
317
318 let range_iter = RangeIter::from_reader(reader)?;
319 Ok(UploadSessionBlocking::new(
320 reqwest::Url::parse(url.as_str())?,
321 range_iter,
322 ))
323 }
324
325 /// # Downloads the content of the HTTP response and saves it to a file.<br>
326 ///
327 /// This method takes a `file_config` object containing various parameters that control how the
328 /// file is downloaded and saved. The `file_config` object includes the file path, file name,
329 /// whether to create the directory recursively, whether to overwrite existing files, and the
330 /// desired file extension.<br><br>
331 ///
332 /// If `create_dir_all` is set to true (default is true), this method will create the directory at the specified
333 /// path if it doesn't exist yet. If it is set to false and the target directory doesn't exist,
334 /// this method will return an [BlockingDownloadError] with an error message indicating that the
335 /// target directory does not exist.<br><br>
336 ///
337 /// The [`FileConfig::file_name`] parameter can be used to specify a custom file name for the downloaded file.
338 /// If it is not provided, the method will attempt to parse the `Content-Disposition` header to extract the file name.
339 /// If no file name can be obtained from the header, this method will return an [BlockingDownloadError::NoFileName]
340 /// with an error message indicating that no file name was found.<br><br>
341 ///
342 /// If the [`FileConfig::extension`] parameter is set to a non-empty string,
343 /// this method will set the file extension of the downloaded file to the specified value. <br><br>
344 ///
345 /// If the target file already exists and [`overwrite_existing_file`] is set to false,
346 /// this method will return an [BlockingDownloadError::FileExists] with an error message
347 /// indicating that the file already exists and cannot be overwritten. <br><br>
348 ///
349 /// If the file is downloaded and saved successfully, this method returns a [`http::Response<PathBuf>`] object
350 /// containing the path to the downloaded file.
351 ///
352 ///
353 /// # Example
354 ///
355 /// ```rust,ignore
356 /// use graph_rs_sdk::http::{BodyRead, FileConfig, ResponseBlockingExt};
357 /// use graph_rs_sdk::*;
358 ///
359 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
360 ///
361 /// static ITEM_ID: &str = "ITEM_ID";
362 ///
363 /// static FORMAT: &str = "pdf";
364 ///
365 /// static DOWNLOAD_DIRECTORY: &str = "./examples";
366 ///
367 /// #[tokio::main]
368 /// async fn main() -> GraphResult<()> {
369 /// let client = Graph::new(ACCESS_TOKEN);
370 ///
371 /// let response = client
372 /// .me()
373 /// .drive()
374 /// .item(ITEM_ID)
375 /// .get_items_content()
376 /// .format(FORMAT)
377 /// .send()?;
378 ///
379 /// println!("{response:#?}");
380 ///
381 /// let response2 = response.download(&FileConfig::new(DOWNLOAD_DIRECTORY))
382 /// .send()?;
383 ///
384 /// let path_buf = response2.body();
385 /// println!("{:#?}", path_buf.metadata());
386 ///
387 /// Ok(())
388 /// }
389 /// ```
390 /// <br><br>
391 /// # Example format and rename
392 ///
393 /// ```rust,ignore
394 /// use graph_rs_sdk::http::{BodyRead, FileConfig};
395 /// use graph_rs_sdk::*;
396 /// use std::ffi::OsStr;
397 ///
398 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
399 ///
400 /// static ITEM_ID: &str = "ITEM_ID";
401 ///
402 /// static FORMAT: &str = "pdf";
403 ///
404 /// static DOWNLOAD_DIRECTORY: &str = "./examples";
405 ///
406 /// static FILE_NAME: &str = "new_file_name.pdf";
407 ///
408 /// #[tokio::main]
409 /// async fn main() -> GraphResult<()> {
410 /// let client = Graph::new(ACCESS_TOKEN);
411 ///
412 /// let response = client
413 /// .me()
414 /// .drive()
415 /// .item(ITEM_ID)
416 /// .get_items_content()
417 /// .format(FORMAT)
418 /// .send()?;
419 ///
420 /// println!("{response:#?}");
421 ///
422 /// let file_config = FileConfig::new(DOWNLOAD_DIRECTORY)
423 /// .file_name(OsStr::new(FILE_NAME));
424 ///
425 /// let response2 = response.download(file_config)
426 /// .send()?;
427 ///
428 /// let path_buf = response2.body();
429 /// println!("{:#?}", path_buf.metadata());
430 ///
431 /// Ok(())
432 /// }
433 /// ```
434 fn download(
435 self,
436 file_config: &FileConfig,
437 ) -> Result<http::Response<PathBuf>, BlockingDownloadError> {
438 let path = file_config.path.clone();
439 let file_name = file_config.file_name.clone();
440 let create_dir_all = file_config.create_directory_all;
441 let overwrite_existing_file = file_config.overwrite_existing_file;
442 let extension = file_config.extension.clone();
443
444 if create_dir_all {
445 create_dir(path.as_path())?;
446 } else if !path.exists() {
447 return Err(BlockingDownloadError::TargetDoesNotExist(
448 path.to_string_lossy().to_string(),
449 ));
450 }
451
452 let path = {
453 if let Some(name) = file_name.or_else(|| parse_content_disposition(self.headers())) {
454 if name.len() > MAX_FILE_NAME_LEN {
455 return Err(BlockingDownloadError::FileNameTooLong);
456 }
457 path.join(name)
458 } else {
459 return Err(BlockingDownloadError::NoFileName);
460 }
461 };
462
463 if let Some(ext) = extension.as_ref() {
464 path.with_extension(ext.as_os_str());
465 }
466
467 if path.exists() && !overwrite_existing_file {
468 return Err(BlockingDownloadError::FileExists(
469 path.to_string_lossy().to_string(),
470 ));
471 }
472
473 let status = self.status();
474 let url = self.url().clone();
475 let _headers = self.headers().clone();
476 let version = self.version();
477
478 Ok(http::Response::builder()
479 .url(url)
480 .status(http::StatusCode::from(&status))
481 .version(version)
482 .body(copy(path, self)?)?)
483 }
484
485 /// If the response is a server error then Microsoft Graph will return
486 /// an error in the response body. The [`ErrorMessage`] type maps to these
487 /// errors and this method deserializes to this type.
488 ///
489 /// Microsoft Graph does not return this error message in all situations so it
490 /// make sure to handle cases where the body could not be deserialized properly.
491 /// ```rust,ignore
492 /// let status = response.status();
493 ///
494 /// if status.is_server_error() || status.is_client_error() {
495 /// let error_message = response.into_error_message().unwrap();
496 /// println!("{error_message:#?}");
497 ///
498 /// // This is the same thing as doing
499 /// let error_message: ErrorMessage = response.json().unwrap();
500 /// }
501 /// ```
502 fn into_graph_error_message(self) -> Result<ErrorMessage, reqwest::Error> {
503 self.json()
504 }
505
506 /// Microsoft Graph specific status code errors mapped from the response [StatusCode].
507 /// Not all status codes map to a Microsoft Graph error.
508 ///
509 /// Use [`ErrorType::as_str`] to get a short description of the Microsoft Graph specific error.
510 /// ```rust,ignore
511 /// let error_type = response.graph_error_type().unwrap();
512 /// println!("{:#?}", error_type.as_str());
513 /// ```
514 fn graph_error_type(&self) -> Option<ErrorType> {
515 let status = self.status();
516 ErrorType::from_u16(status.as_u16())
517 }
518}