#![allow(clippy::default_trait_access)] use crate::{
error::{Error, Result},
option::{CollectionOption, DriveItemPutOption, ObjectOption},
resource::{Drive, DriveField, DriveItem, DriveItemField, TimestampString},
util::{
handle_error_response, ApiPathComponent, DriveLocation, FileName, ItemLocation,
RequestBuilderExt as _, ResponseExt as _,
},
{ConflictBehavior, ExpectRange},
};
use bytes::Bytes;
use reqwest::{header, Client};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::fmt;
use url::Url;
macro_rules! api_url {
($($seg:expr),* $(,)?) => {{
let mut url = Url::parse("https://graph.microsoft.com/v1.0").unwrap();
{
let mut buf = url.path_segments_mut().unwrap();
$(ApiPathComponent::extend_into($seg, &mut buf);)*
} url
}};
}
macro_rules! api_path {
($item:expr) => {{
let mut url = Url::parse("path:///drive").unwrap();
let item: &ItemLocation = $item;
ApiPathComponent::extend_into(item, &mut url.path_segments_mut().unwrap());
url
}
.path()};
}
#[derive(Clone)]
pub struct OneDrive {
client: Client,
token: String,
drive: DriveLocation,
}
impl fmt::Debug for OneDrive {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OneDrive")
.field("client", &self.client)
.field("drive", &self.drive)
.finish_non_exhaustive()
}
}
impl OneDrive {
pub fn new(access_token: impl Into<String>, drive: impl Into<DriveLocation>) -> Self {
let client = Client::builder()
.redirect(reqwest::redirect::Policy::none())
.gzip(true)
.build()
.unwrap();
Self::new_with_client(client, access_token, drive.into())
}
pub fn new_with_client(
client: Client,
access_token: impl Into<String>,
drive: impl Into<DriveLocation>,
) -> Self {
OneDrive {
client,
token: access_token.into(),
drive: drive.into(),
}
}
#[must_use]
pub fn client(&self) -> &Client {
&self.client
}
#[must_use]
pub fn access_token(&self) -> &str {
&self.token
}
pub async fn get_drive_with_option(&self, option: ObjectOption<DriveField>) -> Result<Drive> {
self.client
.get(api_url![&self.drive])
.apply(option)
.bearer_auth(&self.token)
.send()
.await?
.parse()
.await
}
pub async fn get_drive(&self) -> Result<Drive> {
self.get_drive_with_option(Default::default()).await
}
pub async fn list_children_with_option<'a>(
&self,
item: impl Into<ItemLocation<'a>>,
option: CollectionOption<DriveItemField>,
) -> Result<Option<ListChildrenFetcher>> {
let opt_resp = self
.client
.get(api_url![&self.drive, &item.into(), "children"])
.apply(option)
.bearer_auth(&self.token)
.send()
.await?
.parse_optional()
.await?;
Ok(opt_resp.map(ListChildrenFetcher::new))
}
pub async fn list_children<'a>(
&self,
item: impl Into<ItemLocation<'a>>,
) -> Result<Vec<DriveItem>> {
self.list_children_with_option(item, Default::default())
.await?
.ok_or_else(|| Error::unexpected_response("Unexpected empty response"))?
.fetch_all(self)
.await
}
pub async fn get_item_with_option<'a>(
&self,
item: impl Into<ItemLocation<'a>>,
option: ObjectOption<DriveItemField>,
) -> Result<Option<DriveItem>> {
self.client
.get(api_url![&self.drive, &item.into()])
.apply(option)
.bearer_auth(&self.token)
.send()
.await?
.parse_optional()
.await
}
pub async fn get_item<'a>(&self, item: impl Into<ItemLocation<'a>>) -> Result<DriveItem> {
self.get_item_with_option(item, Default::default())
.await?
.ok_or_else(|| Error::unexpected_response("Unexpected empty response"))
}
pub async fn get_item_download_url_with_option<'a>(
&self,
item: impl Into<ItemLocation<'a>>,
option: ObjectOption<DriveItemField>,
) -> Result<String> {
let raw_resp = self
.client
.get(api_url![&self.drive, &item.into(), "content"])
.apply(option)
.bearer_auth(&self.token)
.send()
.await?;
let url = handle_error_response(raw_resp)
.await?
.headers()
.get(header::LOCATION)
.ok_or_else(|| {
Error::unexpected_response(
"Header `Location` not exists in response of `get_item_download_url`",
)
})?
.to_str()
.map_err(|_| Error::unexpected_response("Invalid string header `Location`"))?
.to_owned();
Ok(url)
}
pub async fn get_item_download_url<'a>(
&self,
item: impl Into<ItemLocation<'a>>,
) -> Result<String> {
self.get_item_download_url_with_option(item.into(), Default::default())
.await
}
pub async fn create_drive_item<'a>(
&self,
parent_item: impl Into<ItemLocation<'a>>,
drive_item: DriveItem,
option: DriveItemPutOption,
) -> Result<DriveItem> {
#[derive(Serialize)]
struct Req {
#[serde(rename = "@microsoft.graph.conflictBehavior")]
conflict_behavior: ConflictBehavior,
#[serde(flatten)]
drive_item: DriveItem,
}
let conflict_behavior = option
.get_conflict_behavior()
.unwrap_or(ConflictBehavior::Fail);
self.client
.post(api_url![&self.drive, &parent_item.into(), "children"])
.bearer_auth(&self.token)
.apply(option)
.json(&Req {
conflict_behavior,
drive_item,
})
.send()
.await?
.parse()
.await
}
pub async fn create_folder_with_option<'a>(
&self,
parent_item: impl Into<ItemLocation<'a>>,
name: &FileName,
option: DriveItemPutOption,
) -> Result<DriveItem> {
let drive_item = DriveItem {
name: Some(name.as_str().to_string()),
folder: Some(json!({}).into()),
..Default::default()
};
self.create_drive_item(parent_item, drive_item, option)
.await
}
pub async fn create_folder<'a>(
&self,
parent_item: impl Into<ItemLocation<'a>>,
name: &FileName,
) -> Result<DriveItem> {
self.create_folder_with_option(parent_item, name, Default::default())
.await
}
pub async fn update_item_with_option<'a>(
&self,
item: impl Into<ItemLocation<'a>>,
patch: &DriveItem,
option: ObjectOption<DriveItemField>,
) -> Result<DriveItem> {
self.client
.patch(api_url![&self.drive, &item.into()])
.bearer_auth(&self.token)
.apply(option)
.json(patch)
.send()
.await?
.parse()
.await
}
pub async fn update_item<'a>(
&self,
item: impl Into<ItemLocation<'a>>,
patch: &DriveItem,
) -> Result<DriveItem> {
self.update_item_with_option(item, patch, Default::default())
.await
}
pub const UPLOAD_SMALL_MAX_SIZE: usize = 4_000_000;
pub async fn upload_small<'a>(
&self,
item: impl Into<ItemLocation<'a>>,
data: impl Into<Bytes>,
) -> Result<DriveItem> {
let data = data.into();
self.client
.put(api_url![&self.drive, &item.into(), "content"])
.bearer_auth(&self.token)
.header(header::CONTENT_TYPE, "application/octet-stream")
.header(header::CONTENT_LENGTH, data.len().to_string())
.body(data)
.send()
.await?
.parse()
.await
}
pub async fn new_upload_session_with_initial_option<'a>(
&self,
item: impl Into<ItemLocation<'a>>,
initial: &DriveItem,
option: DriveItemPutOption,
) -> Result<(UploadSession, UploadSessionMeta)> {
#[derive(Serialize)]
struct Item<'a> {
#[serde(rename = "@microsoft.graph.conflictBehavior")]
conflict_behavior: ConflictBehavior,
#[serde(flatten)]
initial: &'a DriveItem,
}
#[derive(Serialize)]
struct Req<'a> {
item: Item<'a>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Resp {
upload_url: String,
#[serde(flatten)]
meta: UploadSessionMeta,
}
let conflict_behavior = option
.get_conflict_behavior()
.unwrap_or(ConflictBehavior::Fail);
let resp: Resp = self
.client
.post(api_url![&self.drive, &item.into(), "createUploadSession"])
.apply(option)
.bearer_auth(&self.token)
.json(&Req {
item: Item {
conflict_behavior,
initial,
},
})
.send()
.await?
.parse()
.await?;
Ok((
UploadSession {
upload_url: resp.upload_url,
},
resp.meta,
))
}
pub async fn new_upload_session_with_option<'a>(
&self,
item: impl Into<ItemLocation<'a>>,
option: DriveItemPutOption,
) -> Result<(UploadSession, UploadSessionMeta)> {
let initial = DriveItem::default();
self.new_upload_session_with_initial_option(item, &initial, option)
.await
}
pub async fn new_upload_session<'a>(
&self,
item: impl Into<ItemLocation<'a>>,
) -> Result<(UploadSession, UploadSessionMeta)> {
self.new_upload_session_with_option(item, Default::default())
.await
}
pub async fn copy<'a, 'b>(
&self,
source_item: impl Into<ItemLocation<'a>>,
dest_folder: impl Into<ItemLocation<'b>>,
dest_name: &FileName,
) -> Result<CopyProgressMonitor> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Req<'a> {
parent_reference: ItemReference<'a>,
name: &'a str,
}
let raw_resp = self
.client
.post(api_url![&self.drive, &source_item.into(), "copy"])
.bearer_auth(&self.token)
.json(&Req {
parent_reference: ItemReference {
path: api_path!(&dest_folder.into()),
},
name: dest_name.as_str(),
})
.send()
.await?;
let url = handle_error_response(raw_resp)
.await?
.headers()
.get(header::LOCATION)
.ok_or_else(|| {
Error::unexpected_response("Header `Location` not exists in response of `copy`")
})?
.to_str()
.map_err(|_| Error::unexpected_response("Invalid string header `Location`"))?
.to_owned();
Ok(CopyProgressMonitor::from_monitor_url(url))
}
pub async fn move_with_option<'a, 'b>(
&self,
source_item: impl Into<ItemLocation<'a>>,
dest_folder: impl Into<ItemLocation<'b>>,
dest_name: Option<&FileName>,
option: DriveItemPutOption,
) -> Result<DriveItem> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Req<'a> {
parent_reference: ItemReference<'a>,
name: Option<&'a str>,
#[serde(rename = "@microsoft.graph.conflictBehavior")]
conflict_behavior: ConflictBehavior,
}
let conflict_behavior = option
.get_conflict_behavior()
.unwrap_or(ConflictBehavior::Fail);
self.client
.patch(api_url![&self.drive, &source_item.into()])
.bearer_auth(&self.token)
.apply(option)
.json(&Req {
parent_reference: ItemReference {
path: api_path!(&dest_folder.into()),
},
name: dest_name.map(FileName::as_str),
conflict_behavior,
})
.send()
.await?
.parse()
.await
}
pub async fn move_<'a, 'b>(
&self,
source_item: impl Into<ItemLocation<'a>>,
dest_folder: impl Into<ItemLocation<'b>>,
dest_name: Option<&FileName>,
) -> Result<DriveItem> {
self.move_with_option(source_item, dest_folder, dest_name, Default::default())
.await
}
pub async fn delete_with_option<'a>(
&self,
item: impl Into<ItemLocation<'a>>,
option: DriveItemPutOption,
) -> Result<()> {
assert!(
option.get_conflict_behavior().is_none(),
"`conflict_behavior` is not supported by `delete[_with_option]`",
);
self.client
.delete(api_url![&self.drive, &item.into()])
.bearer_auth(&self.token)
.apply(option)
.send()
.await?
.parse_no_content()
.await
}
pub async fn delete<'a>(&self, item: impl Into<ItemLocation<'a>>) -> Result<()> {
self.delete_with_option(item, Default::default()).await
}
pub async fn track_root_changes_from_initial_with_option(
&self,
option: CollectionOption<DriveItemField>,
) -> Result<TrackChangeFetcher> {
assert!(
!option.has_get_count(),
"`get_count` is not supported by Track Changes API",
);
let resp = self
.client
.get(api_url![&self.drive, "root", "delta"])
.apply(option)
.bearer_auth(&self.token)
.send()
.await?
.parse()
.await?;
Ok(TrackChangeFetcher::new(resp))
}
pub async fn track_root_changes_from_initial(&self) -> Result<TrackChangeFetcher> {
self.track_root_changes_from_initial_with_option(Default::default())
.await
}
pub async fn track_root_changes_from_delta_url(
&self,
delta_url: &str,
) -> Result<TrackChangeFetcher> {
let resp: DriveItemCollectionResponse = self
.client
.get(delta_url)
.bearer_auth(&self.token)
.send()
.await?
.parse()
.await?;
Ok(TrackChangeFetcher::new(resp))
}
pub async fn get_root_latest_delta_url_with_option(
&self,
option: CollectionOption<DriveItemField>,
) -> Result<String> {
assert!(
!option.has_get_count(),
"`get_count` is not supported by Track Changes API",
);
self.client
.get(api_url![&self.drive, "root", "delta"])
.query(&[("token", "latest")])
.apply(option)
.bearer_auth(&self.token)
.send()
.await?
.parse::<DriveItemCollectionResponse>()
.await?
.delta_url
.ok_or_else(|| {
Error::unexpected_response(
"Missing field `@odata.deltaLink` for getting latest delta",
)
})
}
pub async fn get_root_latest_delta_url(&self) -> Result<String> {
self.get_root_latest_delta_url_with_option(Default::default())
.await
}
}
#[derive(Debug, Clone)]
pub struct CopyProgressMonitor {
monitor_url: String,
}
#[cfg(feature = "beta")]
#[allow(missing_docs)]
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct CopyProgress {
pub percentage_complete: f64,
pub status: CopyStatus,
}
#[cfg(feature = "beta")]
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum CopyStatus {
NotStarted,
InProgress,
Completed,
Updating,
Failed,
DeletePending,
DeleteFailed,
Waiting,
}
impl CopyProgressMonitor {
pub fn from_monitor_url(monitor_url: impl Into<String>) -> Self {
Self {
monitor_url: monitor_url.into(),
}
}
#[must_use]
pub fn monitor_url(&self) -> &str {
&self.monitor_url
}
#[cfg(feature = "beta")]
pub async fn fetch_progress(&self, onedrive: &OneDrive) -> Result<CopyProgress> {
onedrive
.client
.get(&self.monitor_url)
.send()
.await?
.parse()
.await
}
}
#[derive(Debug, Deserialize)]
struct DriveItemCollectionResponse {
value: Option<Vec<DriveItem>>,
#[serde(rename = "@odata.nextLink")]
next_url: Option<String>,
#[serde(rename = "@odata.deltaLink")]
delta_url: Option<String>,
}
#[derive(Debug)]
struct DriveItemFetcher {
last_response: DriveItemCollectionResponse,
}
impl DriveItemFetcher {
fn new(first_response: DriveItemCollectionResponse) -> Self {
Self {
last_response: first_response,
}
}
fn resume_from(next_url: impl Into<String>) -> Self {
Self::new(DriveItemCollectionResponse {
value: None,
next_url: Some(next_url.into()),
delta_url: None,
})
}
fn next_url(&self) -> Option<&str> {
match &self.last_response {
DriveItemCollectionResponse {
value: None,
next_url: Some(next_url),
..
} => Some(next_url),
_ => None,
}
}
fn delta_url(&self) -> Option<&str> {
self.last_response.delta_url.as_deref()
}
async fn fetch_next_page(&mut self, onedrive: &OneDrive) -> Result<Option<Vec<DriveItem>>> {
if let Some(items) = self.last_response.value.take() {
return Ok(Some(items));
}
let url = match self.last_response.next_url.as_ref() {
None => return Ok(None),
Some(url) => url,
};
self.last_response = onedrive
.client
.get(url)
.bearer_auth(&onedrive.token)
.send()
.await?
.parse()
.await?;
Ok(Some(self.last_response.value.take().unwrap_or_default()))
}
async fn fetch_all(mut self, onedrive: &OneDrive) -> Result<(Vec<DriveItem>, Option<String>)> {
let mut buf = vec![];
while let Some(items) = self.fetch_next_page(onedrive).await? {
buf.extend(items);
}
Ok((buf, self.delta_url().map(Into::into)))
}
}
#[derive(Debug)]
pub struct ListChildrenFetcher {
fetcher: DriveItemFetcher,
}
impl ListChildrenFetcher {
fn new(first_response: DriveItemCollectionResponse) -> Self {
Self {
fetcher: DriveItemFetcher::new(first_response),
}
}
#[must_use]
pub fn resume_from(next_url: impl Into<String>) -> Self {
Self {
fetcher: DriveItemFetcher::resume_from(next_url),
}
}
#[must_use]
pub fn next_url(&self) -> Option<&str> {
self.fetcher.next_url()
}
pub async fn fetch_next_page(&mut self, onedrive: &OneDrive) -> Result<Option<Vec<DriveItem>>> {
self.fetcher.fetch_next_page(onedrive).await
}
pub async fn fetch_all(self, onedrive: &OneDrive) -> Result<Vec<DriveItem>> {
self.fetcher
.fetch_all(onedrive)
.await
.map(|(items, _)| items)
}
}
#[derive(Debug)]
pub struct TrackChangeFetcher {
fetcher: DriveItemFetcher,
}
impl TrackChangeFetcher {
fn new(first_response: DriveItemCollectionResponse) -> Self {
Self {
fetcher: DriveItemFetcher::new(first_response),
}
}
#[must_use]
pub fn resume_from(next_url: impl Into<String>) -> Self {
Self {
fetcher: DriveItemFetcher::resume_from(next_url),
}
}
#[must_use]
pub fn next_url(&self) -> Option<&str> {
self.fetcher.next_url()
}
#[must_use]
pub fn delta_url(&self) -> Option<&str> {
self.fetcher.delta_url()
}
pub async fn fetch_next_page(&mut self, onedrive: &OneDrive) -> Result<Option<Vec<DriveItem>>> {
self.fetcher.fetch_next_page(onedrive).await
}
pub async fn fetch_all(self, onedrive: &OneDrive) -> Result<(Vec<DriveItem>, String)> {
let (items, opt_delta_url) = self.fetcher.fetch_all(onedrive).await?;
let delta_url = opt_delta_url.ok_or_else(|| {
Error::unexpected_response("Missing `@odata.deltaLink` for the last page")
})?;
Ok((items, delta_url))
}
}
#[derive(Serialize)]
struct ItemReference<'a> {
path: &'a str,
}
#[derive(Debug)]
pub struct UploadSession {
upload_url: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UploadSessionMeta {
pub next_expected_ranges: Vec<ExpectRange>,
pub expiration_date_time: TimestampString,
}
impl UploadSession {
pub const MAX_PART_SIZE: usize = 60 << 20;
pub fn from_upload_url(upload_url: impl Into<String>) -> Self {
Self {
upload_url: upload_url.into(),
}
}
pub async fn get_meta(&self, client: &Client) -> Result<UploadSessionMeta> {
client
.get(&self.upload_url)
.send()
.await?
.parse::<UploadSessionMeta>()
.await
}
#[must_use]
pub fn upload_url(&self) -> &str {
&self.upload_url
}
pub async fn delete(&self, client: &Client) -> Result<()> {
client
.delete(&self.upload_url)
.send()
.await?
.parse_no_content()
.await
}
pub async fn upload_part(
&self,
data: impl Into<Bytes>,
remote_range: std::ops::Range<u64>,
file_size: u64,
client: &Client,
) -> Result<Option<DriveItem>> {
use std::convert::TryFrom as _;
let data = data.into();
assert!(!data.is_empty(), "Empty data");
assert!(
remote_range.start < remote_range.end && remote_range.end <= file_size
&& remote_range.end - remote_range.start <= u64::try_from(data.len()).unwrap(),
"Invalid remote range",
);
client
.put(&self.upload_url)
.header(
header::CONTENT_RANGE,
format!(
"bytes {}-{}/{}",
remote_range.start,
remote_range.end - 1,
file_size,
),
)
.body(data)
.send()
.await?
.parse_optional()
.await
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::ItemId;
#[test]
fn test_api_url() {
let mock_item_id = ItemId("1234".to_owned());
assert_eq!(
api_path!(&ItemLocation::from_id(&mock_item_id)),
"/drive/items/1234",
);
assert_eq!(
api_path!(&ItemLocation::from_path("/dir/file name").unwrap()),
"/drive/root:%2Fdir%2Ffile%20name:",
);
}
#[test]
fn test_path_name_check() {
let invalid_names = ["", ".*?", "a|b", "a<b>b", ":run", "/", "\\"];
let valid_names = [
"QAQ",
"0",
".",
"a-a:", "魔理沙",
];
let check_name = |s: &str| FileName::new(s).is_some();
let check_path = |s: &str| ItemLocation::from_path(s).is_some();
for s in &valid_names {
assert!(check_name(s), "{}", s);
let path = format!("/{s}");
assert!(check_path(&path), "{}", path);
for s2 in &valid_names {
let mut path = format!("/{s}/{s2}");
assert!(check_path(&path), "{}", path);
path.push('/'); assert!(check_path(&path), "{}", path);
}
}
for s in &invalid_names {
assert!(!check_name(s), "{}", s);
if s.is_empty() {
continue;
}
let path = format!("/{s}");
assert!(!check_path(&path), "{}", path);
for s2 in &valid_names {
let path = format!("/{s2}/{s}");
assert!(!check_path(&path), "{}", path);
}
}
assert!(check_path("/"));
assert!(check_path("/a"));
assert!(check_path("/a/"));
assert!(check_path("/a/b"));
assert!(check_path("/a/b/"));
assert!(!check_path(""));
assert!(!check_path("/a/b//"));
assert!(!check_path("a"));
assert!(!check_path("a/"));
assert!(!check_path("//"));
}
}