onedrive_api/
onedrive.rs

1#![allow(clippy::default_trait_access)] // Forwarding default options is allowed.
2use crate::{
3    error::{Error, Result},
4    option::{CollectionOption, DriveItemPutOption, ObjectOption},
5    resource::{Drive, DriveField, DriveItem, DriveItemField, TimestampString},
6    util::{
7        handle_error_response, ApiPathComponent, DriveLocation, FileName, ItemLocation,
8        RequestBuilderExt as _, ResponseExt as _,
9    },
10    {ConflictBehavior, ExpectRange},
11};
12use bytes::Bytes;
13use reqwest::{header, Client};
14use serde::{Deserialize, Serialize};
15use serde_json::json;
16use std::fmt;
17use url::Url;
18
19macro_rules! api_url {
20    ($($seg:expr),* $(,)?) => {{
21        let mut url = Url::parse("https://graph.microsoft.com/v1.0").unwrap();
22        {
23            let mut buf = url.path_segments_mut().unwrap();
24            $(ApiPathComponent::extend_into($seg, &mut buf);)*
25        } // End borrowing of `url`
26        url
27    }};
28}
29
30/// TODO: More efficient impl.
31macro_rules! api_path {
32    ($item:expr) => {{
33        let mut url = Url::parse("path:///drive").unwrap();
34        let item: &ItemLocation = $item;
35        ApiPathComponent::extend_into(item, &mut url.path_segments_mut().unwrap());
36        url
37    }
38    .path()};
39}
40
41/// The authorized client to access OneDrive resources in a specified Drive.
42#[derive(Clone)]
43pub struct OneDrive {
44    client: Client,
45    token: String,
46    drive: DriveLocation,
47}
48
49impl fmt::Debug for OneDrive {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.debug_struct("OneDrive")
52            .field("client", &self.client)
53            // Skip `token`.
54            .field("drive", &self.drive)
55            .finish_non_exhaustive()
56    }
57}
58
59impl OneDrive {
60    /// Create a new OneDrive instance with access token given to perform operations in a Drive.
61    ///
62    /// # Panics
63    /// It panics if the underlying `reqwest::Client` cannot be created.
64    pub fn new(access_token: impl Into<String>, drive: impl Into<DriveLocation>) -> Self {
65        let client = Client::builder()
66            .redirect(reqwest::redirect::Policy::none())
67            .gzip(true)
68            .build()
69            .unwrap();
70        Self::new_with_client(client, access_token, drive.into())
71    }
72
73    /// Same as [`OneDrive::new`] but with custom `reqwest::Client`.
74    ///
75    /// # Note
76    /// The given `client` should have redirection disabled to
77    /// make [`get_item_download_url[_with_option]`][get_url] work properly.
78    /// See also the docs of [`get_item_download_url[_with_option]`][get_url].
79    ///
80    /// [`OneDrive::new`]: #method.new
81    /// [get_url]: #method.get_item_download_url_with_option
82    pub fn new_with_client(
83        client: Client,
84        access_token: impl Into<String>,
85        drive: impl Into<DriveLocation>,
86    ) -> Self {
87        OneDrive {
88            client,
89            token: access_token.into(),
90            drive: drive.into(),
91        }
92    }
93
94    /// Get the `reqwest::Client` used to create the OneDrive instance.
95    #[must_use]
96    pub fn client(&self) -> &Client {
97        &self.client
98    }
99
100    /// Get the access token used to create the OneDrive instance.
101    #[must_use]
102    pub fn access_token(&self) -> &str {
103        &self.token
104    }
105
106    /// Get current `Drive`.
107    ///
108    /// Retrieve the properties and relationships of a [`resource::Drive`][drive] resource.
109    ///
110    /// # See also
111    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/drive-get?view=graph-rest-1.0)
112    ///
113    /// [drive]: ./resource/struct.Drive.html
114    pub async fn get_drive_with_option(&self, option: ObjectOption<DriveField>) -> Result<Drive> {
115        self.client
116            .get(api_url![&self.drive])
117            .apply(option)
118            .bearer_auth(&self.token)
119            .send()
120            .await?
121            .parse()
122            .await
123    }
124
125    /// Shortcut to `get_drive_with_option` with default parameters.
126    ///
127    /// # See also
128    /// [`get_drive_with_option`][with_opt]
129    ///
130    /// [with_opt]: #method.get_drive_with_option
131    pub async fn get_drive(&self) -> Result<Drive> {
132        self.get_drive_with_option(Default::default()).await
133    }
134
135    /// List children of a `DriveItem`.
136    ///
137    /// Retrieve a collection of [`resource::DriveItem`][drive_item]s in the children relationship
138    /// of the given one.
139    ///
140    /// # Response
141    /// If successful, respond a fetcher for fetching changes from initial state (empty) to the snapshot of
142    /// current states. See [`ListChildrenFetcher`][fetcher] for more details.
143    ///
144    /// If [`if_none_match`][if_none_match] is set and it matches the item tag, return an `None`.
145    ///
146    /// # See also
147    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-list-children?view=graph-rest-1.0)
148    ///
149    /// [drive_item]: ./resource/struct.DriveItem.html
150    /// [if_none_match]: ./option/struct.CollectionOption.html#method.if_none_match
151    /// [fetcher]: ./struct.ListChildrenFetcher.html
152    pub async fn list_children_with_option<'a>(
153        &self,
154        item: impl Into<ItemLocation<'a>>,
155        option: CollectionOption<DriveItemField>,
156    ) -> Result<Option<ListChildrenFetcher>> {
157        let opt_resp = self
158            .client
159            .get(api_url![&self.drive, &item.into(), "children"])
160            .apply(option)
161            .bearer_auth(&self.token)
162            .send()
163            .await?
164            .parse_optional()
165            .await?;
166
167        Ok(opt_resp.map(ListChildrenFetcher::new))
168    }
169
170    /// Shortcut to `list_children_with_option` with default params,
171    /// and fetch and collect all children.
172    ///
173    /// # See also
174    /// [`list_children_with_option`][with_opt]
175    ///
176    /// [with_opt]: #method.list_children_with_option
177    pub async fn list_children<'a>(
178        &self,
179        item: impl Into<ItemLocation<'a>>,
180    ) -> Result<Vec<DriveItem>> {
181        self.list_children_with_option(item, Default::default())
182            .await?
183            .ok_or_else(|| Error::unexpected_response("Unexpected empty response"))?
184            .fetch_all(self)
185            .await
186    }
187
188    /// Get a `DriveItem` resource.
189    ///
190    /// Retrieve the metadata for a [`resource::DriveItem`][drive_item] by file system path or ID.
191    ///
192    /// # Errors
193    /// Will return `Ok(None)` if [`if_none_match`][if_none_match] is set and it matches the item tag.
194    ///
195    /// # See also
196    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-get?view=graph-rest-1.0)
197    ///
198    /// [drive_item]: ./resource/struct.DriveItem.html
199    /// [if_none_match]: ./option/struct.CollectionOption.html#method.if_none_match
200    pub async fn get_item_with_option<'a>(
201        &self,
202        item: impl Into<ItemLocation<'a>>,
203        option: ObjectOption<DriveItemField>,
204    ) -> Result<Option<DriveItem>> {
205        self.client
206            .get(api_url![&self.drive, &item.into()])
207            .apply(option)
208            .bearer_auth(&self.token)
209            .send()
210            .await?
211            .parse_optional()
212            .await
213    }
214
215    /// Shortcut to `get_item_with_option` with default parameters.
216    ///
217    /// # See also
218    /// [`get_item_with_option`][with_opt]
219    ///
220    /// [with_opt]: #method.get_item_with_option
221    pub async fn get_item<'a>(&self, item: impl Into<ItemLocation<'a>>) -> Result<DriveItem> {
222        self.get_item_with_option(item, Default::default())
223            .await?
224            .ok_or_else(|| Error::unexpected_response("Unexpected empty response"))
225    }
226
227    /// Get a pre-authorized download URL for a file.
228    ///
229    /// The URL returned is only valid for a short period of time (a few minutes).
230    ///
231    /// # Note
232    /// This API only works with reqwest redirection disabled, which is the default option set by
233    /// [`OneDrive::new()`][new].
234    /// If the `OneDrive` instance is created by [`new_with_client()`][new_with_client],
235    /// be sure the `reqwest::Client` has redirection disabled.
236    ///
237    /// Only `If-None-Match` is supported in `option`.
238    ///
239    /// # See also
240    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-get-content?view=graph-rest-1.0&tabs=http)
241    ///
242    /// [new]: #method.new
243    /// [new_with_client]: #method.new_with_client
244    pub async fn get_item_download_url_with_option<'a>(
245        &self,
246        item: impl Into<ItemLocation<'a>>,
247        option: ObjectOption<DriveItemField>,
248    ) -> Result<String> {
249        let raw_resp = self
250            .client
251            .get(api_url![&self.drive, &item.into(), "content"])
252            .apply(option)
253            .bearer_auth(&self.token)
254            .send()
255            .await?;
256        let url = handle_error_response(raw_resp)
257            .await?
258            .headers()
259            .get(header::LOCATION)
260            .ok_or_else(|| {
261                Error::unexpected_response(
262                    "Header `Location` not exists in response of `get_item_download_url`",
263                )
264            })?
265            .to_str()
266            .map_err(|_| Error::unexpected_response("Invalid string header `Location`"))?
267            .to_owned();
268        Ok(url)
269    }
270
271    /// Shortcut to [`get_item_download_url_with_option`] with default options.
272    ///
273    /// # See also
274    /// [`get_item_download_url_with_option`]
275    ///
276    /// [`get_item_download_url_with_option`]: #method.get_item_downloda_url_with_option
277    pub async fn get_item_download_url<'a>(
278        &self,
279        item: impl Into<ItemLocation<'a>>,
280    ) -> Result<String> {
281        self.get_item_download_url_with_option(item.into(), Default::default())
282            .await
283    }
284
285    /// Create a new [`DriveItem`][drive_item] allowing to set supported attributes.
286    /// [`DriveItem`][drive_item] resources have facets modeled as properties that provide data
287    /// about the [`DriveItem`][drive_item]'s identities and capabilities. You must provide one
288    /// of the following facets to create an item: `bundle`, `file`, `folder`, `remote_item`.
289    ///
290    /// # Errors
291    /// * Will result in `Err` with HTTP `409 CONFLICT` if [`conflict_behavior`][conflict_behavior]
292    ///   is set to [`Fail`][conflict_fail] and the target already exists.
293    /// * Will result in `Err` with HTTP `400 BAD REQUEST` if facets are not properly set.
294    ///
295    /// # See also
296    ///
297    /// [Microsoft Docs](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children?view=graph-rest-1.0)
298    ///
299    /// [with_opt]: #method.create_folder_with_option
300    /// [drive_item]: ./resource/struct.DriveItem.html
301    /// [conflict_behavior]: ./option/struct.DriveItemPutOption.html#method.conflict_behavior
302    /// [conflict_fail]: ./enum.ConflictBehavior.html#variant.Fail
303    pub async fn create_drive_item<'a>(
304        &self,
305        parent_item: impl Into<ItemLocation<'a>>,
306        drive_item: DriveItem,
307        option: DriveItemPutOption,
308    ) -> Result<DriveItem> {
309        #[derive(Serialize)]
310        struct Req {
311            #[serde(rename = "@microsoft.graph.conflictBehavior")]
312            conflict_behavior: ConflictBehavior,
313            #[serde(flatten)]
314            drive_item: DriveItem,
315        }
316
317        let conflict_behavior = option
318            .get_conflict_behavior()
319            .unwrap_or(ConflictBehavior::Fail);
320
321        self.client
322            .post(api_url![&self.drive, &parent_item.into(), "children"])
323            .bearer_auth(&self.token)
324            .apply(option)
325            .json(&Req {
326                conflict_behavior,
327                drive_item,
328            })
329            .send()
330            .await?
331            .parse()
332            .await
333    }
334
335    /// Create a new folder under an `DriveItem`
336    ///
337    /// Create a new folder [`DriveItem`][drive_item] with a specified parent item or path.
338    ///
339    /// # Errors
340    /// Will result in `Err` with HTTP `409 CONFLICT` if [`conflict_behavior`][conflict_behavior]
341    /// is set to [`Fail`][conflict_fail] and the target already exists.
342    ///
343    /// # See also
344    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-post-children?view=graph-rest-1.0)
345    ///
346    /// [drive_item]: ./resource/struct.DriveItem.html
347    /// [conflict_behavior]: ./option/struct.DriveItemPutOption.html#method.conflict_behavior
348    /// [conflict_fail]: ./enum.ConflictBehavior.html#variant.Fail
349    pub async fn create_folder_with_option<'a>(
350        &self,
351        parent_item: impl Into<ItemLocation<'a>>,
352        name: &FileName,
353        option: DriveItemPutOption,
354    ) -> Result<DriveItem> {
355        let drive_item = DriveItem {
356            name: Some(name.as_str().to_string()),
357            folder: Some(json!({}).into()),
358            ..Default::default()
359        };
360
361        self.create_drive_item(parent_item, drive_item, option)
362            .await
363    }
364
365    /// Shortcut to `create_folder_with_option` with default options.
366    ///
367    /// # See also
368    /// [`create_folder_with_option`][with_opt]
369    ///
370    /// [with_opt]: #method.create_folder_with_option
371    pub async fn create_folder<'a>(
372        &self,
373        parent_item: impl Into<ItemLocation<'a>>,
374        name: &FileName,
375    ) -> Result<DriveItem> {
376        self.create_folder_with_option(parent_item, name, Default::default())
377            .await
378    }
379
380    /// Update `DriveItem` properties
381    ///
382    /// Update the metadata for a [`DriveItem`][drive_item].
383    ///
384    /// If you want to rename or move an [`DriveItem`][drive_item] to another place,
385    /// you should use [`move_`][move_] (or [`move_with_option`][move_with_opt]) instead of this, which is a wrapper
386    /// to this API endpoint to make things easier.
387    ///
388    /// # See also
389    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-update?view=graph-rest-1.0)
390    ///
391    /// [drive_item]: ./resource/struct.DriveItem.html
392    /// [move_]: #method.move_
393    /// [move_with_opt]: #method.move_with_option
394    pub async fn update_item_with_option<'a>(
395        &self,
396        item: impl Into<ItemLocation<'a>>,
397        patch: &DriveItem,
398        option: ObjectOption<DriveItemField>,
399    ) -> Result<DriveItem> {
400        self.client
401            .patch(api_url![&self.drive, &item.into()])
402            .bearer_auth(&self.token)
403            .apply(option)
404            .json(patch)
405            .send()
406            .await?
407            .parse()
408            .await
409    }
410
411    /// Shortcut to `update_item_with_option` with default options.
412    ///
413    /// # See also
414    /// [`update_item_with_option`][with_opt]
415    ///
416    /// [with_opt]: #method.update_item_with_option
417    pub async fn update_item<'a>(
418        &self,
419        item: impl Into<ItemLocation<'a>>,
420        patch: &DriveItem,
421    ) -> Result<DriveItem> {
422        self.update_item_with_option(item, patch, Default::default())
423            .await
424    }
425
426    /// The upload size limit of [`upload_small`].
427    ///
428    /// The value is from
429    /// [OneDrive Developer documentation](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content?view=odsp-graph-online)
430    /// (4MB) which is smaller than
431    /// [Microsoft Graph documentation](https://docs.microsoft.com/en-us/graph/api/driveitem-put-content?view=graph-rest-1.0)
432    /// (250MB).
433    /// The exact limit is unknown. Here we chose the smaller one as a reference.
434    ///
435    /// [`upload_small`]: #method.upload_small
436    pub const UPLOAD_SMALL_MAX_SIZE: usize = 4_000_000; // 4 MB
437
438    /// Upload or replace the contents of a `DriveItem` file.
439    ///
440    /// The simple upload API allows you to provide the contents of a new file or
441    /// update the contents of an existing file in a single API call. This method
442    /// only supports files up to [`Self::UPLOAD_SMALL_MAX_SIZE`]. The length is not checked
443    /// locally and request will still be sent for large data.
444    ///
445    /// # See also
446    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-put-content?view=graph-rest-1.0)
447    ///
448    /// [drive_item]: ./resource/struct.DriveItem.html
449    pub async fn upload_small<'a>(
450        &self,
451        item: impl Into<ItemLocation<'a>>,
452        data: impl Into<Bytes>,
453    ) -> Result<DriveItem> {
454        let data = data.into();
455        self.client
456            .put(api_url![&self.drive, &item.into(), "content"])
457            .bearer_auth(&self.token)
458            .header(header::CONTENT_TYPE, "application/octet-stream")
459            .header(header::CONTENT_LENGTH, data.len().to_string())
460            .body(data)
461            .send()
462            .await?
463            .parse()
464            .await
465    }
466
467    /// Create an upload session.
468    ///
469    /// Create an upload session to allow your app to upload files up to
470    /// the maximum file size. An upload session allows your app to
471    /// upload ranges of the file in sequential API requests, which allows
472    /// the transfer to be resumed if a connection is dropped
473    /// while the upload is in progress.
474    ///
475    /// # Errors
476    /// Will return `Err` with HTTP `412 PRECONDITION_FAILED` if [`if_match`][if_match] is set
477    /// but does not match the item.
478    ///
479    /// # Note
480    /// [`conflict_behavior`][conflict_behavior] is supported.
481    ///
482    /// # See also
483    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#create-an-upload-session)
484    ///
485    /// [if_match]: ./option/struct.CollectionOption.html#method.if_match
486    /// [conflict_behavior]: ./option/struct.DriveItemPutOption.html#method.conflict_behavior
487    /// [upload_sess]: ./struct.UploadSession.html
488    /// [upload_part]: ./struct.UploadSession.html#method.upload_part
489    pub async fn new_upload_session_with_initial_option<'a>(
490        &self,
491        item: impl Into<ItemLocation<'a>>,
492        initial: &DriveItem,
493        option: DriveItemPutOption,
494    ) -> Result<(UploadSession, UploadSessionMeta)> {
495        #[derive(Serialize)]
496        struct Item<'a> {
497            #[serde(rename = "@microsoft.graph.conflictBehavior")]
498            conflict_behavior: ConflictBehavior,
499            #[serde(flatten)]
500            initial: &'a DriveItem,
501        }
502
503        #[derive(Serialize)]
504        struct Req<'a> {
505            item: Item<'a>,
506        }
507
508        #[derive(Deserialize)]
509        #[serde(rename_all = "camelCase")]
510        struct Resp {
511            upload_url: String,
512            #[serde(flatten)]
513            meta: UploadSessionMeta,
514        }
515
516        let conflict_behavior = option
517            .get_conflict_behavior()
518            .unwrap_or(ConflictBehavior::Fail);
519        let resp: Resp = self
520            .client
521            .post(api_url![&self.drive, &item.into(), "createUploadSession"])
522            .apply(option)
523            .bearer_auth(&self.token)
524            .json(&Req {
525                item: Item {
526                    conflict_behavior,
527                    initial,
528                },
529            })
530            .send()
531            .await?
532            .parse()
533            .await?;
534
535        Ok((
536            UploadSession {
537                upload_url: resp.upload_url,
538            },
539            resp.meta,
540        ))
541    }
542
543    /// Shortcut to [`new_upload_session_with_initial_option`] without initial attributes.
544    ///
545    /// [`new_upload_session_with_initial_option`]: #method.new_upload_session_with_initial_option
546    pub async fn new_upload_session_with_option<'a>(
547        &self,
548        item: impl Into<ItemLocation<'a>>,
549        option: DriveItemPutOption,
550    ) -> Result<(UploadSession, UploadSessionMeta)> {
551        let initial = DriveItem::default();
552        self.new_upload_session_with_initial_option(item, &initial, option)
553            .await
554    }
555
556    /// Shortcut to [`new_upload_session_with_option`] with `ConflictBehavior::Fail`.
557    ///
558    /// [`new_upload_session_with_option`]: #method.new_upload_session_with_option
559    pub async fn new_upload_session<'a>(
560        &self,
561        item: impl Into<ItemLocation<'a>>,
562    ) -> Result<(UploadSession, UploadSessionMeta)> {
563        self.new_upload_session_with_option(item, Default::default())
564            .await
565    }
566
567    /// Copy a `DriveItem`.
568    ///
569    /// Asynchronously creates a copy of an driveItem (including any children),
570    /// under a new parent item or with a new name.
571    ///
572    /// # Note
573    /// The conflict behavior is not mentioned in Microsoft Docs, and cannot be specified.
574    ///
575    /// But it seems to behave as [`Rename`][conflict_rename] if the destination folder is just the current
576    /// parent folder, and [`Fail`][conflict_fail] otherwise.
577    ///
578    /// # See also
579    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-copy?view=graph-rest-1.0)
580    ///
581    /// [conflict_rename]: ./enum.ConflictBehavior.html#variant.Rename
582    /// [conflict_fail]: ./enum.ConflictBehavior.html#variant.Fail
583    pub async fn copy<'a, 'b>(
584        &self,
585        source_item: impl Into<ItemLocation<'a>>,
586        dest_folder: impl Into<ItemLocation<'b>>,
587        dest_name: &FileName,
588    ) -> Result<CopyProgressMonitor> {
589        #[derive(Serialize)]
590        #[serde(rename_all = "camelCase")]
591        struct Req<'a> {
592            parent_reference: ItemReference<'a>,
593            name: &'a str,
594        }
595
596        let raw_resp = self
597            .client
598            .post(api_url![&self.drive, &source_item.into(), "copy"])
599            .bearer_auth(&self.token)
600            .json(&Req {
601                parent_reference: ItemReference {
602                    path: api_path!(&dest_folder.into()),
603                },
604                name: dest_name.as_str(),
605            })
606            .send()
607            .await?;
608
609        let url = handle_error_response(raw_resp)
610            .await?
611            .headers()
612            .get(header::LOCATION)
613            .ok_or_else(|| {
614                Error::unexpected_response("Header `Location` not exists in response of `copy`")
615            })?
616            .to_str()
617            .map_err(|_| Error::unexpected_response("Invalid string header `Location`"))?
618            .to_owned();
619
620        Ok(CopyProgressMonitor::from_monitor_url(url))
621    }
622
623    /// Move a `DriveItem` to a new folder.
624    ///
625    /// This is a special case of the Update method. Your app can combine
626    /// moving an item to a new container and updating other properties of
627    /// the item into a single request.
628    ///
629    /// Note: Items cannot be moved between Drives using this request.
630    ///
631    /// # Note
632    /// [`conflict_behavior`][conflict_behavior] is supported.
633    ///
634    /// # Errors
635    /// Will return `Err` with HTTP `412 PRECONDITION_FAILED` if [`if_match`][if_match] is set
636    /// but it does not match the item.
637    ///
638    /// # See also
639    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-move?view=graph-rest-1.0)
640    ///
641    /// [conflict_behavior]: ./option/struct.DriveItemPutOption.html#method.conflict_behavior
642    /// [if_match]: ./option/struct.CollectionOption.html#method.if_match
643    pub async fn move_with_option<'a, 'b>(
644        &self,
645        source_item: impl Into<ItemLocation<'a>>,
646        dest_folder: impl Into<ItemLocation<'b>>,
647        dest_name: Option<&FileName>,
648        option: DriveItemPutOption,
649    ) -> Result<DriveItem> {
650        #[derive(Serialize)]
651        #[serde(rename_all = "camelCase")]
652        struct Req<'a> {
653            parent_reference: ItemReference<'a>,
654            name: Option<&'a str>,
655            #[serde(rename = "@microsoft.graph.conflictBehavior")]
656            conflict_behavior: ConflictBehavior,
657        }
658
659        let conflict_behavior = option
660            .get_conflict_behavior()
661            .unwrap_or(ConflictBehavior::Fail);
662        self.client
663            .patch(api_url![&self.drive, &source_item.into()])
664            .bearer_auth(&self.token)
665            .apply(option)
666            .json(&Req {
667                parent_reference: ItemReference {
668                    path: api_path!(&dest_folder.into()),
669                },
670                name: dest_name.map(FileName::as_str),
671                conflict_behavior,
672            })
673            .send()
674            .await?
675            .parse()
676            .await
677    }
678
679    /// Shortcut to `move_with_option` with `ConflictBehavior::Fail`.
680    ///
681    /// # See also
682    /// [`move_with_option`][with_opt]
683    ///
684    /// [with_opt]: #method.move_with_option
685    pub async fn move_<'a, 'b>(
686        &self,
687        source_item: impl Into<ItemLocation<'a>>,
688        dest_folder: impl Into<ItemLocation<'b>>,
689        dest_name: Option<&FileName>,
690    ) -> Result<DriveItem> {
691        self.move_with_option(source_item, dest_folder, dest_name, Default::default())
692            .await
693    }
694
695    /// Delete a `DriveItem`.
696    ///
697    /// Delete a [`DriveItem`][drive_item] by using its ID or path. Note that deleting items using
698    /// this method will move the items to the recycle bin instead of permanently
699    /// deleting the item.
700    ///
701    /// # Error
702    /// Will result in error with HTTP `412 PRECONDITION_FAILED` if [`if_match`][if_match] is set but
703    /// does not match the item.
704    ///
705    /// # Panics
706    /// [`conflict_behavior`][conflict_behavior] is **NOT** supported. Set it will cause a panic.
707    ///
708    /// # See also
709    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-delete?view=graph-rest-1.0)
710    ///
711    /// [drive_item]: ./resource/struct.DriveItem.html
712    /// [if_match]: ./option/struct.CollectionOption.html#method.if_match
713    /// [conflict_behavior]: ./option/struct.DriveItemPutOption.html#method.conflict_behavior
714    pub async fn delete_with_option<'a>(
715        &self,
716        item: impl Into<ItemLocation<'a>>,
717        option: DriveItemPutOption,
718    ) -> Result<()> {
719        assert!(
720            option.get_conflict_behavior().is_none(),
721            "`conflict_behavior` is not supported by `delete[_with_option]`",
722        );
723
724        self.client
725            .delete(api_url![&self.drive, &item.into()])
726            .bearer_auth(&self.token)
727            .apply(option)
728            .send()
729            .await?
730            .parse_no_content()
731            .await
732    }
733
734    /// Shortcut to `delete_with_option`.
735    ///
736    /// # See also
737    /// [`delete_with_option`][with_opt]
738    ///
739    /// [with_opt]: #method.delete_with_option
740    pub async fn delete<'a>(&self, item: impl Into<ItemLocation<'a>>) -> Result<()> {
741        self.delete_with_option(item, Default::default()).await
742    }
743
744    /// Track changes for root folder from initial state (empty state) to snapshot of current states.
745    ///
746    /// This method allows your app to track changes to a drive and its children over time.
747    /// Deleted items are returned with the deleted facet. Items with this property set
748    /// should be removed from your local state.
749    ///
750    /// Note: you should only delete a folder locally if it is empty after
751    /// syncing all the changes.
752    ///
753    /// # Panics
754    /// Track Changes API does not support [`$count=true` query parameter][dollar_count].
755    /// If [`CollectionOption::get_count`][opt_get_count] is set in option, it will panic.
756    ///
757    /// # Results
758    /// Return a fetcher for fetching changes from initial state (empty) to the snapshot of
759    /// current states. See [`TrackChangeFetcher`][fetcher] for more details.
760    ///
761    /// # See also
762    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-delta?view=graph-rest-1.0)
763    ///
764    /// [fetcher]: ./struct.TrackChangeFetcher.html
765    /// [dollar_count]: https://docs.microsoft.com/en-us/graph/query-parameters#count-parameter
766    /// [opt_get_count]: ./option/struct.CollectionOption.html#method.get_count
767    pub async fn track_root_changes_from_initial_with_option(
768        &self,
769        option: CollectionOption<DriveItemField>,
770    ) -> Result<TrackChangeFetcher> {
771        assert!(
772            !option.has_get_count(),
773            "`get_count` is not supported by Track Changes API",
774        );
775        let resp = self
776            .client
777            .get(api_url![&self.drive, "root", "delta"])
778            .apply(option)
779            .bearer_auth(&self.token)
780            .send()
781            .await?
782            .parse()
783            .await?;
784        Ok(TrackChangeFetcher::new(resp))
785    }
786
787    /// Shortcut to `track_root_changes_from_initial_with_option` with default parameters.
788    ///
789    /// # See also
790    /// [`track_root_changes_from_initial_with_option`][with_opt]
791    ///
792    /// [with_opt]: #method.track_root_changes_from_initial_with_option
793    pub async fn track_root_changes_from_initial(&self) -> Result<TrackChangeFetcher> {
794        self.track_root_changes_from_initial_with_option(Default::default())
795            .await
796    }
797
798    /// Track changes for root folder from snapshot (delta url) to snapshot of current states.
799    ///
800    /// # Note
801    /// There is no `with_option` version of this function. Since delta URL already carries
802    /// query parameters when you get it. The initial parameters will be automatically used
803    /// in all following requests through delta URL.
804    pub async fn track_root_changes_from_delta_url(
805        &self,
806        delta_url: &str,
807    ) -> Result<TrackChangeFetcher> {
808        let resp: DriveItemCollectionResponse = self
809            .client
810            .get(delta_url)
811            .bearer_auth(&self.token)
812            .send()
813            .await?
814            .parse()
815            .await?;
816        Ok(TrackChangeFetcher::new(resp))
817    }
818
819    /// Get a delta url representing the snapshot of current states of root folder.
820    ///
821    /// The delta url can be used in [`track_root_changes_from_delta_url`][track_from_delta] later
822    /// to get diffs between two snapshots of states.
823    ///
824    /// Note that options (query parameters) are saved in delta url, so they are applied to all later
825    /// requests by `track_changes_from_delta_url` without need for specifying them every time.
826    ///
827    /// # Panics
828    /// Track Changes API does not support [`$count=true` query parameter][dollar_count].
829    /// If [`CollectionOption::get_count`][opt_get_count] is set in option, it will panic.
830    ///
831    /// # See also
832    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-delta?view=graph-rest-1.0#retrieving-the-current-deltalink)
833    ///
834    /// [track_from_delta]: #method.track_root_changes_from_delta_url
835    /// [dollar_count]: https://docs.microsoft.com/en-us/graph/query-parameters#count-parameter
836    /// [opt_get_count]: ./option/struct.CollectionOption.html#method.get_count
837    pub async fn get_root_latest_delta_url_with_option(
838        &self,
839        option: CollectionOption<DriveItemField>,
840    ) -> Result<String> {
841        assert!(
842            !option.has_get_count(),
843            "`get_count` is not supported by Track Changes API",
844        );
845        self.client
846            .get(api_url![&self.drive, "root", "delta"])
847            .query(&[("token", "latest")])
848            .apply(option)
849            .bearer_auth(&self.token)
850            .send()
851            .await?
852            .parse::<DriveItemCollectionResponse>()
853            .await?
854            .delta_url
855            .ok_or_else(|| {
856                Error::unexpected_response(
857                    "Missing field `@odata.deltaLink` for getting latest delta",
858                )
859            })
860    }
861
862    /// Shortcut to `get_root_latest_delta_url_with_option` with default parameters.
863    ///
864    /// # See also
865    /// [`get_root_latest_delta_url_with_option`][with_opt]
866    ///
867    /// [with_opt]: #method.get_root_latest_delta_url_with_option
868    pub async fn get_root_latest_delta_url(&self) -> Result<String> {
869        self.get_root_latest_delta_url_with_option(Default::default())
870            .await
871    }
872}
873
874/// The monitor for checking the progress of a asynchronous `copy` operation.
875///
876/// # Notes
877/// This struct is always present. But since retrieving copy progress requires beta API,
878/// it is useless due to the lack of method `fetch_progress` if feature `beta` is not enabled.
879///
880/// # See also
881/// [`OneDrive::copy`][copy]
882///
883/// [Microsoft docs](https://docs.microsoft.com/en-us/graph/long-running-actions-overview)
884///
885/// [copy]: ./struct.OneDrive.html#method.copy
886#[derive(Debug, Clone)]
887pub struct CopyProgressMonitor {
888    monitor_url: String,
889}
890
891/// The progress of a asynchronous `copy` operation. (Beta)
892///
893/// # See also
894/// [Microsoft Docs Beta](https://docs.microsoft.com/en-us/graph/api/resources/asyncjobstatus?view=graph-rest-beta)
895#[cfg(feature = "beta")]
896#[allow(missing_docs)]
897#[derive(Debug, Clone, Deserialize)]
898#[non_exhaustive]
899#[serde(rename_all = "camelCase")]
900pub struct CopyProgress {
901    pub percentage_complete: f64,
902    pub status: CopyStatus,
903}
904
905/// The status of a `copy` operation. (Beta)
906///
907/// # See also
908/// [`CopyProgress`][copy_progress]
909///
910/// [Microsoft Docs Beta](https://docs.microsoft.com/en-us/graph/api/resources/asyncjobstatus?view=graph-rest-beta#json-representation)
911///
912/// [copy_progress]: ./struct.CopyProgress.html
913#[cfg(feature = "beta")]
914#[allow(missing_docs)]
915#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
916#[serde(rename_all = "camelCase")]
917#[non_exhaustive]
918pub enum CopyStatus {
919    NotStarted,
920    InProgress,
921    Completed,
922    Updating,
923    Failed,
924    DeletePending,
925    DeleteFailed,
926    Waiting,
927}
928
929impl CopyProgressMonitor {
930    /// Make a progress monitor using existing `monitor_url`.
931    ///
932    /// `monitor_url` should be got from [`CopyProgressMonitor::monitor_url`][monitor_url]
933    ///
934    /// [monitor_url]: #method.monitor_url
935    pub fn from_monitor_url(monitor_url: impl Into<String>) -> Self {
936        Self {
937            monitor_url: monitor_url.into(),
938        }
939    }
940
941    /// Get the monitor url.
942    #[must_use]
943    pub fn monitor_url(&self) -> &str {
944        &self.monitor_url
945    }
946
947    /// Fetch the `copy` progress. (Beta)
948    ///
949    /// # See also
950    /// [`CopyProgress`][copy_progress]
951    ///
952    /// [copy_progress]: ./struct.CopyProgress.html
953    #[cfg(feature = "beta")]
954    pub async fn fetch_progress(&self, onedrive: &OneDrive) -> Result<CopyProgress> {
955        // No bearer auth.
956        onedrive
957            .client
958            .get(&self.monitor_url)
959            .send()
960            .await?
961            .parse()
962            .await
963    }
964}
965
966#[derive(Debug, Deserialize)]
967struct DriveItemCollectionResponse {
968    value: Option<Vec<DriveItem>>,
969    #[serde(rename = "@odata.nextLink")]
970    next_url: Option<String>,
971    #[serde(rename = "@odata.deltaLink")]
972    delta_url: Option<String>,
973}
974
975#[derive(Debug)]
976struct DriveItemFetcher {
977    last_response: DriveItemCollectionResponse,
978}
979
980impl DriveItemFetcher {
981    fn new(first_response: DriveItemCollectionResponse) -> Self {
982        Self {
983            last_response: first_response,
984        }
985    }
986
987    fn resume_from(next_url: impl Into<String>) -> Self {
988        Self::new(DriveItemCollectionResponse {
989            value: None,
990            next_url: Some(next_url.into()),
991            delta_url: None,
992        })
993    }
994
995    fn next_url(&self) -> Option<&str> {
996        // Return `None` for the first page, or it will
997        // lost items of the first page when resumed.
998        match &self.last_response {
999            DriveItemCollectionResponse {
1000                value: None,
1001                next_url: Some(next_url),
1002                ..
1003            } => Some(next_url),
1004            _ => None,
1005        }
1006    }
1007
1008    fn delta_url(&self) -> Option<&str> {
1009        self.last_response.delta_url.as_deref()
1010    }
1011
1012    async fn fetch_next_page(&mut self, onedrive: &OneDrive) -> Result<Option<Vec<DriveItem>>> {
1013        if let Some(items) = self.last_response.value.take() {
1014            return Ok(Some(items));
1015        }
1016        let url = match self.last_response.next_url.as_ref() {
1017            None => return Ok(None),
1018            Some(url) => url,
1019        };
1020        self.last_response = onedrive
1021            .client
1022            .get(url)
1023            .bearer_auth(&onedrive.token)
1024            .send()
1025            .await?
1026            .parse()
1027            .await?;
1028        Ok(Some(self.last_response.value.take().unwrap_or_default()))
1029    }
1030
1031    async fn fetch_all(mut self, onedrive: &OneDrive) -> Result<(Vec<DriveItem>, Option<String>)> {
1032        let mut buf = vec![];
1033        while let Some(items) = self.fetch_next_page(onedrive).await? {
1034            buf.extend(items);
1035        }
1036        Ok((buf, self.delta_url().map(Into::into)))
1037    }
1038}
1039
1040/// The page fetcher for listing children
1041///
1042/// # See also
1043/// [`OneDrive::list_children_with_option`][list_children_with_opt]
1044///
1045/// [list_children_with_opt]: ./struct.OneDrive.html#method.list_children_with_option
1046#[derive(Debug)]
1047pub struct ListChildrenFetcher {
1048    fetcher: DriveItemFetcher,
1049}
1050
1051impl ListChildrenFetcher {
1052    fn new(first_response: DriveItemCollectionResponse) -> Self {
1053        Self {
1054            fetcher: DriveItemFetcher::new(first_response),
1055        }
1056    }
1057
1058    /// Resume a fetching process from url from
1059    /// [`ListChildrenFetcher::next_url`][next_url].
1060    ///
1061    /// [next_url]: #method.next_url
1062    #[must_use]
1063    pub fn resume_from(next_url: impl Into<String>) -> Self {
1064        Self {
1065            fetcher: DriveItemFetcher::resume_from(next_url),
1066        }
1067    }
1068
1069    /// Try to get the url to the next page.
1070    ///
1071    /// Used for resuming the fetching progress.
1072    ///
1073    /// # Error
1074    /// Will success only if there are more pages and the first page is already read.
1075    ///
1076    /// # Note
1077    /// The first page data from [`OneDrive::list_children_with_option`][list_children_with_opt]
1078    /// will be cached and have no idempotent url to resume/re-fetch.
1079    ///
1080    /// [list_children_with_opt]: ./struct.OneDrive.html#method.list_children_with_option
1081    #[must_use]
1082    pub fn next_url(&self) -> Option<&str> {
1083        self.fetcher.next_url()
1084    }
1085
1086    /// Fetch the next page, or `None` if reaches the end.
1087    pub async fn fetch_next_page(&mut self, onedrive: &OneDrive) -> Result<Option<Vec<DriveItem>>> {
1088        self.fetcher.fetch_next_page(onedrive).await
1089    }
1090
1091    /// Fetch all rest pages and collect all items.
1092    ///
1093    /// # Errors
1094    ///
1095    /// Any error occurs when fetching will lead to an failure, and
1096    /// all progress will be lost.
1097    pub async fn fetch_all(self, onedrive: &OneDrive) -> Result<Vec<DriveItem>> {
1098        self.fetcher
1099            .fetch_all(onedrive)
1100            .await
1101            .map(|(items, _)| items)
1102    }
1103}
1104
1105/// The page fetcher for tracking operations with `Iterator` interface.
1106///
1107/// # See also
1108/// [`OneDrive::track_changes_from_initial`][track_initial]
1109///
1110/// [`OneDrive::track_changes_from_delta_url`][track_delta]
1111///
1112/// [track_initial]: ./struct.OneDrive.html#method.track_changes_from_initial_with_option
1113/// [track_delta]: ./struct.OneDrive.html#method.track_changes_from_delta_url
1114#[derive(Debug)]
1115pub struct TrackChangeFetcher {
1116    fetcher: DriveItemFetcher,
1117}
1118
1119impl TrackChangeFetcher {
1120    fn new(first_response: DriveItemCollectionResponse) -> Self {
1121        Self {
1122            fetcher: DriveItemFetcher::new(first_response),
1123        }
1124    }
1125
1126    /// Resume a fetching process from url.
1127    ///
1128    /// The url should be from [`TrackChangeFetcher::next_url`][next_url].
1129    ///
1130    /// [next_url]: #method.next_url
1131    #[must_use]
1132    pub fn resume_from(next_url: impl Into<String>) -> Self {
1133        Self {
1134            fetcher: DriveItemFetcher::resume_from(next_url),
1135        }
1136    }
1137
1138    /// Try to get the url to the next page.
1139    ///
1140    /// Used for resuming the fetching progress.
1141    ///
1142    /// # Error
1143    /// Will success only if there are more pages and the first page is already read.
1144    ///
1145    /// # Note
1146    /// The first page data from
1147    /// [`OneDrive::track_changes_from_initial_with_option`][track_initial]
1148    /// will be cached and have no idempotent url to resume/re-fetch.
1149    ///
1150    /// [track_initial]: ./struct.OneDrive.html#method.track_changes_from_initial
1151    #[must_use]
1152    pub fn next_url(&self) -> Option<&str> {
1153        self.fetcher.next_url()
1154    }
1155
1156    /// Try to the delta url representing a snapshot of current track change operation.
1157    ///
1158    /// Used for tracking changes from this snapshot (rather than initial) later,
1159    /// using [`OneDrive::track_changes_from_delta_url`][track_delta].
1160    ///
1161    /// # Error
1162    /// Will success only if there are no more pages.
1163    ///
1164    /// # See also
1165    /// [`OneDrive::track_changes_from_delta_url`][track_delta]
1166    ///
1167    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-delta?view=graph-rest-1.0#example-last-page-in-a-set)
1168    ///
1169    /// [track_delta]: ./struct.OneDrive.html#method.track_changes_from_delta_url
1170    #[must_use]
1171    pub fn delta_url(&self) -> Option<&str> {
1172        self.fetcher.delta_url()
1173    }
1174
1175    /// Fetch the next page, or `None` if reaches the end.
1176    pub async fn fetch_next_page(&mut self, onedrive: &OneDrive) -> Result<Option<Vec<DriveItem>>> {
1177        self.fetcher.fetch_next_page(onedrive).await
1178    }
1179
1180    /// Fetch all rest pages, collect all items, and also return `delta_url`.
1181    ///
1182    /// # Errors
1183    ///
1184    /// Any error occurs when fetching will lead to an failure, and
1185    /// all progress will be lost.
1186    pub async fn fetch_all(self, onedrive: &OneDrive) -> Result<(Vec<DriveItem>, String)> {
1187        let (items, opt_delta_url) = self.fetcher.fetch_all(onedrive).await?;
1188        let delta_url = opt_delta_url.ok_or_else(|| {
1189            Error::unexpected_response("Missing `@odata.deltaLink` for the last page")
1190        })?;
1191        Ok((items, delta_url))
1192    }
1193}
1194
1195#[derive(Serialize)]
1196struct ItemReference<'a> {
1197    path: &'a str,
1198}
1199
1200/// An upload session for resumable file uploading process.
1201///
1202/// # See also
1203/// [`OneDrive::new_upload_session`][get_session]
1204///
1205/// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/resources/uploadsession?view=graph-rest-1.0)
1206///
1207/// [get_session]: ./struct.OneDrive.html#method.new_upload_session
1208#[derive(Debug)]
1209pub struct UploadSession {
1210    upload_url: String,
1211}
1212
1213/// Metadata of an in-progress upload session
1214///
1215/// # See also
1216/// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#resuming-an-in-progress-upload)
1217#[derive(Debug, Deserialize)]
1218#[serde(rename_all = "camelCase")]
1219#[non_exhaustive]
1220pub struct UploadSessionMeta {
1221    /// Get a collection of byte ranges that the server is missing for the file.
1222    ///
1223    /// Used for determine what to upload when resuming a session.
1224    pub next_expected_ranges: Vec<ExpectRange>,
1225    /// Get the date and time in UTC that the upload session will expire.
1226    ///
1227    /// The complete file must be uploaded before this expiration time is reached.
1228    pub expiration_date_time: TimestampString,
1229}
1230
1231impl UploadSession {
1232    /// The upload size limit of a single [`upload_part`] call.
1233    ///
1234    /// The value is from
1235    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#upload-bytes-to-the-upload-session)
1236    /// and may not be accurate or stable.
1237    ///
1238    /// [`upload_part`]: #method.upload_part
1239    pub const MAX_PART_SIZE: usize = 60 << 20; // 60 MiB
1240
1241    /// Construct back the upload session from upload URL.
1242    pub fn from_upload_url(upload_url: impl Into<String>) -> Self {
1243        Self {
1244            upload_url: upload_url.into(),
1245        }
1246    }
1247
1248    /// Query the metadata of the upload to find out which byte ranges
1249    /// have been received previously.
1250    ///
1251    /// # See also
1252    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#resuming-an-in-progress-upload)
1253    pub async fn get_meta(&self, client: &Client) -> Result<UploadSessionMeta> {
1254        // No bearer auth.
1255        client
1256            .get(&self.upload_url)
1257            .send()
1258            .await?
1259            .parse::<UploadSessionMeta>()
1260            .await
1261    }
1262
1263    /// The URL endpoint accepting PUT requests.
1264    ///
1265    /// It is exactly what you passed in [`UploadSession::from_upload_url`].
1266    ///
1267    /// [`UploadSession::from_upload_url`]: #method.new
1268    #[must_use]
1269    pub fn upload_url(&self) -> &str {
1270        &self.upload_url
1271    }
1272
1273    /// Cancel the upload session
1274    ///
1275    /// This cleans up the temporary file holding the data previously uploaded.
1276    /// This should be used in scenarios where the upload is aborted, for example,
1277    /// if the user cancels the transfer.
1278    ///
1279    /// Temporary files and their accompanying upload session are automatically
1280    /// cleaned up after the `expirationDateTime` has passed. Temporary files may
1281    /// not be deleted immediately after the expiration time has elapsed.
1282    ///
1283    /// # See also
1284    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#cancel-the-upload-session)
1285    pub async fn delete(&self, client: &Client) -> Result<()> {
1286        // No bearer auth.
1287        client
1288            .delete(&self.upload_url)
1289            .send()
1290            .await?
1291            .parse_no_content()
1292            .await
1293    }
1294
1295    /// Upload bytes to an upload session
1296    ///
1297    /// You can upload the entire file, or split the file into multiple byte ranges,
1298    /// as long as the maximum bytes in any given request is less than 60 MiB.
1299    /// The fragments of the file must be uploaded sequentially in order. Uploading
1300    /// fragments out of order will result in an error.
1301    ///
1302    /// # Notes
1303    /// If your app splits a file into multiple byte ranges, the size of each
1304    /// byte range MUST be a multiple of 320 KiB (327,680 bytes). Using a fragment
1305    /// size that does not divide evenly by 320 KiB will result in errors committing
1306    /// some files. The 60 MiB limit and 320 KiB alignment are not checked locally since
1307    /// they may change in the future.
1308    ///
1309    /// The `file_size` of all part upload requests should be identical.
1310    ///
1311    /// # Results
1312    /// - If the part is uploaded successfully, but the file is not complete yet,
1313    ///   will return `None`.
1314    /// - If this is the last part and it is uploaded successfully,
1315    ///   will return `Some(<newly_created_drive_item>)`.
1316    ///
1317    /// # Errors
1318    /// When the file is completely uploaded, if an item with the same name is created
1319    /// during uploading, the last `upload_to_session` call will return `Err` with
1320    /// HTTP `409 CONFLICT`.
1321    ///
1322    /// # Panics
1323    /// Panic if `remote_range` is invalid or not match the length of `data`.
1324    ///
1325    /// # See also
1326    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#upload-bytes-to-the-upload-session)
1327    pub async fn upload_part(
1328        &self,
1329        data: impl Into<Bytes>,
1330        remote_range: std::ops::Range<u64>,
1331        file_size: u64,
1332        client: &Client,
1333    ) -> Result<Option<DriveItem>> {
1334        use std::convert::TryFrom as _;
1335
1336        let data = data.into();
1337        assert!(!data.is_empty(), "Empty data");
1338        assert!(
1339            remote_range.start < remote_range.end && remote_range.end <= file_size
1340            // `Range<u64>` has no method `len()`.
1341            && remote_range.end - remote_range.start <= u64::try_from(data.len()).unwrap(),
1342            "Invalid remote range",
1343        );
1344
1345        // No bearer auth.
1346        client
1347            .put(&self.upload_url)
1348            .header(
1349                header::CONTENT_RANGE,
1350                format!(
1351                    "bytes {}-{}/{}",
1352                    remote_range.start,
1353                    // Inclusive.
1354                    // We checked `remote_range.start < remote_range.end`,
1355                    // so this never overflows.
1356                    remote_range.end - 1,
1357                    file_size,
1358                ),
1359            )
1360            .body(data)
1361            .send()
1362            .await?
1363            .parse_optional()
1364            .await
1365    }
1366}
1367
1368#[cfg(test)]
1369mod test {
1370    use super::*;
1371    use crate::ItemId;
1372
1373    #[test]
1374    fn test_api_url() {
1375        let mock_item_id = ItemId("1234".to_owned());
1376        assert_eq!(
1377            api_path!(&ItemLocation::from_id(&mock_item_id)),
1378            "/drive/items/1234",
1379        );
1380
1381        assert_eq!(
1382            api_path!(&ItemLocation::from_path("/dir/file name").unwrap()),
1383            "/drive/root:%2Fdir%2Ffile%20name:",
1384        );
1385    }
1386
1387    #[test]
1388    fn test_path_name_check() {
1389        let invalid_names = ["", ".*?", "a|b", "a<b>b", ":run", "/", "\\"];
1390        let valid_names = [
1391            "QAQ",
1392            "0",
1393            ".",
1394            "a-a:", // Unicode colon "\u{ff1a}"
1395            "魔理沙",
1396        ];
1397
1398        let check_name = |s: &str| FileName::new(s).is_some();
1399        let check_path = |s: &str| ItemLocation::from_path(s).is_some();
1400
1401        for s in &valid_names {
1402            assert!(check_name(s), "{}", s);
1403            let path = format!("/{s}");
1404            assert!(check_path(&path), "{}", path);
1405
1406            for s2 in &valid_names {
1407                let mut path = format!("/{s}/{s2}");
1408                assert!(check_path(&path), "{}", path);
1409                path.push('/'); // Trailing
1410                assert!(check_path(&path), "{}", path);
1411            }
1412        }
1413
1414        for s in &invalid_names {
1415            assert!(!check_name(s), "{}", s);
1416
1417            // `/` and `/xx/` is valid and is tested below.
1418            if s.is_empty() {
1419                continue;
1420            }
1421
1422            let path = format!("/{s}");
1423            assert!(!check_path(&path), "{}", path);
1424
1425            for s2 in &valid_names {
1426                let path = format!("/{s2}/{s}");
1427                assert!(!check_path(&path), "{}", path);
1428            }
1429        }
1430
1431        assert!(check_path("/"));
1432        assert!(check_path("/a"));
1433        assert!(check_path("/a/"));
1434        assert!(check_path("/a/b"));
1435        assert!(check_path("/a/b/"));
1436
1437        assert!(!check_path(""));
1438        assert!(!check_path("/a/b//"));
1439        assert!(!check_path("a"));
1440        assert!(!check_path("a/"));
1441        assert!(!check_path("//"));
1442    }
1443}