onedrive_api/
util.rs

1use crate::{
2    error::{Error, Result},
3    resource::{DriveId, ErrorResponse, ItemId, OAuth2ErrorResponse},
4};
5use reqwest::{header, RequestBuilder, Response, StatusCode};
6use serde::{de, Deserialize};
7use url::PathSegmentsMut;
8
9/// Specify the location of a `Drive` resource.
10///
11/// # See also
12/// [`resource::Drive`][drive]
13///
14/// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/drive-get?view=graph-rest-1.0)
15///
16/// [drive]: ./resource/struct.Drive.html
17#[derive(Clone, Debug)]
18pub struct DriveLocation {
19    inner: DriveLocationEnum,
20}
21
22#[derive(Clone, Debug)]
23enum DriveLocationEnum {
24    Me,
25    User(String),
26    Group(String),
27    Site(String),
28    Id(DriveId),
29}
30
31impl DriveLocation {
32    /// Current user's OneDrive.
33    ///
34    /// # See also
35    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/drive-get?view=graph-rest-1.0#get-current-users-onedrive)
36    #[must_use]
37    pub fn me() -> Self {
38        Self {
39            inner: DriveLocationEnum::Me,
40        }
41    }
42
43    /// OneDrive of a user.
44    ///
45    /// # See also
46    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/drive-get?view=graph-rest-1.0#get-a-users-onedrive)
47    pub fn from_user(id_or_principal_name: impl Into<String>) -> Self {
48        Self {
49            inner: DriveLocationEnum::User(id_or_principal_name.into()),
50        }
51    }
52
53    /// The document library associated with a group.
54    ///
55    /// # See also
56    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/drive-get?view=graph-rest-1.0#get-the-document-library-associated-with-a-group)
57    pub fn from_group(group_id: impl Into<String>) -> Self {
58        Self {
59            inner: DriveLocationEnum::Group(group_id.into()),
60        }
61    }
62
63    /// The document library for a site.
64    ///
65    /// # See also
66    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/drive-get?view=graph-rest-1.0#get-the-document-library-for-a-site)
67    pub fn from_site(site_id: impl Into<String>) -> Self {
68        Self {
69            inner: DriveLocationEnum::Site(site_id.into()),
70        }
71    }
72
73    /// A drive with ID specified.
74    ///
75    /// # See also
76    /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/drive-get?view=graph-rest-1.0#get-a-drive-by-id)
77    #[must_use]
78    pub fn from_id(drive_id: DriveId) -> Self {
79        Self {
80            inner: DriveLocationEnum::Id(drive_id),
81        }
82    }
83}
84
85impl From<DriveId> for DriveLocation {
86    fn from(id: DriveId) -> Self {
87        Self::from_id(id)
88    }
89}
90
91/// Reference to a `DriveItem` in a drive.
92/// It does not contains the drive information.
93///
94/// # See also
95/// [`resource::DriveItem`][drive_item]
96///
97/// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-get?view=graph-rest-1.0)
98///
99/// [drive_item]: ./resource/struct.DriveItem.html
100// TODO: Now `DriveLocation` has only owned version, while `ItemLocation` has only borrowed version.
101#[derive(Clone, Copy, Debug)]
102pub struct ItemLocation<'a> {
103    inner: ItemLocationEnum<'a>,
104}
105
106#[derive(Clone, Copy, Debug)]
107enum ItemLocationEnum<'a> {
108    Path(&'a str),
109    Id(&'a str),
110    // See example `GET last user to modify file foo.txt` from
111    // https://docs.microsoft.com/en-us/graph/overview?view=graph-rest-1.0#popular-api-requests
112    ChildOfId {
113        parent_id: &'a str,
114        child_name: &'a str,
115    },
116}
117
118impl<'a> ItemLocation<'a> {
119    /// A UNIX-like `/`-started absolute path to a file or directory in the drive.
120    ///
121    /// # Error
122    /// If `path` contains invalid characters for OneDrive API, it returns None.
123    ///
124    /// # Note
125    /// The trailing `/` is optional.
126    ///
127    /// Special name on Windows like `CON` or `NUL` is tested to be permitted in API,
128    /// but may still cause errors on Windows or OneDrive Online.
129    /// These names will pass the check, but STRONGLY NOT recommended.
130    ///
131    /// # See also
132    /// [Microsoft Docs](https://support.office.com/en-us/article/Invalid-file-names-and-file-types-in-OneDrive-OneDrive-for-Business-and-SharePoint-64883a5d-228e-48f5-b3d2-eb39e07630fa#invalidcharacters)
133    #[must_use]
134    pub fn from_path(path: &'a str) -> Option<Self> {
135        if path == "/" {
136            Some(Self::root())
137        } else if path.starts_with('/')
138            && path[1..]
139                .split_terminator('/')
140                .all(|comp| !comp.is_empty() && FileName::new(comp).is_some())
141        {
142            Some(Self {
143                inner: ItemLocationEnum::Path(path),
144            })
145        } else {
146            None
147        }
148    }
149
150    /// Item id from other API.
151    #[must_use]
152    pub fn from_id(item_id: &'a ItemId) -> Self {
153        Self {
154            inner: ItemLocationEnum::Id(item_id.as_str()),
155        }
156    }
157
158    /// The root directory item.
159    #[must_use]
160    pub fn root() -> Self {
161        Self {
162            inner: ItemLocationEnum::Path("/"),
163        }
164    }
165
166    /// The child item in a directory.
167    #[must_use]
168    pub fn child_of_id(parent_id: &'a ItemId, child_name: &'a FileName) -> Self {
169        Self {
170            inner: ItemLocationEnum::ChildOfId {
171                parent_id: parent_id.as_str(),
172                child_name: child_name.as_str(),
173            },
174        }
175    }
176}
177
178impl<'a> From<&'a ItemId> for ItemLocation<'a> {
179    fn from(id: &'a ItemId) -> Self {
180        Self::from_id(id)
181    }
182}
183
184/// An valid file name str (unsized).
185#[derive(Debug)]
186pub struct FileName(str);
187
188impl FileName {
189    /// Check and wrap the name for a file or a directory in OneDrive.
190    ///
191    /// Returns None if contains invalid characters.
192    ///
193    /// # See also
194    /// [`ItemLocation::from_path`][from_path]
195    ///
196    /// [from_path]: ./struct.ItemLocation.html#method.from_path
197    pub fn new<S: AsRef<str> + ?Sized>(name: &S) -> Option<&Self> {
198        const INVALID_CHARS: &str = r#""*:<>?/\|"#;
199
200        let name = name.as_ref();
201        if !name.is_empty() && !name.contains(|c| INVALID_CHARS.contains(c)) {
202            Some(unsafe { &*(name as *const str as *const Self) })
203        } else {
204            None
205        }
206    }
207
208    /// View the file name as `&str`. It is cost-free.
209    #[must_use]
210    pub fn as_str(&self) -> &str {
211        &self.0
212    }
213}
214
215impl AsRef<str> for FileName {
216    fn as_ref(&self) -> &str {
217        self.as_str()
218    }
219}
220
221pub(crate) trait ApiPathComponent {
222    fn extend_into(&self, buf: &mut PathSegmentsMut);
223}
224
225impl ApiPathComponent for DriveLocation {
226    fn extend_into(&self, buf: &mut PathSegmentsMut) {
227        match &self.inner {
228            DriveLocationEnum::Me => buf.extend(&["me", "drive"]),
229            DriveLocationEnum::User(id) => buf.extend(&["users", id, "drive"]),
230            DriveLocationEnum::Group(id) => buf.extend(&["groups", id, "drive"]),
231            DriveLocationEnum::Site(id) => buf.extend(&["sites", id, "drive"]),
232            DriveLocationEnum::Id(id) => buf.extend(&["drives", id.as_str()]),
233        };
234    }
235}
236
237impl ApiPathComponent for ItemLocation<'_> {
238    fn extend_into(&self, buf: &mut PathSegmentsMut) {
239        match &self.inner {
240            ItemLocationEnum::Path("/") => buf.push("root"),
241            ItemLocationEnum::Path(path) => buf.push(&["root:", path, ":"].join("")),
242            ItemLocationEnum::Id(id) => buf.extend(&["items", id]),
243            ItemLocationEnum::ChildOfId {
244                parent_id,
245                child_name,
246            } => buf.extend(&["items", parent_id, "children", child_name]),
247        };
248    }
249}
250
251impl ApiPathComponent for str {
252    fn extend_into(&self, buf: &mut PathSegmentsMut) {
253        buf.push(self);
254    }
255}
256
257pub(crate) trait RequestBuilderTransformer {
258    fn trans(self, req: RequestBuilder) -> RequestBuilder;
259}
260
261pub(crate) trait RequestBuilderExt: Sized {
262    fn apply(self, trans: impl RequestBuilderTransformer) -> Self;
263}
264
265impl RequestBuilderExt for RequestBuilder {
266    fn apply(self, trans: impl RequestBuilderTransformer) -> Self {
267        trans.trans(self)
268    }
269}
270
271type BoxFuture<T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'static>>;
272
273// TODO: Avoid boxing?
274pub(crate) trait ResponseExt: Sized {
275    fn parse<T: de::DeserializeOwned>(self) -> BoxFuture<Result<T>>;
276    fn parse_optional<T: de::DeserializeOwned>(self) -> BoxFuture<Result<Option<T>>>;
277    fn parse_no_content(self) -> BoxFuture<Result<()>>;
278}
279
280impl ResponseExt for Response {
281    fn parse<T: de::DeserializeOwned>(self) -> BoxFuture<Result<T>> {
282        Box::pin(async move { Ok(handle_error_response(self).await?.json().await?) })
283    }
284
285    fn parse_optional<T: de::DeserializeOwned>(self) -> BoxFuture<Result<Option<T>>> {
286        Box::pin(async move {
287            match self.status() {
288                StatusCode::NOT_MODIFIED | StatusCode::ACCEPTED => Ok(None),
289                _ => Ok(Some(handle_error_response(self).await?.json().await?)),
290            }
291        })
292    }
293
294    fn parse_no_content(self) -> BoxFuture<Result<()>> {
295        Box::pin(async move {
296            handle_error_response(self).await?;
297            Ok(())
298        })
299    }
300}
301
302pub(crate) async fn handle_error_response(resp: Response) -> Result<Response> {
303    #[derive(Deserialize)]
304    struct Resp {
305        error: ErrorResponse,
306    }
307
308    let status = resp.status();
309    // `get_item_download_url_with_option` expects 302.
310    if status.is_success() || status.is_redirection() {
311        Ok(resp)
312    } else {
313        let retry_after = parse_retry_after_sec(&resp);
314        let resp: Resp = resp.json().await?;
315        Err(Error::from_error_response(status, resp.error, retry_after))
316    }
317}
318
319pub(crate) async fn handle_oauth2_error_response(resp: Response) -> Result<Response> {
320    let status = resp.status();
321    if status.is_success() {
322        Ok(resp)
323    } else {
324        let retry_after = parse_retry_after_sec(&resp);
325        let resp: OAuth2ErrorResponse = resp.json().await?;
326        Err(Error::from_oauth2_error_response(status, resp, retry_after))
327    }
328}
329
330/// The documentation said it is in seconds:
331/// <https://learn.microsoft.com/en-us/graph/throttling#best-practices-to-handle-throttling>.
332/// And HTTP requires it to be a non-negative integer:
333/// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After>.
334fn parse_retry_after_sec(resp: &Response) -> Option<u32> {
335    resp.headers()
336        .get(header::RETRY_AFTER)?
337        .to_str()
338        .ok()?
339        .parse()
340        .ok()
341}