This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git
The following commit(s) were added to refs/heads/main by this push:
new 64a0f4492 feat(services/onedrive): add signer to utilize the refresh
token (#5733)
64a0f4492 is described below
commit 64a0f44925359a19f9748c49fc3d55913097c883
Author: Erick Guan <[email protected]>
AuthorDate: Tue Mar 11 13:00:00 2025 +0100
feat(services/onedrive): add signer to utilize the refresh token (#5733)
* chore(services/onedrive): extract OneDrive to core structure
* Use http_client from OpenDAL's context
* feat(services/onedrive): add signer to utilize the refresh token
* fixup! chore(services/onedrive): extract OneDrive to core structure
* fixup! chore(services/onedrive): extract OneDrive to core structure
* fixup! chore(services/onedrive): extract OneDrive to core structure
---
core/src/services/onedrive/backend.rs | 308 +++---------------------
core/src/services/onedrive/builder.rs | 151 ++++++++++--
core/src/services/onedrive/config.rs | 12 +-
core/src/services/onedrive/core.rs | 377 ++++++++++++++++++++++++++++++
core/src/services/onedrive/delete.rs | 18 +-
core/src/services/onedrive/docs.md | 11 +-
core/src/services/onedrive/error.rs | 5 +-
core/src/services/onedrive/graph_model.rs | 57 ++---
core/src/services/onedrive/lister.rs | 66 +++---
core/src/services/onedrive/mod.rs | 2 +
core/src/services/onedrive/writer.rs | 41 ++--
11 files changed, 637 insertions(+), 411 deletions(-)
diff --git a/core/src/services/onedrive/backend.rs
b/core/src/services/onedrive/backend.rs
index a252055ed..48d52673d 100644
--- a/core/src/services/onedrive/backend.rs
+++ b/core/src/services/onedrive/backend.rs
@@ -16,111 +16,54 @@
// under the License.
use std::fmt::Debug;
+use std::fmt::Formatter;
use std::sync::Arc;
-use bytes::Buf;
-use bytes::Bytes;
-use http::header;
-use http::Request;
use http::Response;
use http::StatusCode;
-use super::delete::OnedriveDeleter;
+use super::core::OneDriveCore;
+use super::delete::OneDriveDeleter;
use super::error::parse_error;
-use super::graph_model::CreateDirPayload;
-use super::graph_model::ItemType;
-use super::graph_model::OneDriveUploadSessionCreationRequestBody;
-use super::graph_model::OnedriveGetItemBody;
-use super::lister::OnedriveLister;
+use super::lister::OneDriveLister;
use super::writer::OneDriveWriter;
use crate::raw::*;
use crate::*;
#[derive(Clone)]
pub struct OnedriveBackend {
- info: Arc<AccessorInfo>,
- root: String,
- access_token: String,
- client: HttpClient,
-}
-
-impl OnedriveBackend {
- pub(crate) fn new(root: String, access_token: String, http_client:
HttpClient) -> Self {
- Self {
- info: {
- let ma = AccessorInfo::default();
- ma.set_scheme(Scheme::Onedrive)
- .set_root(&root)
- .set_native_capability(Capability {
- read: true,
- write: true,
- stat: true,
- stat_has_etag: true,
- stat_has_last_modified: true,
- stat_has_content_length: true,
- delete: true,
- create_dir: true,
- list: true,
- list_has_content_length: true,
- list_has_etag: true,
- list_has_last_modified: true,
- shared: true,
- ..Default::default()
- });
-
- ma.into()
- },
- root,
- access_token,
- client: http_client,
- }
- }
+ pub core: Arc<OneDriveCore>,
}
impl Debug for OnedriveBackend {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let mut de = f.debug_struct("OneDriveBackend");
- de.field("root", &self.root);
- de.field("access_token", &"<redacted>");
- de.finish()
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("OnedriveBackend")
+ .field("core", &self.core)
+ .finish()
}
}
impl Access for OnedriveBackend {
type Reader = HttpBody;
type Writer = oio::OneShotWriter<OneDriveWriter>;
- type Lister = oio::PageLister<OnedriveLister>;
- type Deleter = oio::OneShotDeleter<OnedriveDeleter>;
+ type Lister = oio::PageLister<OneDriveLister>;
+ type Deleter = oio::OneShotDeleter<OneDriveDeleter>;
type BlockingReader = ();
type BlockingWriter = ();
type BlockingLister = ();
type BlockingDeleter = ();
fn info(&self) -> Arc<AccessorInfo> {
- self.info.clone()
+ self.core.info.clone()
}
- async fn create_dir(&self, path: &str, _: OpCreateDir) ->
Result<RpCreateDir> {
+ async fn create_dir(&self, path: &str, _args: OpCreateDir) ->
Result<RpCreateDir> {
if path == "/" {
// skip, the root path exists in the personal OneDrive.
return Ok(RpCreateDir::default());
}
- let path = build_rooted_abs_path(&self.root, path);
- let path_before_last_slash = get_parent(&path);
- let encoded_path = percent_encode_path(path_before_last_slash);
-
- let uri = format!(
- "https://graph.microsoft.com/v1.0/me/drive/root:{}:/children",
- encoded_path
- );
-
- let folder_name = get_basename(&path);
- let folder_name = folder_name.strip_suffix('/').unwrap_or(folder_name);
-
- let body = CreateDirPayload::new(folder_name.to_string());
-
- let response = self.onedrive_create_dir(&uri, body).await?;
+ let response = self.core.onedrive_create_dir(path).await?;
let status = response.status();
match status {
@@ -129,51 +72,23 @@ impl Access for OnedriveBackend {
}
}
- async fn stat(&self, path: &str, _: OpStat) -> Result<RpStat> {
- // Stat root always returns a DIR.
- if path == "/" {
- return Ok(RpStat::new(Metadata::new(EntryMode::DIR)));
- }
-
- let resp = self.onedrive_get_stat(path).await?;
- let status = resp.status();
-
- if status.is_success() {
- let bytes = resp.into_body();
- let decoded_response: OnedriveGetItemBody =
-
serde_json::from_reader(bytes.reader()).map_err(new_json_deserialize_error)?;
-
- let entry_mode: EntryMode = match decoded_response.item_type {
- ItemType::Folder { .. } => EntryMode::DIR,
- ItemType::File { .. } => EntryMode::FILE,
- };
-
- let mut meta = Metadata::new(entry_mode);
- meta.set_etag(&decoded_response.e_tag);
+ async fn stat(&self, path: &str, _args: OpStat) -> Result<RpStat> {
+ let path = build_rooted_abs_path(&self.core.root, path);
+ let meta = self.core.onedrive_stat(path.as_str()).await?;
- let last_modified = decoded_response.last_modified_date_time;
- let date_utc_last_modified =
parse_datetime_from_rfc3339(&last_modified)?;
- meta.set_last_modified(date_utc_last_modified);
-
- meta.set_content_length(decoded_response.size);
-
- Ok(RpStat::new(meta))
- } else {
- Err(parse_error(resp))
- }
+ Ok(RpStat::new(meta))
}
async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead,
Self::Reader)> {
- let resp = self.onedrive_get_content(path, args.range()).await?;
-
- let status = resp.status();
+ let response = self.core.onedrive_get_content(path,
args.range()).await?;
+ let status = response.status();
match status {
StatusCode::OK | StatusCode::PARTIAL_CONTENT => {
- Ok((RpRead::default(), resp.into_body()))
+ Ok((RpRead::default(), response.into_body()))
}
_ => {
- let (part, mut body) = resp.into_parts();
+ let (part, mut body) = response.into_parts();
let buf = body.to_buffer().await?;
Err(parse_error(Response::from_parts(part, buf)))
}
@@ -181,192 +96,23 @@ impl Access for OnedriveBackend {
}
async fn write(&self, path: &str, args: OpWrite) -> Result<(RpWrite,
Self::Writer)> {
- let path = build_rooted_abs_path(&self.root, path);
+ let path = build_rooted_abs_path(&self.core.root, path);
Ok((
RpWrite::default(),
- oio::OneShotWriter::new(OneDriveWriter::new(self.clone(), args,
path)),
+ oio::OneShotWriter::new(OneDriveWriter::new(self.core.clone(),
args, path)),
))
}
async fn delete(&self) -> Result<(RpDelete, Self::Deleter)> {
Ok((
RpDelete::default(),
-
oio::OneShotDeleter::new(OnedriveDeleter::new(Arc::new(self.clone()))),
+ oio::OneShotDeleter::new(OneDriveDeleter::new(self.core.clone())),
))
}
- async fn list(&self, path: &str, _op_list: OpList) -> Result<(RpList,
Self::Lister)> {
- let l = OnedriveLister::new(self.root.clone(), path.into(),
self.clone());
-
+ async fn list(&self, path: &str, _args: OpList) -> Result<(RpList,
Self::Lister)> {
+ let l = OneDriveLister::new(path.to_string(), self.core.clone());
Ok((RpList::default(), oio::PageLister::new(l)))
}
}
-
-impl OnedriveBackend {
- pub(crate) const BASE_URL: &'static str =
"https://graph.microsoft.com/v1.0/me";
-
- async fn onedrive_get_stat(&self, path: &str) -> Result<Response<Buffer>> {
- let path = build_rooted_abs_path(&self.root, path);
- let url: String = format!(
- "https://graph.microsoft.com/v1.0/me/drive/root:{}{}",
- percent_encode_path(&path),
- ""
- );
-
- let mut req = Request::get(&url);
-
- let auth_header_content = format!("Bearer {}", self.access_token);
- req = req.header(header::AUTHORIZATION, auth_header_content);
-
- let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
-
- self.client.send(req).await
- }
-
- pub(crate) async fn onedrive_get_next_list_page(&self, url: &str) ->
Result<Response<Buffer>> {
- let mut req = Request::get(url);
-
- let auth_header_content = format!("Bearer {}", self.access_token);
- req = req.header(header::AUTHORIZATION, auth_header_content);
-
- let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
-
- self.client.send(req).await
- }
-
- pub async fn onedrive_get_content(
- &self,
- path: &str,
- range: BytesRange,
- ) -> Result<Response<HttpBody>> {
- let path = build_rooted_abs_path(&self.root, path);
- let url: String = format!(
- "https://graph.microsoft.com/v1.0/me/drive/root:{}{}",
- percent_encode_path(&path),
- ":/content"
- );
-
- let mut req = Request::get(&url).header(header::RANGE,
range.to_header());
-
- let auth_header_content = format!("Bearer {}", self.access_token);
- req = req.header(header::AUTHORIZATION, auth_header_content);
-
- let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
-
- self.client.fetch(req).await
- }
-
- pub async fn onedrive_upload_simple(
- &self,
- path: &str,
- size: Option<usize>,
- args: &OpWrite,
- body: Buffer,
- ) -> Result<Response<Buffer>> {
- let url = format!(
- "https://graph.microsoft.com/v1.0/me/drive/root:{}:/content",
- percent_encode_path(path)
- );
-
- let mut req = Request::put(&url);
-
- let auth_header_content = format!("Bearer {}", self.access_token);
- req = req.header(header::AUTHORIZATION, auth_header_content);
-
- if let Some(size) = size {
- req = req.header(header::CONTENT_LENGTH, size)
- }
-
- if let Some(mime) = args.content_type() {
- req = req.header(header::CONTENT_TYPE, mime)
- }
-
- let req = req.body(body).map_err(new_request_build_error)?;
-
- self.client.send(req).await
- }
-
- pub(crate) async fn onedrive_chunked_upload(
- &self,
- url: &str,
- args: &OpWrite,
- offset: usize,
- chunk_end: usize,
- total_len: usize,
- body: Buffer,
- ) -> Result<Response<Buffer>> {
- let mut req = Request::put(url);
-
- let auth_header_content = format!("Bearer {}", self.access_token);
- req = req.header(header::AUTHORIZATION, auth_header_content);
-
- let range = format!("bytes {}-{}/{}", offset, chunk_end, total_len);
- req = req.header("Content-Range".to_string(), range);
-
- let size = chunk_end - offset + 1;
- req = req.header(header::CONTENT_LENGTH, size.to_string());
-
- if let Some(mime) = args.content_type() {
- req = req.header(header::CONTENT_TYPE, mime)
- }
-
- let req = req.body(body).map_err(new_request_build_error)?;
-
- self.client.send(req).await
- }
-
- pub(crate) async fn onedrive_create_upload_session(
- &self,
- url: &str,
- body: OneDriveUploadSessionCreationRequestBody,
- ) -> Result<Response<Buffer>> {
- let mut req = Request::post(url);
-
- let auth_header_content = format!("Bearer {}", self.access_token);
- req = req.header(header::AUTHORIZATION, auth_header_content);
-
- req = req.header(header::CONTENT_TYPE, "application/json");
-
- let body_bytes =
serde_json::to_vec(&body).map_err(new_json_serialize_error)?;
- let asyn_body = Buffer::from(Bytes::from(body_bytes));
- let req = req.body(asyn_body).map_err(new_request_build_error)?;
-
- self.client.send(req).await
- }
-
- async fn onedrive_create_dir(
- &self,
- url: &str,
- body: CreateDirPayload,
- ) -> Result<Response<Buffer>> {
- let mut req = Request::post(url);
-
- let auth_header_content = format!("Bearer {}", self.access_token);
- req = req.header(header::AUTHORIZATION, auth_header_content);
- req = req.header(header::CONTENT_TYPE, "application/json");
-
- let body_bytes =
serde_json::to_vec(&body).map_err(new_json_serialize_error)?;
- let async_body = Buffer::from(bytes::Bytes::from(body_bytes));
- let req = req.body(async_body).map_err(new_request_build_error)?;
-
- self.client.send(req).await
- }
-
- pub(crate) async fn onedrive_delete(&self, path: &str) ->
Result<Response<Buffer>> {
- let path = build_abs_path(&self.root, path);
- let url = format!(
- "https://graph.microsoft.com/v1.0/me/drive/root:/{}",
- percent_encode_path(&path)
- );
-
- let mut req = Request::delete(&url);
-
- let auth_header_content = format!("Bearer {}", self.access_token);
- req = req.header(header::AUTHORIZATION, auth_header_content);
-
- let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
-
- self.client.send(req).await
- }
-}
diff --git a/core/src/services/onedrive/builder.rs
b/core/src/services/onedrive/builder.rs
index 9ee0eb0e9..42ed8b096 100644
--- a/core/src/services/onedrive/builder.rs
+++ b/core/src/services/onedrive/builder.rs
@@ -17,12 +17,20 @@
use std::fmt::Debug;
use std::fmt::Formatter;
+use std::sync::Arc;
+use chrono::DateTime;
+use chrono::Utc;
use log::debug;
+use services::onedrive::core::OneDriveCore;
+use services::onedrive::core::OneDriveSigner;
+
+use tokio::sync::Mutex;
use super::backend::OnedriveBackend;
use crate::raw::normalize_root;
use crate::raw::Access;
+use crate::raw::AccessorInfo;
use crate::raw::HttpClient;
use crate::services::OnedriveConfig;
use crate::Scheme;
@@ -38,7 +46,7 @@ impl Configurator for OnedriveConfig {
}
}
-/// [OneDrive](https://onedrive.com) backend support.
+/// Microsoft [OneDrive](https://onedrive.com) backend support.
#[doc = include_str!("docs.md")]
#[derive(Default)]
pub struct OnedriveBuilder {
@@ -55,14 +63,6 @@ impl Debug for OnedriveBuilder {
}
impl OnedriveBuilder {
- /// set the bearer access token for OneDrive
- ///
- /// default: no access token, which leads to failure
- pub fn access_token(mut self, access_token: &str) -> Self {
- self.config.access_token = Some(access_token.to_string());
- self
- }
-
/// Set root path of OneDrive folder.
pub fn root(mut self, root: &str) -> Self {
self.config.root = if root.is_empty() {
@@ -80,10 +80,55 @@ impl OnedriveBuilder {
///
/// This API is part of OpenDAL's Raw API. `HttpClient` could be changed
/// during minor updates.
+ #[deprecated(since = "0.53.0", note = "Use `Operator::update_http_client`
instead")]
+ #[allow(deprecated)]
pub fn http_client(mut self, http_client: HttpClient) -> Self {
self.http_client = Some(http_client);
self
}
+
+ /// Set the access token for a time limited access to Microsoft Graph API
(also OneDrive).
+ ///
+ /// Microsoft Graph API uses a typical OAuth 2.0 flow for authentication
and authorization.
+ /// You can get a access token from [Microsoft Graph
Explore](https://developer.microsoft.com/en-us/graph/graph-explorer).
+ ///
+ /// # Note
+ ///
+ /// - An access token is short-lived.
+ /// - Use a refresh_token if you want to use OneDrive API for an extended
period of time.
+ pub fn access_token(mut self, access_token: &str) -> Self {
+ self.config.access_token = Some(access_token.to_string());
+ self
+ }
+
+ /// Set the refresh token for long term access to Microsoft Graph API.
+ ///
+ /// OpenDAL will use a refresh token to maintain a fresh access token
automatically.
+ ///
+ /// # Note
+ ///
+ /// - A refresh token is available through a OAuth 2.0 flow, with an
additional scope `offline_access`.
+ pub fn refresh_token(mut self, refresh_token: &str) -> Self {
+ self.config.refresh_token = Some(refresh_token.to_string());
+ self
+ }
+
+ /// Set the client_id for a Microsoft Graph API application (available
though Azure's registration portal)
+ ///
+ /// Required when using the refresh token.
+ pub fn client_id(mut self, client_id: &str) -> Self {
+ self.config.client_id = Some(client_id.to_string());
+ self
+ }
+
+ /// Set the client_secret for a Microsoft Graph API application
+ ///
+ /// Required for Web app when using the refresh token.
+ /// Don't use a client secret when use in a native app since the native
app can't store the secret reliably.
+ pub fn client_secret(mut self, client_secret: &str) -> Self {
+ self.config.client_secret = Some(client_secret.to_string());
+ self
+ }
}
impl Builder for OnedriveBuilder {
@@ -94,18 +139,86 @@ impl Builder for OnedriveBuilder {
let root = normalize_root(&self.config.root.unwrap_or_default());
debug!("backend use root {}", root);
- let client = if let Some(client) = self.http_client {
- client
- } else {
- HttpClient::new().map_err(|err| {
- err.with_operation("Builder::build")
+ let info = AccessorInfo::default();
+ info.set_scheme(Scheme::Onedrive)
+ .set_root(&root)
+ .set_native_capability(Capability {
+ read: true,
+ write: true,
+
+ stat: true,
+ stat_has_content_length: true,
+ stat_has_etag: true,
+ stat_has_last_modified: true,
+
+ delete: true,
+ create_dir: true,
+
+ list: true,
+ list_has_content_length: true,
+ list_has_etag: true,
+ list_has_last_modified: true,
+
+ shared: true,
+
+ ..Default::default()
+ });
+
+ // allow deprecated api here for compatibility
+ #[allow(deprecated)]
+ if let Some(client) = self.http_client {
+ info.update_http_client(|_| client);
+ }
+
+ let accessor_info = Arc::new(info);
+ let mut signer = OneDriveSigner::new(accessor_info.clone());
+
+ // Requires OAuth 2.0 tokens:
+ // - `access_token` (the short-lived token)
+ // - `refresh_token` flow (the long term token)
+ // to be mutually exclusive for setting up for implementation
simplicity
+ match (self.config.access_token, self.config.refresh_token) {
+ (Some(access_token), None) => {
+ signer.access_token = access_token;
+ signer.expires_in = DateTime::<Utc>::MAX_UTC;
+ }
+ (None, Some(refresh_token)) => {
+ let client_id = self.config.client_id.ok_or_else(|| {
+ Error::new(
+ ErrorKind::ConfigInvalid,
+ "client_id must be set when refresh_token is set",
+ )
.with_context("service", Scheme::Onedrive)
- })?
+ })?;
+
+ signer.refresh_token = refresh_token;
+ signer.client_id = client_id;
+ if let Some(client_secret) = self.config.client_secret {
+ signer.client_secret = client_secret;
+ }
+ }
+ (Some(_), Some(_)) => {
+ return Err(Error::new(
+ ErrorKind::ConfigInvalid,
+ "access_token and refresh_token cannot be set at the same
time",
+ )
+ .with_context("service", Scheme::Onedrive))
+ }
+ (None, None) => {
+ return Err(Error::new(
+ ErrorKind::ConfigInvalid,
+ "access_token or refresh_token must be set",
+ )
+ .with_context("service", Scheme::Onedrive))
+ }
};
- match self.config.access_token.clone() {
- Some(access_token) => Ok(OnedriveBackend::new(root, access_token,
client)),
- None => Err(Error::new(ErrorKind::ConfigInvalid, "access_token not
set")),
- }
+ let core = Arc::new(OneDriveCore {
+ info: accessor_info,
+ root,
+ signer: Arc::new(Mutex::new(signer)),
+ });
+
+ Ok(OnedriveBackend { core })
}
}
diff --git a/core/src/services/onedrive/config.rs
b/core/src/services/onedrive/config.rs
index 79bfa6ee5..b4f5fcc03 100644
--- a/core/src/services/onedrive/config.rs
+++ b/core/src/services/onedrive/config.rs
@@ -26,10 +26,16 @@ use serde::Serialize;
#[serde(default)]
#[non_exhaustive]
pub struct OnedriveConfig {
- /// bearer access token for OneDrive
- pub access_token: Option<String>,
- /// root path of OneDrive folder.
+ /// The root path for the OneDrive service for the file access
pub root: Option<String>,
+ /// Microsoft Graph API (also OneDrive API) access token
+ pub access_token: Option<String>,
+ /// Microsoft Graph API (also OneDrive API) refresh token
+ pub refresh_token: Option<String>,
+ /// Microsoft Graph API Application (client) ID that is in the Azure's app
registration portal
+ pub client_id: Option<String>,
+ /// Microsoft Graph API Application client secret that is in the Azure's
app registration portal
+ pub client_secret: Option<String>,
}
impl Debug for OnedriveConfig {
diff --git a/core/src/services/onedrive/core.rs
b/core/src/services/onedrive/core.rs
new file mode 100644
index 000000000..35e54aae8
--- /dev/null
+++ b/core/src/services/onedrive/core.rs
@@ -0,0 +1,377 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::fmt::Debug;
+use std::fmt::Formatter;
+use std::sync::Arc;
+
+use bytes::Buf;
+use bytes::Bytes;
+use chrono::DateTime;
+use chrono::Utc;
+use http::header;
+use http::Request;
+use http::Response;
+
+use http::StatusCode;
+use tokio::sync::Mutex;
+
+use super::error::parse_error;
+use super::graph_model::CreateDirPayload;
+use super::graph_model::GraphOAuthRefreshTokenResponseBody;
+use super::graph_model::ItemType;
+use super::graph_model::OneDriveItem;
+use super::graph_model::OneDriveUploadSessionCreationRequestBody;
+use crate::raw::*;
+use crate::*;
+
+pub struct OneDriveCore {
+ pub info: Arc<AccessorInfo>,
+ pub root: String,
+ pub signer: Arc<Mutex<OneDriveSigner>>,
+}
+
+impl Debug for OneDriveCore {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("OneDriveCore")
+ .field("root", &self.root)
+ .finish_non_exhaustive()
+ }
+}
+
+// OneDrive returns 400 when try to access a dir with the POSIX special
directory entries
+const SPECIAL_POSIX_ENTRIES: [&str; 3] = [".", "/", ""];
+
+// OneDrive API parameters allows using with a parameter of:
+//
+// - ID
+// - file path
+//
+// `services-onedrive` uses the file path based API for simplicity.
+// Read more at
https://learn.microsoft.com/en-us/graph/onedrive-addressing-driveitems
+//
+// When debugging and running behavior tests against `services-onedrive`,
+// please try to keep the drive clean to reduce the likelihood of flaky
results.
+impl OneDriveCore {
+ // OneDrive personal's base URL. `me` is an alias that represents the
user's "Drive".
+ pub(crate) const DRIVE_ROOT_URL: &str =
"https://graph.microsoft.com/v1.0/me/drive/root";
+
+ /// Get a URL to an OneDrive item
+ ///
+ /// This function is useful for get an item and listing where OneDrive
requires a more precise file path.
+ pub(crate) fn onedrive_item_url(root: &str, path: &str) -> String {
+ // OneDrive requires the root to be the same as `DRIVE_ROOT_URL`.
+ // For files under the root, the URL pattern becomes
`https://graph.microsoft.com/v1.0/me/drive/root:<path>:`
+ if root == "/" && SPECIAL_POSIX_ENTRIES.contains(&path) {
+ Self::DRIVE_ROOT_URL.to_string()
+ } else {
+ // OneDrive returns 400 when try to access a folder with a ending
slash
+ let path = build_rooted_abs_path(root, path);
+ let path = path.strip_suffix('/').unwrap_or(path.as_str());
+ format!("{}:{}", Self::DRIVE_ROOT_URL, percent_encode_path(path))
+ }
+ }
+
+ pub(crate) async fn onedrive_stat(&self, path: &str) -> Result<Metadata> {
+ let response = self.onedrive_get_stat(path).await?;
+ let status = response.status();
+
+ if !status.is_success() {
+ return Err(parse_error(response));
+ }
+
+ let bytes = response.into_body();
+ let decoded_response: OneDriveItem =
+
serde_json::from_reader(bytes.reader()).map_err(new_json_deserialize_error)?;
+
+ let entry_mode: EntryMode = match decoded_response.item_type {
+ ItemType::Folder { .. } => EntryMode::DIR,
+ ItemType::File { .. } => EntryMode::FILE,
+ };
+
+ let mut meta = Metadata::new(entry_mode)
+ .with_etag(decoded_response.e_tag)
+ .with_content_length(decoded_response.size.max(0) as u64);
+
+ let last_modified = decoded_response.last_modified_date_time;
+ let date_utc_last_modified =
parse_datetime_from_rfc3339(&last_modified)?;
+ meta.set_last_modified(date_utc_last_modified);
+
+ Ok(meta)
+ }
+
+ pub(crate) async fn onedrive_get_stat(&self, path: &str) ->
Result<Response<Buffer>> {
+ let url: String = format!("{}:{}", Self::DRIVE_ROOT_URL,
percent_encode_path(path));
+
+ let mut request = Request::get(&url)
+ .body(Buffer::new())
+ .map_err(new_request_build_error)?;
+
+ self.sign(&mut request).await?;
+
+ self.info.http_client().send(request).await
+ }
+
+ pub(crate) async fn onedrive_get_next_list_page(&self, url: &str) ->
Result<Response<Buffer>> {
+ let mut request = Request::get(url)
+ .body(Buffer::new())
+ .map_err(new_request_build_error)?;
+
+ self.sign(&mut request).await?;
+
+ self.info.http_client().send(request).await
+ }
+
+ pub(crate) async fn onedrive_get_content(
+ &self,
+ path: &str,
+ range: BytesRange,
+ ) -> Result<Response<HttpBody>> {
+ let path = build_rooted_abs_path(&self.root, path);
+ let url: String = format!(
+ "{}:{}:/content",
+ Self::DRIVE_ROOT_URL,
+ percent_encode_path(&path),
+ );
+
+ let request = Request::get(&url).header(header::RANGE,
range.to_header());
+
+ let mut request = request
+ .body(Buffer::new())
+ .map_err(new_request_build_error)?;
+
+ self.sign(&mut request).await?;
+
+ self.info.http_client().fetch(request).await
+ }
+
+ pub async fn onedrive_upload_simple(
+ &self,
+ path: &str,
+ size: Option<usize>,
+ args: &OpWrite,
+ body: Buffer,
+ ) -> Result<Response<Buffer>> {
+ let url = format!(
+ "{}:{}:/content",
+ Self::DRIVE_ROOT_URL,
+ percent_encode_path(path)
+ );
+
+ let mut request = Request::put(&url);
+
+ if let Some(size) = size {
+ request = request.header(header::CONTENT_LENGTH, size)
+ }
+
+ if let Some(mime) = args.content_type() {
+ request = request.header(header::CONTENT_TYPE, mime)
+ }
+
+ let mut request = request.body(body).map_err(new_request_build_error)?;
+
+ self.sign(&mut request).await?;
+
+ self.info.http_client().send(request).await
+ }
+
+ pub(crate) async fn onedrive_chunked_upload(
+ &self,
+ url: &str,
+ args: &OpWrite,
+ offset: usize,
+ chunk_end: usize,
+ total_len: usize,
+ body: Buffer,
+ ) -> Result<Response<Buffer>> {
+ let mut request = Request::put(url);
+
+ let range = format!("bytes {}-{}/{}", offset, chunk_end, total_len);
+ request = request.header("Content-Range".to_string(), range);
+
+ let size = chunk_end - offset + 1;
+ request = request.header(header::CONTENT_LENGTH, size.to_string());
+
+ if let Some(mime) = args.content_type() {
+ request = request.header(header::CONTENT_TYPE, mime)
+ }
+
+ let mut request = request.body(body).map_err(new_request_build_error)?;
+
+ self.sign(&mut request).await?;
+
+ self.info.http_client().send(request).await
+ }
+
+ pub(crate) async fn onedrive_create_upload_session(
+ &self,
+ url: &str,
+ body: OneDriveUploadSessionCreationRequestBody,
+ ) -> Result<Response<Buffer>> {
+ let body_bytes =
serde_json::to_vec(&body).map_err(new_json_serialize_error)?;
+ let body = Buffer::from(Bytes::from(body_bytes));
+ let mut request = Request::post(url)
+ .header(header::CONTENT_TYPE, "application/json")
+ .body(body)
+ .map_err(new_request_build_error)?;
+
+ self.sign(&mut request).await?;
+
+ self.info.http_client().send(request).await
+ }
+
+ /// Create a directory
+ ///
+ /// When creates a folder, OneDrive returns a status code with 201.
+ /// When using `microsoft.graph.conflictBehavior=replace` to replace a
folder, OneDrive returns 200.
+ pub(crate) async fn onedrive_create_dir(&self, path: &str) ->
Result<Response<Buffer>> {
+ let path = build_rooted_abs_path(&self.root, path);
+ let path_before_last_slash = get_parent(&path);
+ let normalized = path_before_last_slash
+ .strip_suffix('/')
+ .unwrap_or(path_before_last_slash);
+ let encoded_path = percent_encode_path(normalized);
+
+ let url = format!("{}:{}:/children", Self::DRIVE_ROOT_URL,
encoded_path);
+
+ let folder_name = get_basename(&path);
+ let folder_name = folder_name.strip_suffix('/').unwrap_or(folder_name);
+
+ let payload = CreateDirPayload::new(folder_name.to_string());
+ let body_bytes =
serde_json::to_vec(&payload).map_err(new_json_serialize_error)?;
+ let body = Buffer::from(bytes::Bytes::from(body_bytes));
+
+ let mut request = Request::post(url)
+ .header(header::CONTENT_TYPE, "application/json")
+ .body(body)
+ .map_err(new_request_build_error)?;
+
+ self.sign(&mut request).await?;
+
+ self.info.http_client().send(request).await
+ }
+
+ pub(crate) async fn onedrive_delete(&self, path: &str) ->
Result<Response<Buffer>> {
+ let path = build_abs_path(&self.root, path);
+ let url = format!("{}:/{}:", Self::DRIVE_ROOT_URL,
percent_encode_path(&path));
+
+ let mut request = Request::delete(&url)
+ .body(Buffer::new())
+ .map_err(new_request_build_error)?;
+
+ self.sign(&mut request).await?;
+
+ self.info.http_client().send(request).await
+ }
+
+ pub async fn sign<T>(&self, request: &mut Request<T>) -> Result<()> {
+ let mut signer = self.signer.lock().await;
+ signer.sign(request).await
+ }
+}
+
+// keeps track of OAuth 2.0 tokens and refreshes the access token.
+pub struct OneDriveSigner {
+ pub info: Arc<AccessorInfo>, // to use `http_client`
+
+ pub client_id: String,
+ pub client_secret: String,
+ pub refresh_token: String,
+
+ pub access_token: String,
+ pub expires_in: DateTime<Utc>,
+}
+
+// OneDrive is part of Graph API hence shares the same authentication and
authorization processes.
+// `common` applies to account types:
+//
+// - consumers
+// - work and school account
+//
+// set to `common` for simplicity
+const ONEDRIVE_REFRESH_TOKEN: &str =
"https://login.microsoftonline.com/common/oauth2/v2.0/token";
+
+impl OneDriveSigner {
+ pub fn new(info: Arc<AccessorInfo>) -> Self {
+ OneDriveSigner {
+ info,
+
+ client_id: "".to_string(),
+ client_secret: "".to_string(),
+ refresh_token: "".to_string(),
+ access_token: "".to_string(),
+ expires_in: DateTime::<Utc>::MIN_UTC,
+ }
+ }
+
+ async fn refresh_tokens(&mut self) -> Result<()> {
+ // OneDrive users must provide at least this required permission scope
+ let encoded_payload = format!(
+
"client_id={}&client_secret={}&scope=Files.ReadWrite&refresh_token={}&grant_type=refresh_token",
+ percent_encode_path(self.client_id.as_str()),
+ percent_encode_path(self.client_secret.as_str()),
+ percent_encode_path(self.refresh_token.as_str())
+ );
+ let request = Request::post(ONEDRIVE_REFRESH_TOKEN)
+ .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
+ .body(Buffer::from(encoded_payload))
+ .map_err(new_request_build_error)?;
+
+ let response = self.info.http_client().send(request).await?;
+ let status = response.status();
+ match status {
+ StatusCode::OK => {
+ let resp_body = response.into_body();
+ let data: GraphOAuthRefreshTokenResponseBody =
+ serde_json::from_reader(resp_body.reader())
+ .map_err(new_json_deserialize_error)?;
+ self.access_token = data.access_token;
+ self.refresh_token = data.refresh_token;
+ self.expires_in = Utc::now()
+ + chrono::TimeDelta::try_seconds(data.expires_in)
+ .expect("expires_in must be valid seconds")
+ - chrono::TimeDelta::minutes(2); // assumes 2 mins
graceful transmission for implementation simplicity
+ Ok(())
+ }
+ _ => Err(parse_error(response)),
+ }
+ }
+
+ /// Sign a request.
+ pub async fn sign<T>(&mut self, request: &mut Request<T>) -> Result<()> {
+ if !self.access_token.is_empty() && self.expires_in > Utc::now() {
+ let value = format!("Bearer {}", self.access_token)
+ .parse()
+ .expect("access_token must be valid header value");
+
+ request.headers_mut().insert(header::AUTHORIZATION, value);
+ return Ok(());
+ }
+
+ self.refresh_tokens().await?;
+
+ let auth_header_content = format!("Bearer {}", self.access_token)
+ .parse()
+ .expect("Fetched access_token is invalid as a header value");
+
+ request
+ .headers_mut()
+ .insert(header::AUTHORIZATION, auth_header_content);
+
+ Ok(())
+ }
+}
diff --git a/core/src/services/onedrive/delete.rs
b/core/src/services/onedrive/delete.rs
index 1e4dcdd36..2a12f605c 100644
--- a/core/src/services/onedrive/delete.rs
+++ b/core/src/services/onedrive/delete.rs
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-use super::backend::OnedriveBackend;
+use super::core::OneDriveCore;
use super::error::parse_error;
use crate::raw::*;
use crate::*;
@@ -24,25 +24,25 @@ use std::sync::Arc;
/// Delete operation
/// Documentation:
https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delete?view=odsp-graph-online
-pub struct OnedriveDeleter {
- core: Arc<OnedriveBackend>,
+pub struct OneDriveDeleter {
+ core: Arc<OneDriveCore>,
}
-impl OnedriveDeleter {
- pub fn new(core: Arc<OnedriveBackend>) -> Self {
+impl OneDriveDeleter {
+ pub fn new(core: Arc<OneDriveCore>) -> Self {
Self { core }
}
}
-impl oio::OneShotDelete for OnedriveDeleter {
+impl oio::OneShotDelete for OneDriveDeleter {
async fn delete_once(&self, path: String, _: OpDelete) -> Result<()> {
- let resp = self.core.onedrive_delete(&path).await?;
+ let response = self.core.onedrive_delete(&path).await?;
- let status = resp.status();
+ let status = response.status();
match status {
StatusCode::NO_CONTENT | StatusCode::NOT_FOUND => Ok(()),
- _ => Err(parse_error(resp)),
+ _ => Err(parse_error(response)),
}
}
}
diff --git a/core/src/services/onedrive/docs.md
b/core/src/services/onedrive/docs.md
index f4a0f8992..24feabe74 100644
--- a/core/src/services/onedrive/docs.md
+++ b/core/src/services/onedrive/docs.md
@@ -12,14 +12,17 @@ This service can be used to:
## Notes
-Currently, only OneDrive Personal is supported.
+Currently, OpenDAL supports OneDrive Personal only.
## Configuration
-- `access_token`: set the access_token for Graph API
-- `root`: Set the work directory for backend
+- `access_token`: set a short-live access token for Microsoft Graph API (also,
OneDrive API)
+- `refresh_token`: set a long term access token for Microsoft Graph API
+- `client_id`: set the client ID for a Microsoft Graph API application
(available though Azure's registration portal)
+- `client_secret`: set the client secret for a Microsoft Graph API application
+- `root`: Set the work directory for OneDrive backend
-You can refer to [`OnedriveBuilder`]'s docs for more information
+Read more at [`OnedriveBuilder`].
## Example
diff --git a/core/src/services/onedrive/error.rs
b/core/src/services/onedrive/error.rs
index 8564eedc6..22073aaf8 100644
--- a/core/src/services/onedrive/error.rs
+++ b/core/src/services/onedrive/error.rs
@@ -22,12 +22,13 @@ use crate::raw::*;
use crate::*;
/// Parse error response into Error.
-pub(super) fn parse_error(resp: Response<Buffer>) -> Error {
- let (parts, body) = resp.into_parts();
+pub(super) fn parse_error(response: Response<Buffer>) -> Error {
+ let (parts, body) = response.into_parts();
let bs = body.to_bytes();
let (kind, retryable) = match parts.status {
StatusCode::NOT_FOUND => (ErrorKind::NotFound, false),
+ StatusCode::CONFLICT => (ErrorKind::AlreadyExists, false),
StatusCode::FORBIDDEN => (ErrorKind::PermissionDenied, false),
StatusCode::INTERNAL_SERVER_ERROR
| StatusCode::BAD_GATEWAY
diff --git a/core/src/services/onedrive/graph_model.rs
b/core/src/services/onedrive/graph_model.rs
index 95561c589..1635b1c51 100644
--- a/core/src/services/onedrive/graph_model.rs
+++ b/core/src/services/onedrive/graph_model.rs
@@ -20,8 +20,15 @@ use std::collections::HashMap;
use serde::Deserialize;
use serde::Serialize;
+#[derive(Debug, Deserialize)]
+pub struct GraphOAuthRefreshTokenResponseBody {
+ pub access_token: String,
+ pub refresh_token: String,
+ pub expires_in: i64, // in seconds
+}
+
#[derive(Debug, Serialize, Deserialize)]
-pub struct GraphApiOnedriveListResponse {
+pub struct GraphApiOneDriveListResponse {
#[serde(rename = "@odata.nextLink")]
pub next_link: Option<String>,
pub value: Vec<OneDriveItem>,
@@ -30,20 +37,13 @@ pub struct GraphApiOnedriveListResponse {
/// mapping for a DriveItem representation
/// read more at
https://learn.microsoft.com/en-us/onedrive/developer/rest-api/resources/driveitem
#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
pub struct OneDriveItem {
pub name: String,
-
- #[serde(rename = "lastModifiedDateTime")]
pub last_modified_date_time: String,
-
- #[serde(rename = "eTag")]
pub e_tag: String,
-
pub size: i64,
-
- #[serde(rename = "parentReference")]
pub parent_reference: ParentReference,
-
#[serde(flatten)]
pub item_type: ItemType,
}
@@ -70,36 +70,24 @@ pub enum ItemType {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub struct OnedriveGetItemBody {
- #[serde(rename = "cTag")]
- pub(crate) c_tag: String,
- #[serde(rename = "eTag")]
- pub(crate) e_tag: String,
- id: String,
- #[serde(rename = "lastModifiedDateTime")]
- pub(crate) last_modified_date_time: String,
- pub(crate) name: String,
- pub(crate) size: u64,
-
- #[serde(flatten)]
- pub(crate) item_type: ItemType,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
pub struct File {
- #[serde(rename = "mimeType")]
mime_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
pub struct Folder {
- #[serde(rename = "childCount")]
child_count: i64,
}
+// Microsoft's documentation wants developers to set this as URL parameters.
+// If we follow the documentation, we can't replace the directory (409 existed
error).
+// Though, even with this declaration, behavior tests show **flaky behavior**.
+const REPLACE_EXISTING_ITEM_WHEN_CONFLICT: &str = "replace";
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateDirPayload {
- // folder: String,
#[serde(rename = "@microsoft.graph.conflictBehavior")]
conflict_behavior: String,
name: String,
@@ -109,7 +97,7 @@ pub struct CreateDirPayload {
impl CreateDirPayload {
pub fn new(name: String) -> Self {
Self {
- conflict_behavior: "replace".to_string(),
+ conflict_behavior: REPLACE_EXISTING_ITEM_WHEN_CONFLICT.to_string(),
name,
folder: EmptyStruct {},
}
@@ -122,15 +110,14 @@ struct EmptyStruct {}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct FileUploadItem {
#[serde(rename = "@microsoft.graph.conflictBehavior")]
- microsoft_graph_conflict_behavior: String,
+ conflict_behavior: String,
name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
pub struct OneDriveUploadSessionCreationResponseBody {
- #[serde(rename = "uploadUrl")]
pub upload_url: String,
- #[serde(rename = "expirationDateTime")]
pub expiration_date_time: String,
}
@@ -143,7 +130,7 @@ impl OneDriveUploadSessionCreationRequestBody {
pub fn new(path: String) -> Self {
OneDriveUploadSessionCreationRequestBody {
item: FileUploadItem {
- microsoft_graph_conflict_behavior: "replace".to_string(),
+ conflict_behavior:
REPLACE_EXISTING_ITEM_WHEN_CONFLICT.to_string(),
name: path,
},
}
@@ -224,7 +211,7 @@ fn test_parse_one_drive_json() {
]
}"#;
- let response: GraphApiOnedriveListResponse =
serde_json::from_str(data).unwrap();
+ let response: GraphApiOneDriveListResponse =
serde_json::from_str(data).unwrap();
assert_eq!(response.value.len(), 2);
let item = &response.value[0];
assert_eq!(item.name, "name");
@@ -290,7 +277,7 @@ fn test_parse_folder_single() {
]
}"#;
- let response: GraphApiOnedriveListResponse =
serde_json::from_str(response_json).unwrap();
+ let response: GraphApiOneDriveListResponse =
serde_json::from_str(response_json).unwrap();
assert_eq!(response.value.len(), 1);
let item = &response.value[0];
if let ItemType::Folder { folder, .. } = &item.item_type {
diff --git a/core/src/services/onedrive/lister.rs
b/core/src/services/onedrive/lister.rs
index 8d030a311..314c24548 100644
--- a/core/src/services/onedrive/lister.rs
+++ b/core/src/services/onedrive/lister.rs
@@ -15,76 +15,68 @@
// specific language governing permissions and limitations
// under the License.
+use std::sync::Arc;
+
use bytes::Buf;
-use super::backend::OnedriveBackend;
+use super::core::OneDriveCore;
use super::error::parse_error;
-use super::graph_model::GraphApiOnedriveListResponse;
+use super::graph_model::GraphApiOneDriveListResponse;
use super::graph_model::ItemType;
use crate::raw::oio;
use crate::raw::*;
use crate::*;
-pub struct OnedriveLister {
- root: String,
+pub struct OneDriveLister {
+ core: Arc<OneDriveCore>,
path: String,
- backend: OnedriveBackend,
}
-impl OnedriveLister {
+impl OneDriveLister {
const DRIVE_ROOT_PREFIX: &'static str = "/drive/root:";
- pub(crate) fn new(root: String, path: String, backend: OnedriveBackend) ->
Self {
- Self {
- root,
- path,
- backend,
- }
+ pub(crate) fn new(path: String, core: Arc<OneDriveCore>) -> Self {
+ Self { core, path }
}
}
-impl oio::PageList for OnedriveLister {
+impl oio::PageList for OneDriveLister {
async fn next_page(&self, ctx: &mut oio::PageContext) -> Result<()> {
let request_url = if ctx.token.is_empty() {
- let path = build_rooted_abs_path(&self.root, &self.path);
- let url: String = if path == "." || path == "/" {
-
"https://graph.microsoft.com/v1.0/me/drive/root/children".to_string()
- } else {
- format!(
-
"https://graph.microsoft.com/v1.0/me/drive/root:/{}:/children",
- percent_encode_path(&path),
- )
- };
- url
+ format!(
+ "{}:/children",
+ OneDriveCore::onedrive_item_url(&self.core.root, &self.path)
+ )
} else {
ctx.token.clone()
};
- let resp = self
- .backend
- .onedrive_get_next_list_page(&request_url)
- .await?;
+ let response =
self.core.onedrive_get_next_list_page(&request_url).await?;
- let status_code = resp.status();
+ let status_code = response.status();
if !status_code.is_success() {
if status_code == http::StatusCode::NOT_FOUND {
ctx.done = true;
return Ok(());
}
- let error = parse_error(resp);
+ let error = parse_error(response);
return Err(error);
}
- let bytes = resp.into_body();
- let decoded_response: GraphApiOnedriveListResponse =
+ let bytes = response.into_body();
+ let decoded_response: GraphApiOneDriveListResponse =
serde_json::from_reader(bytes.reader()).map_err(new_json_deserialize_error)?;
// Include the current directory itself when handling the first page
of the listing.
if ctx.token.is_empty() && !ctx.done {
- let path = build_abs_path(&self.root, self.path.as_str());
- let path = build_rel_path(&self.root, &path);
- let e = oio::Entry::new(&path, Metadata::new(EntryMode::DIR));
- ctx.entries.push_back(e);
+ let path = self.path.clone();
//build_rooted_abs_path(&self.core.root, &self.path);
+ let full_path = build_rooted_abs_path(&self.core.root, &self.path);
+ let trimmed_full_path =
full_path.strip_suffix('/').unwrap_or(&full_path);
+ // TODO: when listing a directory directly, we could reuse the
stat result,
+ // cache the result when listing nested directory
+ let meta = self.core.onedrive_stat(trimmed_full_path).await?;
+ let entry = oio::Entry::new(&path, meta);
+ ctx.entries.push_back(entry);
}
if let Some(next_link) = decoded_response.next_link {
@@ -101,13 +93,13 @@ impl oio::PageList for OnedriveLister {
.unwrap_or("");
let path = format!("{}/{}", parent_path, name);
- let mut normalized_path = build_rel_path(&self.root, &path);
+ let mut normalized_path = build_rel_path(self.core.root.as_str(),
path.as_str());
let entry_mode = match drive_item.item_type {
ItemType::Folder { .. } => EntryMode::DIR,
ItemType::File { .. } => EntryMode::FILE,
};
- // OneDrive returns the folder without the trailing `/`
+ // Add the trailing `/` because OneDrive returns a directory with
the name
if entry_mode == EntryMode::DIR {
normalized_path.push('/');
}
diff --git a/core/src/services/onedrive/mod.rs
b/core/src/services/onedrive/mod.rs
index cbe3a7ce0..fadcfc9cb 100644
--- a/core/src/services/onedrive/mod.rs
+++ b/core/src/services/onedrive/mod.rs
@@ -18,6 +18,8 @@
#[cfg(feature = "services-onedrive")]
mod backend;
#[cfg(feature = "services-onedrive")]
+mod core;
+#[cfg(feature = "services-onedrive")]
mod delete;
#[cfg(feature = "services-onedrive")]
mod error;
diff --git a/core/src/services/onedrive/writer.rs
b/core/src/services/onedrive/writer.rs
index 4a29d90d2..d18a5d507 100644
--- a/core/src/services/onedrive/writer.rs
+++ b/core/src/services/onedrive/writer.rs
@@ -15,11 +15,13 @@
// specific language governing permissions and limitations
// under the License.
+use std::sync::Arc;
+
use bytes::Buf;
use bytes::Bytes;
use http::StatusCode;
-use super::backend::OnedriveBackend;
+use super::core::OneDriveCore;
use super::error::parse_error;
use super::graph_model::OneDriveUploadSessionCreationRequestBody;
use super::graph_model::OneDriveUploadSessionCreationResponseBody;
@@ -27,7 +29,7 @@ use crate::raw::*;
use crate::*;
pub struct OneDriveWriter {
- backend: OnedriveBackend,
+ core: Arc<OneDriveCore>,
op: OpWrite,
path: String,
@@ -38,8 +40,8 @@ impl OneDriveWriter {
// If your app splits a file into multiple byte ranges, the size of each
byte range MUST be a multiple of 320 KiB (327,680 bytes). Using a fragment size
that does not divide evenly by 320 KiB will result in errors committing some
files.
//
https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online#upload-bytes-to-the-upload-session
const CHUNK_SIZE_FACTOR: usize = 327_680;
- pub fn new(backend: OnedriveBackend, op: OpWrite, path: String) -> Self {
- OneDriveWriter { backend, op, path }
+ pub fn new(core: Arc<OneDriveCore>, op: OpWrite, path: String) -> Self {
+ OneDriveWriter { core, op, path }
}
}
@@ -59,18 +61,18 @@ impl oio::OneShotWrite for OneDriveWriter {
impl OneDriveWriter {
async fn write_simple(&self, bs: Buffer) -> Result<()> {
- let resp = self
- .backend
+ let response = self
+ .core
.onedrive_upload_simple(&self.path, Some(bs.len()), &self.op, bs)
.await?;
- let status = resp.status();
+ let status = response.status();
match status {
// Typical response code: 201 Created
// Reference:
https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content?view=odsp-graph-online#response
StatusCode::CREATED | StatusCode::OK => Ok(()),
- _ => Err(parse_error(resp)),
+ _ => Err(parse_error(response)),
}
}
@@ -94,8 +96,8 @@ impl OneDriveWriter {
let total_len = total_bytes.len();
let chunk_end = end - 1;
- let resp = self
- .backend
+ let response = self
+ .core
.onedrive_chunked_upload(
&session_response.upload_url,
&OpWrite::default(),
@@ -106,13 +108,13 @@ impl OneDriveWriter {
)
.await?;
- let status = resp.status();
+ let status = response.status();
match status {
// Typical response code: 202 Accepted
// Reference:
https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content?view=odsp-graph-online#response
StatusCode::ACCEPTED | StatusCode::CREATED | StatusCode::OK =>
{}
- _ => return Err(parse_error(resp)),
+ _ => return Err(parse_error(response)),
}
offset += OneDriveWriter::CHUNK_SIZE_FACTOR;
@@ -129,28 +131,25 @@ impl OneDriveWriter {
)
})?;
let url = format!(
- "{}/drive/root:{}:/createUploadSession",
- OnedriveBackend::BASE_URL,
+ "{}:{}:/createUploadSession",
+ OneDriveCore::DRIVE_ROOT_URL,
percent_encode_path(&self.path)
);
let body =
OneDriveUploadSessionCreationRequestBody::new(file_name_from_path.to_string());
- let resp = self
- .backend
- .onedrive_create_upload_session(&url, body)
- .await?;
+ let response = self.core.onedrive_create_upload_session(&url,
body).await?;
- let status = resp.status();
+ let status = response.status();
match status {
// Reference:
https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online#response
StatusCode::OK => {
- let bs = resp.into_body();
+ let bs = response.into_body();
let result: OneDriveUploadSessionCreationResponseBody =
serde_json::from_reader(bs.reader()).map_err(new_json_deserialize_error)?;
Ok(result)
}
- _ => Err(parse_error(resp)),
+ _ => Err(parse_error(response)),
}
}
}