-
Notifications
You must be signed in to change notification settings - Fork 703
feat(icloud): Add basic Apple iCloud Drive support #3980
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
5747e1e
feat:Add Apple iCloud Drive support
bokket 24a835d
cargo fmt
bokket 47d8310
Merge branch 'main' of github.com:apache/incubator-opendal into main
bokket 5b37b5a
add some docs
bokket 5a621f4
cargo fmt
bokket 4df6b11
add response json test
bokket dad592d
fmt
bokket 230876d
modify typos.toml
bokket 2f25383
add env.example region
bokket 45ad927
Merge branch 'main' of github.com:apache/incubator-opendal into main
bokket d1711a6
use is_china_mainland instead region
bokket d000f5b
fmt
bokket e419146
make clippy happy
bokket b75b3bd
Merge branch 'main' of github.com:apache/incubator-opendal into main
bokket 131cd41
Merge branch 'main' of github.com:apache/incubator-opendal into main
bokket 6c94ae9
refactor path_cache trait
bokket 8fe102c
refactor dir struct
bokket ef4fee5
fmt
bokket 40fc0a5
remove Useless comment
bokket 1fdb8d0
Merge branch 'main' of github.com:apache/incubator-opendal into main
bokket 572d7c9
delete dead code
bokket 9ce244e
merge upstream
bokket 8ce7511
fix conflicts
bokket File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,330 @@ | ||
| // 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 async_trait::async_trait; | ||
| use http::StatusCode; | ||
| use serde::Deserialize; | ||
| use std::collections::HashMap; | ||
| use std::fmt::{Debug, Formatter}; | ||
| use std::sync::Arc; | ||
| use tokio::sync::Mutex; | ||
|
|
||
| use crate::raw::*; | ||
| use crate::*; | ||
| use crate::{Capability, Scheme}; | ||
|
|
||
| use super::core::{parse_error, IcloudCore, IcloudPathQuery, IcloudSigner, SessionData}; | ||
|
|
||
| /// Config for icloud services support. | ||
| #[derive(Default, Deserialize)] | ||
| #[serde(default)] | ||
| #[non_exhaustive] | ||
| pub struct IcloudConfig { | ||
| /// root of this backend. | ||
| /// | ||
| /// All operations will happen under this root. | ||
| /// | ||
| /// default to `/` if not set. | ||
| pub root: Option<String>, | ||
| /// apple_id of this backend. | ||
| /// | ||
| /// apple_id must be full, mostly like `[email protected]`. | ||
| pub apple_id: Option<String>, | ||
| /// password of this backend. | ||
| /// | ||
| /// password must be full. | ||
| pub password: Option<String>, | ||
|
|
||
| /// Session | ||
| /// | ||
| /// token must be valid. | ||
| pub trust_token: Option<String>, | ||
| pub ds_web_auth_token: Option<String>, | ||
| /// enable the china origin | ||
| /// China region `origin` Header needs to be set to "https://www.icloud.com.cn". | ||
| /// | ||
| /// otherwise Apple server will return 302. | ||
| pub is_china_mainland: bool, | ||
| } | ||
|
|
||
| impl Debug for IcloudConfig { | ||
| fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||
| let mut d = f.debug_struct("IcloudBuilder"); | ||
| d.field("root", &self.root); | ||
| d.field("is_china_mainland", &self.is_china_mainland); | ||
| d.finish_non_exhaustive() | ||
| } | ||
| } | ||
|
|
||
| /// [IcloudDrive](https://www.icloud.com/iclouddrive/) service support. | ||
| #[doc = include_str!("docs.md")] | ||
| #[derive(Default)] | ||
| pub struct IcloudBuilder { | ||
| /// icloud config for web session request | ||
| pub config: IcloudConfig, | ||
| /// Specify the http client that used by this service. | ||
| /// | ||
| /// # Notes | ||
| /// | ||
| /// This API is part of OpenDAL's Raw API. `HttpClient` could be changed | ||
| /// during minor updates. | ||
| pub http_client: Option<HttpClient>, | ||
| } | ||
|
|
||
| impl Debug for IcloudBuilder { | ||
| fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||
| let mut d = f.debug_struct("IcloudBuilder"); | ||
|
|
||
| d.field("config", &self.config); | ||
| d.finish_non_exhaustive() | ||
| } | ||
| } | ||
|
|
||
| impl IcloudBuilder { | ||
| /// Set root of this backend. | ||
| /// | ||
| /// All operations will happen under this root. | ||
| pub fn root(&mut self, root: &str) -> &mut Self { | ||
| self.config.root = if root.is_empty() { | ||
| None | ||
| } else { | ||
| Some(root.to_string()) | ||
| }; | ||
|
|
||
| self | ||
| } | ||
|
|
||
| /// Your Apple id | ||
| /// | ||
| /// It is required. your Apple login email, e.g. `[email protected]` | ||
| pub fn apple_id(&mut self, apple_id: &str) -> &mut Self { | ||
| self.config.apple_id = if apple_id.is_empty() { | ||
| None | ||
| } else { | ||
| Some(apple_id.to_string()) | ||
| }; | ||
|
|
||
| self | ||
| } | ||
|
|
||
| /// Your Apple id password | ||
| /// | ||
| /// It is required. your icloud login password, e.g. `password` | ||
| pub fn password(&mut self, password: &str) -> &mut Self { | ||
| self.config.password = if password.is_empty() { | ||
| None | ||
| } else { | ||
| Some(password.to_string()) | ||
| }; | ||
|
|
||
| self | ||
| } | ||
|
|
||
| /// Trust token and ds_web_auth_token is used for temporary access to the icloudDrive API. | ||
| /// | ||
| /// Authenticate using session token | ||
| pub fn trust_token(&mut self, trust_token: &str) -> &mut Self { | ||
Xuanwo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self.config.trust_token = if trust_token.is_empty() { | ||
| None | ||
| } else { | ||
| Some(trust_token.to_string()) | ||
| }; | ||
|
|
||
| self | ||
| } | ||
| /// ds_web_auth_token must be set in Session | ||
| /// | ||
| /// Avoid Two Factor Authentication | ||
| pub fn ds_web_auth_token(&mut self, ds_web_auth_token: &str) -> &mut Self { | ||
| self.config.ds_web_auth_token = if ds_web_auth_token.is_empty() { | ||
| None | ||
| } else { | ||
| Some(ds_web_auth_token.to_string()) | ||
| }; | ||
|
|
||
| self | ||
| } | ||
| /// Enable the china origin | ||
| /// For China, use "https://www.icloud.com.cn" | ||
| /// For Other region, use "https://www.icloud.com" | ||
| pub fn is_china_mainland(&mut self, is_china_mainland: bool) -> &mut Self { | ||
| self.config.is_china_mainland = is_china_mainland; | ||
| self | ||
| } | ||
|
|
||
| /// Specify the http client that used by this service. | ||
| /// | ||
| /// # Notes | ||
| /// | ||
| /// This API is part of OpenDAL's Raw API. `HttpClient` could be changed | ||
| /// during minor updates. | ||
| pub fn http_client(&mut self, client: HttpClient) -> &mut Self { | ||
| self.http_client = Some(client); | ||
| self | ||
| } | ||
| } | ||
|
|
||
| impl Builder for IcloudBuilder { | ||
| const SCHEME: Scheme = Scheme::Icloud; | ||
| type Accessor = IcloudBackend; | ||
|
|
||
| fn from_map(map: HashMap<String, String>) -> Self { | ||
| let config = IcloudConfig::deserialize(ConfigDeserializer::new(map)) | ||
| .expect("config deserialize must succeed"); | ||
| IcloudBuilder { | ||
| config, | ||
| http_client: None, | ||
| } | ||
| } | ||
|
|
||
| fn build(&mut self) -> Result<Self::Accessor> { | ||
| let root = normalize_root(&self.config.root.take().unwrap_or_default()); | ||
|
|
||
| let apple_id = match &self.config.apple_id { | ||
| Some(apple_id) => Ok(apple_id.clone()), | ||
| None => Err(Error::new(ErrorKind::ConfigInvalid, "apple_id is empty") | ||
| .with_operation("Builder::build") | ||
| .with_context("service", Scheme::Icloud)), | ||
| }?; | ||
|
|
||
| let password = match &self.config.password { | ||
| Some(password) => Ok(password.clone()), | ||
| None => Err(Error::new(ErrorKind::ConfigInvalid, "password is empty") | ||
| .with_operation("Builder::build") | ||
| .with_context("service", Scheme::Icloud)), | ||
| }?; | ||
|
|
||
| let ds_web_auth_token = match &self.config.ds_web_auth_token { | ||
| Some(ds_web_auth_token) => Ok(ds_web_auth_token.clone()), | ||
| None => Err( | ||
| Error::new(ErrorKind::ConfigInvalid, "ds_web_auth_token is empty") | ||
| .with_operation("Builder::build") | ||
| .with_context("service", Scheme::Icloud), | ||
| ), | ||
| }?; | ||
|
|
||
| let trust_token = match &self.config.trust_token { | ||
| Some(trust_token) => Ok(trust_token.clone()), | ||
| None => Err(Error::new(ErrorKind::ConfigInvalid, "trust_token is empty") | ||
| .with_operation("Builder::build") | ||
| .with_context("service", Scheme::Icloud)), | ||
| }?; | ||
|
|
||
| let client = if let Some(client) = self.http_client.take() { | ||
| client | ||
| } else { | ||
| HttpClient::new().map_err(|err| { | ||
| err.with_operation("Builder::build") | ||
| .with_context("service", Scheme::Icloud) | ||
| })? | ||
| }; | ||
|
|
||
| let session_data = SessionData::new(); | ||
|
|
||
| let signer = IcloudSigner { | ||
| client: client.clone(), | ||
| data: session_data, | ||
| apple_id, | ||
| password, | ||
| trust_token: Some(trust_token), | ||
| ds_web_auth_token: Some(ds_web_auth_token), | ||
| is_china_mainland: self.config.is_china_mainland, | ||
| }; | ||
|
|
||
| let signer = Arc::new(Mutex::new(signer)); | ||
| Ok(IcloudBackend { | ||
| core: Arc::new(IcloudCore { | ||
| signer: signer.clone(), | ||
| root, | ||
| path_cache: PathCacher::new(IcloudPathQuery::new(client, signer.clone())), | ||
| }), | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| #[derive(Debug, Clone)] | ||
| pub struct IcloudBackend { | ||
| core: Arc<IcloudCore>, | ||
| } | ||
|
|
||
| #[async_trait] | ||
| impl Accessor for IcloudBackend { | ||
| type Reader = IncomingAsyncBody; | ||
| type BlockingReader = (); | ||
| type Writer = (); | ||
| type BlockingWriter = (); | ||
| type Lister = (); | ||
| type BlockingLister = (); | ||
|
|
||
| fn info(&self) -> AccessorInfo { | ||
| let mut ma = AccessorInfo::default(); | ||
| ma.set_scheme(Scheme::Icloud) | ||
| .set_root(&self.core.root) | ||
| .set_native_capability(Capability { | ||
| stat: true, | ||
| read: true, | ||
| ..Default::default() | ||
| }); | ||
| ma | ||
| } | ||
|
|
||
| async fn stat(&self, path: &str, _: OpStat) -> Result<RpStat> { | ||
| // icloud get the filename by id, instead obtain the metadata by filename | ||
| if path == "/" { | ||
| return Ok(RpStat::new(Metadata::new(EntryMode::DIR))); | ||
| } | ||
|
|
||
| let node = self.core.stat(path).await?; | ||
|
|
||
| let mut meta = Metadata::new(match node.type_field.as_str() { | ||
| "FOLDER" => EntryMode::DIR, | ||
| _ => EntryMode::FILE, | ||
| }); | ||
|
|
||
| if meta.mode() == EntryMode::DIR || path.ends_with('/') { | ||
| return Ok(RpStat::new(Metadata::new(EntryMode::DIR))); | ||
| } | ||
|
|
||
| meta = meta.with_content_length(node.size); | ||
|
|
||
| let last_modified = parse_datetime_from_rfc3339(&node.date_modified)?; | ||
| meta = meta.with_last_modified(last_modified); | ||
|
|
||
| Ok(RpStat::new(meta)) | ||
| } | ||
|
|
||
| async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> { | ||
| let resp = self.core.read(path, &args).await?; | ||
| let status = resp.status(); | ||
|
|
||
| match status { | ||
| StatusCode::OK | StatusCode::PARTIAL_CONTENT => { | ||
| let size = parse_content_length(resp.headers())?; | ||
| let range = parse_content_range(resp.headers())?; | ||
| Ok(( | ||
| RpRead::new().with_size(size).with_range(range), | ||
| resp.into_body(), | ||
| )) | ||
| } | ||
| StatusCode::RANGE_NOT_SATISFIABLE => { | ||
| resp.into_body().consume().await?; | ||
| Ok((RpRead::new().with_size(Some(0)), IncomingAsyncBody::empty())) | ||
| } | ||
| _ => Err(parse_error(resp).await?), | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.