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}