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 Some(url) = self.last_response.next_url.as_ref() else {
1017 return Ok(None);
1018 };
1019 self.last_response = onedrive
1020 .client
1021 .get(url)
1022 .bearer_auth(&onedrive.token)
1023 .send()
1024 .await?
1025 .parse()
1026 .await?;
1027 Ok(Some(self.last_response.value.take().unwrap_or_default()))
1028 }
1029
1030 async fn fetch_all(mut self, onedrive: &OneDrive) -> Result<(Vec<DriveItem>, Option<String>)> {
1031 let mut buf = vec![];
1032 while let Some(items) = self.fetch_next_page(onedrive).await? {
1033 buf.extend(items);
1034 }
1035 Ok((buf, self.delta_url().map(Into::into)))
1036 }
1037}
1038
1039/// The page fetcher for listing children
1040///
1041/// # See also
1042/// [`OneDrive::list_children_with_option`][list_children_with_opt]
1043///
1044/// [list_children_with_opt]: ./struct.OneDrive.html#method.list_children_with_option
1045#[derive(Debug)]
1046pub struct ListChildrenFetcher {
1047 fetcher: DriveItemFetcher,
1048}
1049
1050impl ListChildrenFetcher {
1051 fn new(first_response: DriveItemCollectionResponse) -> Self {
1052 Self {
1053 fetcher: DriveItemFetcher::new(first_response),
1054 }
1055 }
1056
1057 /// Resume a fetching process from url from
1058 /// [`ListChildrenFetcher::next_url`][next_url].
1059 ///
1060 /// [next_url]: #method.next_url
1061 #[must_use]
1062 pub fn resume_from(next_url: impl Into<String>) -> Self {
1063 Self {
1064 fetcher: DriveItemFetcher::resume_from(next_url),
1065 }
1066 }
1067
1068 /// Try to get the url to the next page.
1069 ///
1070 /// Used for resuming the fetching progress.
1071 ///
1072 /// # Error
1073 /// Will success only if there are more pages and the first page is already read.
1074 ///
1075 /// # Note
1076 /// The first page data from [`OneDrive::list_children_with_option`][list_children_with_opt]
1077 /// will be cached and have no idempotent url to resume/re-fetch.
1078 ///
1079 /// [list_children_with_opt]: ./struct.OneDrive.html#method.list_children_with_option
1080 #[must_use]
1081 pub fn next_url(&self) -> Option<&str> {
1082 self.fetcher.next_url()
1083 }
1084
1085 /// Fetch the next page, or `None` if reaches the end.
1086 pub async fn fetch_next_page(&mut self, onedrive: &OneDrive) -> Result<Option<Vec<DriveItem>>> {
1087 self.fetcher.fetch_next_page(onedrive).await
1088 }
1089
1090 /// Fetch all rest pages and collect all items.
1091 ///
1092 /// # Errors
1093 ///
1094 /// Any error occurs when fetching will lead to an failure, and
1095 /// all progress will be lost.
1096 pub async fn fetch_all(self, onedrive: &OneDrive) -> Result<Vec<DriveItem>> {
1097 self.fetcher
1098 .fetch_all(onedrive)
1099 .await
1100 .map(|(items, _)| items)
1101 }
1102}
1103
1104/// The page fetcher for tracking operations with `Iterator` interface.
1105///
1106/// # See also
1107/// [`OneDrive::track_changes_from_initial`][track_initial]
1108///
1109/// [`OneDrive::track_changes_from_delta_url`][track_delta]
1110///
1111/// [track_initial]: ./struct.OneDrive.html#method.track_changes_from_initial_with_option
1112/// [track_delta]: ./struct.OneDrive.html#method.track_changes_from_delta_url
1113#[derive(Debug)]
1114pub struct TrackChangeFetcher {
1115 fetcher: DriveItemFetcher,
1116}
1117
1118impl TrackChangeFetcher {
1119 fn new(first_response: DriveItemCollectionResponse) -> Self {
1120 Self {
1121 fetcher: DriveItemFetcher::new(first_response),
1122 }
1123 }
1124
1125 /// Resume a fetching process from url.
1126 ///
1127 /// The url should be from [`TrackChangeFetcher::next_url`][next_url].
1128 ///
1129 /// [next_url]: #method.next_url
1130 #[must_use]
1131 pub fn resume_from(next_url: impl Into<String>) -> Self {
1132 Self {
1133 fetcher: DriveItemFetcher::resume_from(next_url),
1134 }
1135 }
1136
1137 /// Try to get the url to the next page.
1138 ///
1139 /// Used for resuming the fetching progress.
1140 ///
1141 /// # Error
1142 /// Will success only if there are more pages and the first page is already read.
1143 ///
1144 /// # Note
1145 /// The first page data from
1146 /// [`OneDrive::track_changes_from_initial_with_option`][track_initial]
1147 /// will be cached and have no idempotent url to resume/re-fetch.
1148 ///
1149 /// [track_initial]: ./struct.OneDrive.html#method.track_changes_from_initial
1150 #[must_use]
1151 pub fn next_url(&self) -> Option<&str> {
1152 self.fetcher.next_url()
1153 }
1154
1155 /// Try to the delta url representing a snapshot of current track change operation.
1156 ///
1157 /// Used for tracking changes from this snapshot (rather than initial) later,
1158 /// using [`OneDrive::track_changes_from_delta_url`][track_delta].
1159 ///
1160 /// # Error
1161 /// Will success only if there are no more pages.
1162 ///
1163 /// # See also
1164 /// [`OneDrive::track_changes_from_delta_url`][track_delta]
1165 ///
1166 /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-delta?view=graph-rest-1.0#example-last-page-in-a-set)
1167 ///
1168 /// [track_delta]: ./struct.OneDrive.html#method.track_changes_from_delta_url
1169 #[must_use]
1170 pub fn delta_url(&self) -> Option<&str> {
1171 self.fetcher.delta_url()
1172 }
1173
1174 /// Fetch the next page, or `None` if reaches the end.
1175 pub async fn fetch_next_page(&mut self, onedrive: &OneDrive) -> Result<Option<Vec<DriveItem>>> {
1176 self.fetcher.fetch_next_page(onedrive).await
1177 }
1178
1179 /// Fetch all rest pages, collect all items, and also return `delta_url`.
1180 ///
1181 /// # Errors
1182 ///
1183 /// Any error occurs when fetching will lead to an failure, and
1184 /// all progress will be lost.
1185 pub async fn fetch_all(self, onedrive: &OneDrive) -> Result<(Vec<DriveItem>, String)> {
1186 let (items, opt_delta_url) = self.fetcher.fetch_all(onedrive).await?;
1187 let delta_url = opt_delta_url.ok_or_else(|| {
1188 Error::unexpected_response("Missing `@odata.deltaLink` for the last page")
1189 })?;
1190 Ok((items, delta_url))
1191 }
1192}
1193
1194#[derive(Serialize)]
1195struct ItemReference<'a> {
1196 path: &'a str,
1197}
1198
1199/// An upload session for resumable file uploading process.
1200///
1201/// # See also
1202/// [`OneDrive::new_upload_session`][get_session]
1203///
1204/// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/resources/uploadsession?view=graph-rest-1.0)
1205///
1206/// [get_session]: ./struct.OneDrive.html#method.new_upload_session
1207#[derive(Debug)]
1208pub struct UploadSession {
1209 upload_url: String,
1210}
1211
1212/// Metadata of an in-progress upload session
1213///
1214/// # See also
1215/// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#resuming-an-in-progress-upload)
1216#[derive(Debug, Deserialize)]
1217#[serde(rename_all = "camelCase")]
1218#[non_exhaustive]
1219pub struct UploadSessionMeta {
1220 /// Get a collection of byte ranges that the server is missing for the file.
1221 ///
1222 /// Used for determine what to upload when resuming a session.
1223 pub next_expected_ranges: Vec<ExpectRange>,
1224 /// Get the date and time in UTC that the upload session will expire.
1225 ///
1226 /// The complete file must be uploaded before this expiration time is reached.
1227 pub expiration_date_time: TimestampString,
1228}
1229
1230impl UploadSession {
1231 /// The upload size limit of a single [`upload_part`] call.
1232 ///
1233 /// The value is from
1234 /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#upload-bytes-to-the-upload-session)
1235 /// and may not be accurate or stable.
1236 ///
1237 /// [`upload_part`]: #method.upload_part
1238 pub const MAX_PART_SIZE: usize = 60 << 20; // 60 MiB
1239
1240 /// Construct back the upload session from upload URL.
1241 pub fn from_upload_url(upload_url: impl Into<String>) -> Self {
1242 Self {
1243 upload_url: upload_url.into(),
1244 }
1245 }
1246
1247 /// Query the metadata of the upload to find out which byte ranges
1248 /// have been received previously.
1249 ///
1250 /// # See also
1251 /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#resuming-an-in-progress-upload)
1252 pub async fn get_meta(&self, client: &Client) -> Result<UploadSessionMeta> {
1253 // No bearer auth.
1254 client
1255 .get(&self.upload_url)
1256 .send()
1257 .await?
1258 .parse::<UploadSessionMeta>()
1259 .await
1260 }
1261
1262 /// The URL endpoint accepting PUT requests.
1263 ///
1264 /// It is exactly what you passed in [`UploadSession::from_upload_url`].
1265 ///
1266 /// [`UploadSession::from_upload_url`]: #method.new
1267 #[must_use]
1268 pub fn upload_url(&self) -> &str {
1269 &self.upload_url
1270 }
1271
1272 /// Cancel the upload session
1273 ///
1274 /// This cleans up the temporary file holding the data previously uploaded.
1275 /// This should be used in scenarios where the upload is aborted, for example,
1276 /// if the user cancels the transfer.
1277 ///
1278 /// Temporary files and their accompanying upload session are automatically
1279 /// cleaned up after the `expirationDateTime` has passed. Temporary files may
1280 /// not be deleted immediately after the expiration time has elapsed.
1281 ///
1282 /// # See also
1283 /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#cancel-the-upload-session)
1284 pub async fn delete(&self, client: &Client) -> Result<()> {
1285 // No bearer auth.
1286 client
1287 .delete(&self.upload_url)
1288 .send()
1289 .await?
1290 .parse_no_content()
1291 .await
1292 }
1293
1294 /// Upload bytes to an upload session
1295 ///
1296 /// You can upload the entire file, or split the file into multiple byte ranges,
1297 /// as long as the maximum bytes in any given request is less than 60 MiB.
1298 /// The fragments of the file must be uploaded sequentially in order. Uploading
1299 /// fragments out of order will result in an error.
1300 ///
1301 /// # Notes
1302 /// If your app splits a file into multiple byte ranges, the size of each
1303 /// byte range MUST be a multiple of 320 KiB (327,680 bytes). Using a fragment
1304 /// size that does not divide evenly by 320 KiB will result in errors committing
1305 /// some files. The 60 MiB limit and 320 KiB alignment are not checked locally since
1306 /// they may change in the future.
1307 ///
1308 /// The `file_size` of all part upload requests should be identical.
1309 ///
1310 /// # Results
1311 /// - If the part is uploaded successfully, but the file is not complete yet,
1312 /// will return `None`.
1313 /// - If this is the last part and it is uploaded successfully,
1314 /// will return `Some(<newly_created_drive_item>)`.
1315 ///
1316 /// # Errors
1317 /// When the file is completely uploaded, if an item with the same name is created
1318 /// during uploading, the last `upload_to_session` call will return `Err` with
1319 /// HTTP `409 CONFLICT`.
1320 ///
1321 /// # Panics
1322 /// Panic if `remote_range` is invalid or not match the length of `data`.
1323 ///
1324 /// # See also
1325 /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#upload-bytes-to-the-upload-session)
1326 pub async fn upload_part(
1327 &self,
1328 data: impl Into<Bytes>,
1329 remote_range: std::ops::Range<u64>,
1330 file_size: u64,
1331 client: &Client,
1332 ) -> Result<Option<DriveItem>> {
1333 use std::convert::TryFrom as _;
1334
1335 let data = data.into();
1336 assert!(!data.is_empty(), "Empty data");
1337 assert!(
1338 remote_range.start < remote_range.end && remote_range.end <= file_size
1339 // `Range<u64>` has no method `len()`.
1340 && remote_range.end - remote_range.start <= u64::try_from(data.len()).unwrap(),
1341 "Invalid remote range",
1342 );
1343
1344 // No bearer auth.
1345 client
1346 .put(&self.upload_url)
1347 .header(
1348 header::CONTENT_RANGE,
1349 format!(
1350 "bytes {}-{}/{}",
1351 remote_range.start,
1352 // Inclusive.
1353 // We checked `remote_range.start < remote_range.end`,
1354 // so this never overflows.
1355 remote_range.end - 1,
1356 file_size,
1357 ),
1358 )
1359 .body(data)
1360 .send()
1361 .await?
1362 .parse_optional()
1363 .await
1364 }
1365}
1366
1367#[cfg(test)]
1368mod test {
1369 use super::*;
1370 use crate::ItemId;
1371
1372 #[test]
1373 fn test_api_url() {
1374 let mock_item_id = ItemId("1234".to_owned());
1375 assert_eq!(
1376 api_path!(&ItemLocation::from_id(&mock_item_id)),
1377 "/drive/items/1234",
1378 );
1379
1380 assert_eq!(
1381 api_path!(&ItemLocation::from_path("/dir/file name").unwrap()),
1382 "/drive/root:%2Fdir%2Ffile%20name:",
1383 );
1384 }
1385
1386 #[test]
1387 fn test_path_name_check() {
1388 let invalid_names = ["", ".*?", "a|b", "a<b>b", ":run", "/", "\\"];
1389 let valid_names = [
1390 "QAQ",
1391 "0",
1392 ".",
1393 "a-a:", // Unicode colon "\u{ff1a}"
1394 "魔理沙",
1395 ];
1396
1397 let check_name = |s: &str| FileName::new(s).is_some();
1398 let check_path = |s: &str| ItemLocation::from_path(s).is_some();
1399
1400 for s in &valid_names {
1401 assert!(check_name(s), "{}", s);
1402 let path = format!("/{s}");
1403 assert!(check_path(&path), "{}", path);
1404
1405 for s2 in &valid_names {
1406 let mut path = format!("/{s}/{s2}");
1407 assert!(check_path(&path), "{}", path);
1408 path.push('/'); // Trailing
1409 assert!(check_path(&path), "{}", path);
1410 }
1411 }
1412
1413 for s in &invalid_names {
1414 assert!(!check_name(s), "{}", s);
1415
1416 // `/` and `/xx/` is valid and is tested below.
1417 if s.is_empty() {
1418 continue;
1419 }
1420
1421 let path = format!("/{s}");
1422 assert!(!check_path(&path), "{}", path);
1423
1424 for s2 in &valid_names {
1425 let path = format!("/{s2}/{s}");
1426 assert!(!check_path(&path), "{}", path);
1427 }
1428 }
1429
1430 assert!(check_path("/"));
1431 assert!(check_path("/a"));
1432 assert!(check_path("/a/"));
1433 assert!(check_path("/a/b"));
1434 assert!(check_path("/a/b/"));
1435
1436 assert!(!check_path(""));
1437 assert!(!check_path("/a/b//"));
1438 assert!(!check_path("a"));
1439 assert!(!check_path("a/"));
1440 assert!(!check_path("//"));
1441 }
1442}