graph_http/traits/response_ext.rs
1use crate::internal::{
2 copy_async, create_dir_async, FileConfig, HttpResponseBuilderExt, RangeIter, UploadSession,
3};
4use crate::traits::UploadSessionLink;
5use async_trait::async_trait;
6use graph_error::download::AsyncDownloadError;
7use graph_error::{ErrorMessage, ErrorType, GraphFailure, GraphResult};
8use reqwest::header::HeaderMap;
9use reqwest::Response;
10use std::ffi::OsString;
11use std::path::PathBuf;
12use tokio::io::AsyncReadExt;
13
14pub(crate) const MAX_FILE_NAME_LEN: usize = 255;
15
16#[allow(clippy::single_char_pattern)]
17pub(crate) fn parse_content_disposition(headers: &HeaderMap) -> Option<OsString> {
18 if let Some(value) = headers.get("content-disposition") {
19 if let Ok(header) = std::str::from_utf8(value.as_ref()) {
20 let mut v: Vec<&str> = header.split(';').collect();
21 v.retain(|s| !s.is_empty());
22
23 // The filename* indicates that the filename is encoded
24 if let Some(value) = v.iter().find(|s| s.starts_with("filename*=utf-8''")) {
25 let s = value.replace("filename*=utf-8''", "");
26 if let Ok(s) = percent_encoding::percent_decode(s.as_bytes()).decode_utf8() {
27 return Some(OsString::from(s.to_string().replace('/', "-")));
28 }
29 }
30
31 if let Some(value) = v.last() {
32 if value.trim_start().starts_with("filename=") {
33 return Some(OsString::from(
34 value
35 .replace("\"", "")
36 .replace("filename=", "")
37 .replace('/', "-")
38 .trim(),
39 ));
40 }
41 }
42 }
43 }
44 None
45}
46
47#[async_trait]
48pub trait ResponseExt {
49 async fn job_status(&self) -> Option<GraphResult<reqwest::Response>>;
50
51 /// # Begin an upload session using any [`std::io::Reader`].<br>
52 ///
53 /// Converts the current request object into an upload session object for uploading large
54 /// files to OneDrive or SharePoint.<br>
55 ///
56 /// This method takes a `reader` object that implements the [std::io::Read] and [Send] traits,
57 /// and returns a [GraphResult] containing an [UploadSession] object.<br>
58 ///
59 /// The [UploadSession] object contains the upload URL for the file, as well as a [RangeIter] iterator
60 /// that can be used to send the file contents to the server in multiple chunks (or "ranges").
61 /// If the upload URL is not found in the response body, this method returns a `GraphFailure`
62 /// with an error message indicating that no upload URL was found.<br>
63 ///
64 ///
65 /// ## Requires reqwest::Response body to be valid JSON
66 /// The body of the reqwest::Response must be valid JSON with an
67 /// [uploadUrl] field.
68 ///
69 /// # Example
70 /// ```rust,ignore
71 /// use graph_rs_sdk::http::{AsyncIterator, ResponseExt};
72 /// use graph_rs_sdk::*;
73 ///
74 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
75 ///
76 /// // Put the path to your file and the file name itself that
77 /// // you want to upload to one drive.
78 /// static LOCAL_FILE_PATH: &str = "/path/to/file/file.txt";
79 ///
80 /// // Parent folder id of where to store this file.
81 /// static DRIVE_PARENT_ID: &str = "PARENT_ID";
82 ///
83 /// // The conflict behavior can be one of: fail, rename, or replace.
84 /// static CONFLICT_BEHAVIOR: &str = "rename";
85 /// #[tokio::main]
86 /// async fn main() -> GraphResult<()> {
87 /// let client = Graph::new(ACCESS_TOKEN);
88 ///
89 /// let conflict_behavior = CONFLICT_BEHAVIOR.to_string()
90 /// let upload = serde_json::json!({
91 /// "@microsoft.graph.conflictBehavior": Some(conflict_behavior)
92 /// });
93 ///
94 /// let response = client
95 /// .me()
96 /// .drive()
97 /// .item_by_path(PATH_IN_ONE_DRIVE)
98 /// .create_upload_session(&upload)
99 /// .send()
100 /// .await
101 /// .unwrap();
102 ///
103 /// let file = std::fs::File::open(PATH_IN_ONE_DRIVE)?;
104 ///
105 /// let mut iter = response.into_upload_session(file).await?;
106 ///
107 /// while let Some(result) = iter.next().await {
108 /// let response = result?;
109 /// println!("{response:#?}");
110 /// }
111 ///
112 /// Ok(())
113 /// }
114 /// ```
115 async fn into_upload_session(
116 self,
117 reader: impl std::io::Read + Send,
118 ) -> GraphResult<UploadSession>;
119
120 /// # Begin an upload session using any [tokio::io::AsyncReadExt].<br>
121 ///
122 /// Converts the current request object into an upload session object for uploading large
123 /// files to OneDrive or SharePoint.<br>
124 ///
125 /// This method takes a `reader` object that implements the [tokio::io::AsyncReadExt], [Send], and [Unpin] traits,
126 /// and returns a [GraphResult] containing an [UploadSession] object.<br>
127 ///
128 /// The [UploadSession] object contains the upload URL for the file, as well as a [RangeIter] iterator
129 /// that can be used to send the file contents to the server in multiple chunks (or "ranges").
130 /// If the upload URL is not found in the response body, this method returns a `GraphFailure`
131 /// with an error message indicating that no upload URL was found.<br>
132 ///
133 ///
134 /// ## Requires reqwest::Response body can be deserialized to valid JSON
135 /// The body of the reqwest::Response must be valid JSON with an
136 /// [uploadUrl] field.
137 ///
138 /// # Example
139 /// ```rust,ignore
140 /// use graph_rs_sdk::http::{AsyncIterator, ResponseExt};
141 /// use graph_rs_sdk::*;
142 ///
143 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
144 ///
145 /// // Put the path to your file and the file name itself that
146 /// // you want to upload to one drive.
147 /// static LOCAL_FILE_PATH: &str = "/path/to/file/file.txt";
148 ///
149 /// // Parent folder id of where to store this file.
150 /// static DRIVE_PARENT_ID: &str = "PARENT_ID";
151 ///
152 /// // The conflict behavior can be one of: fail, rename, or replace.
153 /// static CONFLICT_BEHAVIOR: &str = "rename";
154 ///
155 /// #[tokio::main]
156 /// async fn main() -> GraphResult<()> {
157 /// let client = Graph::new(ACCESS_TOKEN);
158 ///
159 /// let conflict_behavior = CONFLICT_BEHAVIOR.to_string()
160 /// let upload = serde_json::json!({
161 /// "@microsoft.graph.conflictBehavior": Some(conflict_behavior)
162 /// });
163 ///
164 /// let response = client
165 /// .me()
166 /// .drive()
167 /// .item_by_path(PATH_IN_ONE_DRIVE)
168 /// .create_upload_session(&upload)
169 /// .send()
170 /// .await
171 /// .unwrap();
172 ///
173 /// let file = tokio::fs::File::open(PATH_IN_ONE_DRIVE).await?;
174 ///
175 /// let mut iter = response.into_upload_session_async_read(file).await?;
176 ///
177 /// while let Some(result) = iter.next().await {
178 /// let response = result?;
179 /// println!("{response:#?}");
180 /// }
181 ///
182 /// Ok(())
183 /// }
184 /// ```
185 async fn into_upload_session_async_read(
186 self,
187 reader: impl AsyncReadExt + Send + Unpin,
188 ) -> GraphResult<UploadSession>;
189
190 /// # Downloads the content of the HTTP response and saves it to a file.<br>
191 ///
192 /// This method takes a `file_config` object containing various parameters that control how the
193 /// file is downloaded and saved. The `file_config` object includes the file path, file name,
194 /// whether to create the directory recursively, whether to overwrite existing files, and the
195 /// desired file extension.<br><br>
196 ///
197 /// If `create_dir_all` is set to true (default is true), this method will recursively create the directory
198 /// at the path specified if it doesn't exist yet. If it is set to false and the target directory doesn't exist,
199 /// this method will return an [AsyncDownloadError] with an error message indicating that the
200 /// target directory does not exist.<br><br>
201 ///
202 /// The [`FileConfig::file_name`] parameter can be used to specify a custom file name for the downloaded file.
203 /// If it is not provided, the method will attempt to parse the `Content-Disposition` header to extract the file name.
204 /// If no file name can be obtained from the header, this method will return an [AsyncDownloadError::NoFileName]
205 /// with an error message indicating that no file name was found.<br><br>
206 ///
207 /// If the [`FileConfig::extension`] parameter is set to a non-empty string,
208 /// this method will set the file extension of the downloaded file to the specified value. <br><br>
209 ///
210 /// If the target file already exists and [`overwrite_existing_file`] is set to false,
211 /// this method will return an [AsyncDownloadError::FileExists] with an error message
212 /// indicating that the file already exists and cannot be overwritten. <br><br>
213 ///
214 /// If the file is downloaded and saved successfully, this method returns a [`http::Response<PathBuf>`] object
215 /// containing the path to the downloaded file.
216 ///
217 ///
218 /// # Example
219 ///
220 /// ```rust,ignore
221 /// use graph_rs_sdk::http::{BodyRead, FileConfig};
222 /// use graph_rs_sdk::*;
223 ///
224 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
225 ///
226 /// static ITEM_ID: &str = "ITEM_ID";
227 ///
228 /// static FORMAT: &str = "pdf";
229 ///
230 /// static DOWNLOAD_DIRECTORY: &str = "./examples";
231 ///
232 /// #[tokio::main]
233 /// async fn main() -> GraphResult<()> {
234 /// let client = Graph::new(ACCESS_TOKEN);
235 ///
236 /// let response = client
237 /// .me()
238 /// .drive()
239 /// .item(ITEM_ID)
240 /// .get_items_content()
241 /// .format(FORMAT)
242 /// .send()
243 /// .await?;
244 ///
245 /// println!("{response:#?}");
246 ///
247 /// let response2 = response.download(&FileConfig::new(DOWNLOAD_DIRECTORY))
248 /// .send()
249 /// .await?;
250 ///
251 /// let path_buf = response2.body();
252 /// println!("{:#?}", path_buf.metadata());
253 ///
254 /// Ok(())
255 /// }
256 /// ```
257 /// <br><br>
258 /// # Example format and rename
259 ///
260 /// ```rust,ignore
261 /// use graph_rs_sdk::http::{BodyRead, FileConfig};
262 /// use graph_rs_sdk::*;
263 /// use std::ffi::OsStr;
264 ///
265 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
266 ///
267 /// static ITEM_ID: &str = "ITEM_ID";
268 ///
269 /// static FORMAT: &str = "pdf";
270 ///
271 /// static DOWNLOAD_DIRECTORY: &str = "./examples";
272 ///
273 /// static FILE_NAME: &str = "new_file_name.pdf";
274 ///
275 /// #[tokio::main]
276 /// async fn main() -> GraphResult<()> {
277 /// let client = Graph::new(ACCESS_TOKEN);
278 ///
279 /// let response = client
280 /// .me()
281 /// .drive()
282 /// .item(ITEM_ID)
283 /// .get_items_content()
284 /// .format(FORMAT)
285 /// .send()
286 /// .await?;
287 ///
288 /// println!("{response:#?}");
289 ///
290 /// let file_config = FileConfig::new(DOWNLOAD_DIRECTORY)
291 /// .file_name(OsStr::new(FILE_NAME));
292 ///
293 /// let response2 = response.download(file_config)
294 /// .send()
295 /// .await?;
296 ///
297 /// let path_buf = response2.body();
298 /// println!("{:#?}", path_buf.metadata());
299 ///
300 /// Ok(())
301 /// }
302 /// ```
303 async fn download(
304 self,
305 file_config: &FileConfig,
306 ) -> Result<http::Response<PathBuf>, AsyncDownloadError>;
307
308 /// If the response is a server error then Microsoft Graph will return
309 /// an error in the response body. The [`ErrorMessage`] type maps to these
310 /// errors and this method deserializes to this type.
311 ///
312 /// Microsoft Graph does not return this error message in all situations so it
313 /// make sure to handle cases where the body could not be deserialized properly.
314 /// ```rust,ignore
315 /// let status = response.status();
316 ///
317 /// if status.is_server_error() || status.is_client_error() {
318 /// let error_message = response.into_error_message().await.unwrap();
319 /// println!("{error_message:#?}");
320 ///
321 /// // This is the same thing as doing
322 /// let error_message: ErrorMessage = response.json().await.unwrap();
323 /// }
324 /// ```
325 async fn into_graph_error_message(self) -> Result<ErrorMessage, reqwest::Error>;
326
327 /// Microsoft Graph specific status code errors mapped from the response [StatusCode].
328 /// Not all status codes map to a Microsoft Graph error.
329 ///
330 /// Use [`ErrorType::as_str`] to get a short description of the Microsoft Graph specific error.
331 /// ```rust,ignore
332 /// let error_type = response.graph_error_type().unwrap();
333 /// println!("{:#?}", error_type.as_str());
334 /// ```
335 fn graph_error_type(&self) -> Option<ErrorType>;
336}
337
338#[async_trait]
339impl ResponseExt for reqwest::Response {
340 async fn job_status(&self) -> Option<GraphResult<Response>> {
341 let url = self
342 .headers()
343 .get(reqwest::header::LOCATION)?
344 .to_str()
345 .ok()?;
346 let result = reqwest::Client::new()
347 .get(url)
348 .send()
349 .await
350 .map_err(GraphFailure::from);
351
352 Some(result)
353 }
354
355 /// # Begin an upload session using any [`std::io::Reader`].<br>
356 ///
357 /// Converts the current request object into an upload session object for uploading large
358 /// files to OneDrive or SharePoint.<br>
359 ///
360 /// This method takes a `reader` object that implements the [std::io::Read] and [Send] traits,
361 /// and returns a [GraphResult] containing an [UploadSession] object.<br>
362 ///
363 /// The [UploadSession] object contains the upload URL for the file, as well as a [RangeIter] iterator
364 /// that can be used to send the file contents to the server in multiple chunks (or "ranges").
365 /// If the upload URL is not found in the response body, this method returns a `GraphFailure`
366 /// with an error message indicating that no upload URL was found.<br>
367 ///
368 ///
369 /// ## Requires reqwest::Response body to be valid JSON
370 /// The body of the reqwest::Response must be valid JSON with an
371 /// [uploadUrl] field.
372 ///
373 /// # Example
374 /// ```rust,ignore
375 /// use graph_rs_sdk::http::{AsyncIterator, ResponseExt};
376 /// use graph_rs_sdk::*;
377 ///
378 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
379 ///
380 /// // Put the path to your file and the file name itself that
381 /// // you want to upload to one drive.
382 /// static LOCAL_FILE_PATH: &str = "/path/to/file/file.txt";
383 ///
384 /// // Parent folder id of where to store this file.
385 /// static DRIVE_PARENT_ID: &str = "PARENT_ID";
386 ///
387 /// // The conflict behavior can be one of: fail, rename, or replace.
388 /// static CONFLICT_BEHAVIOR: &str = "rename";
389 /// #[tokio::main]
390 /// async fn main() -> GraphResult<()> {
391 /// let client = Graph::new(ACCESS_TOKEN);
392 ///
393 /// let conflict_behavior = CONFLICT_BEHAVIOR.to_string();
394 /// let upload = serde_json::json!({
395 /// "@microsoft.graph.conflictBehavior": Some(conflict_behavior)
396 /// });
397 ///
398 /// let response = client
399 /// .me()
400 /// .drive()
401 /// .item_by_path(PATH_IN_ONE_DRIVE)
402 /// .create_upload_session(&upload)
403 /// .send()
404 /// .await
405 /// .unwrap();
406 ///
407 /// let file = std::fs::File::open(PATH_IN_ONE_DRIVE)?;
408 ///
409 /// let mut iter = response.into_upload_session(file).await?;
410 ///
411 /// while let Some(result) = iter.next().await {
412 /// let response = result?;
413 /// println!("{response:#?}");
414 /// }
415 ///
416 /// Ok(())
417 /// }
418 /// ```
419 async fn into_upload_session(
420 self,
421 reader: impl std::io::Read + Send,
422 ) -> GraphResult<UploadSession> {
423 let body: serde_json::Value = self.json().await?;
424 let url = body
425 .upload_session_link()
426 .ok_or_else(|| GraphFailure::not_found("No uploadUrl found in response body"))?;
427
428 let range_iter = RangeIter::from_reader(reader)?;
429 Ok(UploadSession::new(
430 reqwest::Url::parse(url.as_str())?,
431 range_iter,
432 ))
433 }
434
435 /// # Begin an upload session using any [tokio::io::AsyncReadExt].<br>
436 ///
437 /// Converts the current request object into an upload session object for uploading large
438 /// files to OneDrive or SharePoint.<br>
439 ///
440 /// This method takes a `reader` object that implements the [tokio::io::AsyncReadExt], [Send], and [Unpin] traits,
441 /// and returns a [GraphResult] containing an [UploadSession] object.<br>
442 ///
443 /// The [UploadSession] object contains the upload URL for the file, as well as a [RangeIter] iterator
444 /// that can be used to send the file contents to the server in multiple chunks (or "ranges").
445 /// If the upload URL is not found in the response body, this method returns a `GraphFailure`
446 /// with an error message indicating that no upload URL was found.<br>
447 ///
448 ///
449 /// ## Requires reqwest::Response body can be deserialized to valid JSON
450 /// The body of the reqwest::Response must be valid JSON with an
451 /// [uploadUrl] field.
452 ///
453 /// # Example
454 /// ```rust,ignore
455 /// use graph_rs_sdk::http::{AsyncIterator, ResponseExt};
456 /// use graph_rs_sdk::*;
457 ///
458 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
459 ///
460 /// // Put the path to your file and the file name itself that
461 /// // you want to upload to one drive.
462 /// static LOCAL_FILE_PATH: &str = "/path/to/file/file.txt";
463 ///
464 /// // Parent folder id of where to store this file.
465 /// static DRIVE_PARENT_ID: &str = "PARENT_ID";
466 ///
467 /// // The conflict behavior can be one of: fail, rename, or replace.
468 /// static CONFLICT_BEHAVIOR: &str = "rename";
469 ///
470 /// #[tokio::main]
471 /// async fn main() -> GraphResult<()> {
472 /// let client = Graph::new(ACCESS_TOKEN);
473 ///
474 /// let conflict_behavior = CONFLICT_BEHAVIOR.to_string();
475 /// let upload = serde_json::json!({
476 /// "@microsoft.graph.conflictBehavior": Some(conflict_behavior)
477 /// });
478 ///
479 /// let response = client
480 /// .me()
481 /// .drive()
482 /// .item_by_path(PATH_IN_ONE_DRIVE)
483 /// .create_upload_session(&upload)
484 /// .send()
485 /// .await
486 /// .unwrap();
487 ///
488 /// let file = tokio::fs::File::open(PATH_IN_ONE_DRIVE).await?;
489 ///
490 /// let mut iter = response.into_upload_session_async_read(file).await?;
491 ///
492 /// while let Some(result) = iter.next().await {
493 /// let response = result?;
494 /// println!("{response:#?}");
495 /// }
496 ///
497 /// Ok(())
498 /// }
499 /// ```
500 async fn into_upload_session_async_read(
501 self,
502 reader: impl AsyncReadExt + Send + Unpin,
503 ) -> GraphResult<UploadSession> {
504 let body: serde_json::Value = self.json().await?;
505 let url = body
506 .upload_session_link()
507 .ok_or_else(|| GraphFailure::not_found("No uploadUrl found in response body"))?;
508
509 let range_iter = RangeIter::from_async_read(reader).await?;
510 Ok(UploadSession::new(
511 reqwest::Url::parse(url.as_str())?,
512 range_iter,
513 ))
514 }
515
516 /// # Downloads the content of the HTTP response and saves it to a file.<br>
517 ///
518 /// This method takes a `file_config` object containing various parameters that control how the
519 /// file is downloaded and saved. The `file_config` object includes the file path, file name,
520 /// whether to create the directory recursively, whether to overwrite existing files, and the
521 /// desired file extension.<br><br>
522 ///
523 /// If `create_dir_all` is set to true, this method will create the directory at the specified
524 /// path if it doesn't exist yet. If it is set to false and the target directory doesn't exist,
525 /// this method will return an `AsyncDownloadError` with an error message indicating that the
526 /// target directory does not exist.<br><br>
527 ///
528 /// The [`FileConfig::file_name`] parameter can be used to specify a custom file name for the downloaded file.
529 /// If it is not provided, the method will attempt to parse the `Content-Disposition` header to extract the file name.
530 /// If no file name can be obtained from the header, this method will return an [AsyncDownloadError::NoFileName]
531 /// with an error message indicating that no file name was found.<br><br>
532 ///
533 /// If the [`FileConfig::extension`] parameter is set to a non-empty string,
534 /// this method will set the file extension of the downloaded file to the specified value. <br><br>
535 ///
536 /// If the target file already exists and [`overwrite_existing_file`] is set to false,
537 /// this method will return an [AsyncDownloadError::FileExists] with an error message
538 /// indicating that the file already exists and cannot be overwritten. <br><br>
539 ///
540 /// If the file is downloaded and saved successfully, this method returns a [`http::Response<PathBuf>`] object
541 /// containing the path to the downloaded file.
542 ///
543 ///
544 /// # Example
545 ///
546 /// ```rust,ignore
547 /// use graph_rs_sdk::http::{BodyRead, FileConfig};
548 /// use graph_rs_sdk::*;
549 ///
550 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
551 ///
552 /// static ITEM_ID: &str = "ITEM_ID";
553 ///
554 /// static FORMAT: &str = "pdf";
555 ///
556 /// static DOWNLOAD_DIRECTORY: &str = "./examples";
557 ///
558 /// #[tokio::main]
559 /// async fn main() -> GraphResult<()> {
560 /// let client = Graph::new(ACCESS_TOKEN);
561 ///
562 /// let response = client
563 /// .me()
564 /// .drive()
565 /// .item(ITEM_ID)
566 /// .get_items_content()
567 /// .format(FORMAT)
568 /// .send()
569 /// .await?;
570 ///
571 /// println!("{response:#?}");
572 ///
573 /// let response2 = response.download(&FileConfig::new(DOWNLOAD_DIRECTORY))
574 /// .send()
575 /// .await?;
576 ///
577 /// let path_buf = response2.body();
578 /// println!("{:#?}", path_buf.metadata());
579 ///
580 /// Ok(())
581 /// }
582 /// ```
583 /// <br><br>
584 /// # Example format and rename
585 ///
586 /// ```rust,ignore
587 /// use graph_rs_sdk::http::{BodyRead, FileConfig};
588 /// use graph_rs_sdk::*;
589 /// use std::ffi::OsStr;
590 ///
591 /// static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
592 ///
593 /// static ITEM_ID: &str = "ITEM_ID";
594 ///
595 /// static FORMAT: &str = "pdf";
596 ///
597 /// static DOWNLOAD_DIRECTORY: &str = "./examples";
598 ///
599 /// static FILE_NAME: &str = "new_file_name.pdf";
600 ///
601 /// #[tokio::main]
602 /// async fn main() -> GraphResult<()> {
603 /// let client = Graph::new(ACCESS_TOKEN);
604 ///
605 /// let response = client
606 /// .me()
607 /// .drive()
608 /// .item(ITEM_ID)
609 /// .get_items_content()
610 /// .format(FORMAT)
611 /// .send()
612 /// .await?;
613 ///
614 /// println!("{response:#?}");
615 ///
616 /// let file_config = FileConfig::new(DOWNLOAD_DIRECTORY)
617 /// .file_name(OsStr::new(FILE_NAME));
618 ///
619 /// let response2 = response.download(file_config)
620 /// .send()
621 /// .await?;
622 ///
623 /// let path_buf = response2.body();
624 /// println!("{:#?}", path_buf.metadata());
625 ///
626 /// Ok(())
627 /// }
628 /// ```
629 async fn download(
630 self,
631 file_config: &FileConfig,
632 ) -> Result<http::Response<PathBuf>, AsyncDownloadError> {
633 let path = file_config.path.clone();
634 let file_name = file_config.file_name.clone();
635 let create_dir_all = file_config.create_directory_all;
636 let overwrite_existing_file = file_config.overwrite_existing_file;
637 let extension = file_config.extension.clone();
638
639 if create_dir_all {
640 create_dir_async(path.as_path()).await?;
641 } else if !path.exists() {
642 return Err(AsyncDownloadError::TargetDoesNotExist(
643 path.to_string_lossy().to_string(),
644 ));
645 }
646
647 let path = {
648 if let Some(name) = file_name.or_else(|| parse_content_disposition(self.headers())) {
649 if name.len() > MAX_FILE_NAME_LEN {
650 return Err(AsyncDownloadError::FileNameTooLong);
651 }
652 path.join(name)
653 } else {
654 return Err(AsyncDownloadError::NoFileName);
655 }
656 };
657
658 if let Some(ext) = extension.as_ref() {
659 path.with_extension(ext.as_os_str());
660 }
661
662 if path.exists() && !overwrite_existing_file {
663 return Err(AsyncDownloadError::FileExists(
664 path.to_string_lossy().to_string(),
665 ));
666 }
667
668 let status = self.status();
669 let url = self.url().clone();
670 let _headers = self.headers().clone();
671 let version = self.version();
672
673 Ok(http::Response::builder()
674 .url(url)
675 .status(http::StatusCode::from(&status))
676 .version(version)
677 .body(copy_async(path, self).await?)?)
678 }
679
680 /// If the response is a server error then Microsoft Graph will return
681 /// an error in the response body. The [`ErrorMessage`] type maps to these
682 /// errors and this method deserializes to this type.
683 ///
684 /// Microsoft Graph does not return this error message in all situations so it
685 /// make sure to handle cases where the body could not be deserialized properly.
686 /// ```rust,ignore
687 /// let status = response.status();
688 ///
689 /// if status.is_server_error() || status.is_client_error() {
690 /// let error_message = response.into_error_message().await.unwrap();
691 /// println!("{error_message:#?}");
692 ///
693 /// // This is the same thing as doing
694 /// let error_message: ErrorMessage = response.json().await.unwrap();
695 /// }
696 /// ```
697 async fn into_graph_error_message(self) -> Result<ErrorMessage, reqwest::Error> {
698 self.json().await
699 }
700
701 /// Microsoft Graph specific status code errors mapped from the response [StatusCode].
702 /// Not all status codes map to a Microsoft Graph error.
703 ///
704 /// Use [`ErrorType::as_str`] to get a short description of the Microsoft Graph specific error.
705 /// ```rust,ignore
706 /// let error_type = response.graph_error_type().unwrap();
707 /// println!("{:#?}", error_type.as_str());
708 /// ```
709 fn graph_error_type(&self) -> Option<ErrorType> {
710 let status = self.status();
711 ErrorType::from_u16(status.as_u16())
712 }
713}