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}