├── .gitignore ├── clippy.toml ├── shell.nix ├── onedrive-api-test ├── Cargo.toml ├── README.md ├── tests │ └── real_test │ │ ├── util.rs │ │ └── main.rs └── src │ └── main.rs ├── .github └── workflows │ ├── future_proof.yaml │ ├── real_test.yaml │ └── ci.yaml ├── README.md ├── LICENSE-MIT ├── Cargo.toml ├── src ├── error.rs ├── lib.rs ├── util.rs ├── option.rs ├── resource.rs ├── auth.rs └── onedrive.rs ├── CHANGELOG.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | /.vscode 4 | 5 | .env* 6 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | doc-valid-idents = ["OAuth2", "OData", "OneDrive", ".."] 2 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | pkgs.mkShell { 3 | buildInputs = [ 4 | pkg-config 5 | openssl 6 | ]; 7 | } 8 | -------------------------------------------------------------------------------- /onedrive-api-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "onedrive-api-test" 3 | version = "0.0.0" 4 | publish = false 5 | edition.workspace = true 6 | license.workspace = true 7 | # Keep in sync. 8 | rust-version = "1.74" 9 | 10 | [lints] 11 | workspace = true 12 | 13 | [dependencies] 14 | anyhow = "1" 15 | onedrive-api = { path = "..", features = ["beta"] } 16 | open = "5" 17 | pico-args = "0.5" 18 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 19 | 20 | [dev-dependencies] 21 | envy = "0.4" 22 | rand = "0.9" 23 | reqwest = "0.12" 24 | serde = "1" 25 | serde_json = "1" 26 | -------------------------------------------------------------------------------- /.github/workflows/future_proof.yaml: -------------------------------------------------------------------------------- 1 | name: Future proof tests 2 | on: 3 | schedule: 4 | - cron: '21 1 * * 0' # Sun *-*-* 01:21:00 UTC 5 | 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | env: 12 | RUST_BACKTRACE: full 13 | 14 | jobs: 15 | outdated: 16 | name: Outdated 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Install cargo-outdated 22 | uses: dtolnay/install@cargo-outdated 23 | - name: cargo-outdated 24 | run: | 25 | rm Cargo.lock # Ignore trivially updatable compatible versions. 26 | cargo outdated --workspace --exit-code 1 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # onedrive-api 2 | 3 | [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Foxalica%2Fonedrive-api%2Fbadge%3Fref%3Dmaster&style=flat)](https://actions-badge.atrox.dev/oxalica/onedrive-api/goto?ref=master) 4 | [![crates.io](https://img.shields.io/crates/v/onedrive-api.svg)](https://crates.io/crates/onedrive-api) 5 | [![Documentation](https://docs.rs/onedrive-api/badge.svg)](https://docs.rs/onedrive-api) 6 | 7 | A non-official middle-level HTTP API bindings to the [OneDrive][onedrive] REST API 8 | through [Microsoft Graph][graph], and also some utilities for authentication, 9 | 10 | - [Changelog](CHANGELOG.md) 11 | 12 | [onedrive]: https://products.office.com/en-us/onedrive/online-cloud-storage 13 | [graph]: https://docs.microsoft.com/graph/overview 14 | 15 | # License 16 | - [MIT license](LICENSE-MIT) 17 | -------------------------------------------------------------------------------- /.github/workflows/real_test.yaml: -------------------------------------------------------------------------------- 1 | name: Real test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - ci-test 7 | 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | 13 | env: 14 | RUST_BACKTRACE: full 15 | 16 | jobs: 17 | test: 18 | name: Real test 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Install Rust stable 24 | uses: dtolnay/rust-toolchain@stable 25 | - name: Build 26 | run: | 27 | cd onedrive-api-test 28 | cargo test --no-run 29 | - name: Test 30 | env: 31 | ONEDRIVE_API_TEST_CLIENT_ID: ${{ secrets.ONEDRIVE_API_TEST_CLIENT_ID }} 32 | ONEDRIVE_API_TEST_REDIRECT_URI: ${{ secrets.ONEDRIVE_API_TEST_REDIRECT_URI }} 33 | ONEDRIVE_API_TEST_REFRESH_TOKEN: ${{ secrets.ONEDRIVE_API_TEST_REFRESH_TOKEN }} 34 | run: | 35 | cd onedrive-api-test 36 | cargo test -- --include-ignored 37 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "onedrive-api" 3 | version = "0.10.2" 4 | repository = "https://github.com/oxalica/onedrive-api" 5 | categories = ["api-bindings"] 6 | keywords = ["onedrive", "http", "api"] 7 | description = "OneDrive HTTP REST API" 8 | documentation = "https://docs.rs/onedrive-api" 9 | exclude = ["/.github"] 10 | # NB. Sync with CI. 11 | rust-version = "1.74" # Transitive dependency windows-* 12 | license.workspace = true 13 | edition.workspace = true 14 | 15 | [workspace] 16 | members = ["onedrive-api-test"] 17 | 18 | [workspace.package] 19 | license = "MIT" 20 | edition = "2021" 21 | 22 | [lints] 23 | workspace = true 24 | 25 | [features] 26 | default = ["reqwest/default"] 27 | beta = [] 28 | 29 | [dependencies] 30 | # Compat with `reqwest` 31 | bytes = "1" 32 | reqwest = { version = "0.12", default-features = false, features = ["json", "gzip"] } 33 | serde = { version = "1", features = ["derive"] } 34 | serde_json = "1" 35 | strum = { version = "0.27", features = ["derive"] } 36 | thiserror = "2" 37 | url = "2" 38 | 39 | [dev-dependencies] 40 | reqwest = { version = "0.12", default-features = false, features = ["blocking"] } 41 | 42 | [package.metadata.docs.rs] 43 | all-features = true 44 | 45 | [workspace.lints.clippy] 46 | pedantic = { level = "warn", priority = -1 } 47 | missing-errors-doc = "allow" # Of course network requests can fail. 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | 6 | permissions: 7 | contents: read 8 | 9 | env: 10 | RUST_BACKTRACE: full 11 | 12 | jobs: 13 | format: 14 | name: Format 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Install Rust stable 20 | uses: dtolnay/rust-toolchain@stable 21 | - name: cargo fmt 22 | run: cargo fmt --all -- --check 23 | 24 | clippy: 25 | name: Clippy 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: Install Rust stable 31 | uses: dtolnay/rust-toolchain@stable 32 | - name: cargo clippy 33 | run: cargo clippy --workspace --all-targets -- -Dwarnings 34 | - name: cargo doc 35 | run: cargo doc --workspace ${{ matrix.feature_flag }} 36 | env: 37 | RUSTDOCFLAGS: -Dwarnings 38 | 39 | test: 40 | strategy: 41 | matrix: 42 | os: [ubuntu-latest, windows-latest, macOS-latest] 43 | rust: [stable, beta, '1.74'] # NB. Sync with Cargo.toml. 44 | include: 45 | - os: ubuntu-latest 46 | feature_flag: --features=beta 47 | name: Test ${{ matrix.os }} ${{ matrix.rust }} ${{ matrix.feature_flag }} 48 | runs-on: ${{ matrix.os }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | - name: Install Rust stable 53 | uses: dtolnay/rust-toolchain@master 54 | with: 55 | toolchain: ${{ matrix.rust }} 56 | - name: Build 57 | run: cargo build ${{ matrix.feature_flag }} 58 | - name: Test 59 | if: ${{ matrix.rust != '1.66.1' }} 60 | run: cargo test ${{ matrix.feature_flag }} 61 | -------------------------------------------------------------------------------- /onedrive-api-test/README.md: -------------------------------------------------------------------------------- 1 | ## onedrive-api-test 2 | 3 | This is a sub-crate to test `onedrive-api` by sending real requests to [Microsoft OneDrive][onedrive]. 4 | 5 | To run the test, you need to get a token for it. 6 | Here are the steps: 7 | 8 | 1. Login any of your Microsoft accounts, 9 | goto [`App Registrition`][app_registrition] on `Microsoft Azure` 10 | and register a new application. 11 | 12 | - You should choose the proper `Supported account types` to allow it to 13 | access `personal Microsoft accounts`. 14 | - In `Redirect URI `, choose `Public client/native (mobile & desktop)` and 15 | provide URL `https://login.microsoftonline.com/common/oauth2/nativeclient` 16 | (default URL for native apps). 17 | 18 | After a successful registrition, you got an `Application (client) ID` in UUID format. 19 | 20 | 2. Create a NEW Microsoft account *only* for test. 21 | This is highly recommended since the test may corrupt your files in OneDrive. 22 | 23 | 2. `cd` to the directory containing this README file, 24 | and run `cargo run -- `. 25 | It will prompt a browser and let you login to Microsoft. 26 | 27 | 3. Check and login the **test-only account** in browser, and it will redirect you 28 | to an blank page with url containing query string `code=...`. 29 | 30 | 4. Copy **the whole** URL to the console in step 2 and press enter. It will 31 | retrieve an token for test and save it to `.env` in current directory. 32 | 33 | 5. `source .env && cargo test` 34 | It will run tests in OneDrive of your test-only account. 35 | 36 | Also, you can revoke tokens granted on https://account.live.com/consent/Manage 37 | 38 | 39 | [onedrive]: https://onedrive.live.com 40 | [app_registrition]: https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade 41 | -------------------------------------------------------------------------------- /onedrive-api-test/tests/real_test/util.rs: -------------------------------------------------------------------------------- 1 | use onedrive_api::*; 2 | use serde::Deserialize; 3 | 4 | #[derive(Debug, Deserialize)] 5 | struct Env { 6 | client_id: String, 7 | redirect_uri: String, 8 | refresh_token: String, 9 | } 10 | 11 | pub static TOKEN: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); 12 | 13 | pub async fn get_logined_onedrive() -> OneDrive { 14 | let token = TOKEN 15 | .get_or_init(|| async { 16 | let env: Env = envy::prefixed("ONEDRIVE_API_TEST_").from_env().unwrap(); 17 | let auth = Auth::new( 18 | env.client_id, 19 | Permission::new_read().write(true).offline_access(true), 20 | env.redirect_uri, 21 | Tenant::Consumers, 22 | ); 23 | auth.login_with_refresh_token(&env.refresh_token, &ClientCredential::None) 24 | .await 25 | .expect("Login failed") 26 | .access_token 27 | }) 28 | .await; 29 | OneDrive::new(token.clone(), DriveLocation::me()) 30 | } 31 | 32 | pub fn gen_filename() -> &'static FileName { 33 | use std::sync::atomic::{AtomicU64, Ordering}; 34 | use std::sync::OnceLock; 35 | 36 | // Randomly initialized counter. 37 | static COUNTER: OnceLock = OnceLock::new(); 38 | let id = COUNTER 39 | // Avoid overflow to keep it monotonic. 40 | .get_or_init(|| AtomicU64::new(rand::random::().into())) 41 | .fetch_add(1, Ordering::Relaxed); 42 | let s = Box::leak(format!("$onedrive_api_tests.{id}").into_boxed_str()); 43 | FileName::new(s).unwrap() 44 | } 45 | 46 | pub fn rooted_location(name: &FileName) -> ItemLocation<'static> { 47 | let s = Box::leak(format!("/{}", name.as_str()).into_boxed_str()); 48 | ItemLocation::from_path(s).unwrap() 49 | } 50 | 51 | pub async fn download(url: &str) -> Vec { 52 | reqwest::get(url) 53 | .await 54 | .expect("Failed to request for downloading file") 55 | .bytes() 56 | .await 57 | .expect("Failed to download file") 58 | .to_vec() 59 | } 60 | -------------------------------------------------------------------------------- /onedrive-api-test/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{ensure, Context as _, Result}; 2 | use onedrive_api::{Auth, ClientCredential, Permission, Tenant}; 3 | use std::{ 4 | env, 5 | fs::File, 6 | io::{self, Write as _}, 7 | process::exit, 8 | }; 9 | 10 | const DEFAULT_REDIRECT_URI: &str = "https://login.microsoftonline.com/common/oauth2/nativeclient"; 11 | 12 | #[derive(Debug)] 13 | struct Args { 14 | client_id: String, 15 | output_file: String, 16 | redirect_uri: String, 17 | } 18 | 19 | fn parse_args() -> Result { 20 | let mut args = pico_args::Arguments::from_env(); 21 | let output_file = args 22 | .opt_value_from_str("-o")? 23 | .unwrap_or_else(|| ".env".to_owned()); 24 | let redirect_uri = args 25 | .opt_value_from_str("-r")? 26 | .unwrap_or_else(|| DEFAULT_REDIRECT_URI.to_owned()); 27 | let client_id = args 28 | .free_from_str::() 29 | .context("Missing client id")?; 30 | ensure!(args.finish().is_empty(), "Too many arguments"); 31 | Ok(Args { 32 | client_id, 33 | output_file, 34 | redirect_uri, 35 | }) 36 | } 37 | 38 | fn exit_with_help() -> ! { 39 | eprintln!( 40 | " 41 | This binary helps you to get an authorized refresh token to be used in tests. 42 | 43 | USAGE: {} [-o ] [-r ] 44 | `output_file` is default to be `.env` 45 | `redirect_url` is default to be `{}` 46 | ", 47 | env::args().next().unwrap(), 48 | DEFAULT_REDIRECT_URI, 49 | ); 50 | exit(1); 51 | } 52 | 53 | #[rustfmt::skip::macros(writeln)] 54 | #[tokio::main] 55 | async fn main() -> Result<()> { 56 | let args = parse_args().unwrap_or_else(|err| { 57 | eprintln!("{err}"); 58 | exit_with_help(); 59 | }); 60 | 61 | let auth = Auth::new( 62 | args.client_id.clone(), 63 | Permission::new_read().write(true).offline_access(true), 64 | args.redirect_uri.clone(), 65 | Tenant::Consumers, 66 | ); 67 | let url = auth.code_auth_url(); 68 | eprintln!("Code auth url: {url}"); 69 | if open::that(url.as_str()).is_err() { 70 | eprintln!("Cannot open browser, please open the url above manually."); 71 | } 72 | eprintln!("Please login in browser, paste the redirected URL here and then press "); 73 | 74 | let code = loop { 75 | const NEEDLE: &str = "nativeclient?code="; 76 | 77 | eprint!("Redirected URL: "); 78 | io::stdout().flush()?; 79 | let mut inp = String::new(); 80 | io::stdin().read_line(&mut inp)?; 81 | let inp = inp.trim(); 82 | 83 | match inp.find(NEEDLE) { 84 | Some(pos) => break inp[pos + NEEDLE.len()..].to_owned(), 85 | _ => eprintln!("Invalid!"), 86 | } 87 | }; 88 | 89 | eprintln!("Logining..."); 90 | let token = auth.login_with_code(&code, &ClientCredential::None).await?; 91 | let refresh_token = token.refresh_token.expect("Missing refresh token"); 92 | 93 | { 94 | let mut f = File::create(&args.output_file)?; 95 | writeln!(f, "export ONEDRIVE_API_TEST_CLIENT_ID='{}'", args.client_id)?; 96 | writeln!(f, "export ONEDRIVE_API_TEST_REDIRECT_URI='{}'", args.redirect_uri)?; 97 | writeln!(f, "export ONEDRIVE_API_TEST_REFRESH_TOKEN='{refresh_token}'")?; 98 | } 99 | 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::resource::{ErrorResponse, OAuth2ErrorResponse}; 4 | use reqwest::StatusCode; 5 | use thiserror::Error; 6 | 7 | /// An alias to `Result` of [`Error`][error]. 8 | /// 9 | /// [error]: ./struct.Error.html 10 | pub type Result = std::result::Result; 11 | 12 | /// Error of API request 13 | #[derive(Debug, Error)] 14 | #[error(transparent)] 15 | pub struct Error { 16 | inner: Box, 17 | } 18 | 19 | #[derive(Debug, Error)] 20 | enum ErrorKind { 21 | // Errors about ser/de are included. 22 | #[error("Request error: {0}")] 23 | RequestError(#[source] reqwest::Error), 24 | #[error("Unexpected response: {reason}")] 25 | UnexpectedResponse { reason: &'static str }, 26 | #[error("Api error with {status}: ({}) {}", .response.code, .response.message)] 27 | ErrorResponse { 28 | status: StatusCode, 29 | response: ErrorResponse, 30 | retry_after: Option, 31 | }, 32 | #[error("OAuth2 error with {status}: ({}) {}", .response.error, .response.error_description)] 33 | OAuth2Error { 34 | status: StatusCode, 35 | response: OAuth2ErrorResponse, 36 | retry_after: Option, 37 | }, 38 | } 39 | 40 | impl Error { 41 | pub(crate) fn from_error_response( 42 | status: StatusCode, 43 | response: ErrorResponse, 44 | retry_after: Option, 45 | ) -> Self { 46 | Self { 47 | inner: Box::new(ErrorKind::ErrorResponse { 48 | status, 49 | response, 50 | retry_after, 51 | }), 52 | } 53 | } 54 | 55 | pub(crate) fn unexpected_response(reason: &'static str) -> Self { 56 | Self { 57 | inner: Box::new(ErrorKind::UnexpectedResponse { reason }), 58 | } 59 | } 60 | 61 | pub(crate) fn from_oauth2_error_response( 62 | status: StatusCode, 63 | response: OAuth2ErrorResponse, 64 | retry_after: Option, 65 | ) -> Self { 66 | Self { 67 | inner: Box::new(ErrorKind::OAuth2Error { 68 | status, 69 | response, 70 | retry_after, 71 | }), 72 | } 73 | } 74 | 75 | /// Get the error response from API if caused by error status code. 76 | #[must_use] 77 | pub fn error_response(&self) -> Option<&ErrorResponse> { 78 | match &*self.inner { 79 | ErrorKind::ErrorResponse { response, .. } => Some(response), 80 | _ => None, 81 | } 82 | } 83 | 84 | /// Get the OAuth2 error response from API if caused by OAuth2 error response. 85 | #[must_use] 86 | pub fn oauth2_error_response(&self) -> Option<&OAuth2ErrorResponse> { 87 | match &*self.inner { 88 | ErrorKind::OAuth2Error { response, .. } => Some(response), 89 | _ => None, 90 | } 91 | } 92 | 93 | /// Get the HTTP status code if caused by error status code. 94 | #[must_use] 95 | pub fn status_code(&self) -> Option { 96 | match &*self.inner { 97 | ErrorKind::RequestError(source) => source.status(), 98 | ErrorKind::UnexpectedResponse { .. } => None, 99 | ErrorKind::ErrorResponse { status, .. } | ErrorKind::OAuth2Error { status, .. } => { 100 | Some(*status) 101 | } 102 | } 103 | } 104 | 105 | /// Get the retry delay hint on rate limited (HTTP 429) or server unavailability, if any. 106 | /// 107 | /// This is parsed from response header `Retry-After`. 108 | /// See: 109 | #[must_use] 110 | pub fn retry_after(&self) -> Option { 111 | match &*self.inner { 112 | ErrorKind::ErrorResponse { retry_after, .. } 113 | | ErrorKind::OAuth2Error { retry_after, .. } => { 114 | Some(Duration::from_secs((*retry_after)?.into())) 115 | } 116 | _ => None, 117 | } 118 | } 119 | } 120 | 121 | impl From for Error { 122 | fn from(source: reqwest::Error) -> Self { 123 | Self { 124 | inner: Box::new(ErrorKind::RequestError(source)), 125 | } 126 | } 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use std::error::Error as _; 132 | 133 | use super::*; 134 | 135 | #[test] 136 | fn error_source() { 137 | let err = reqwest::blocking::get("urn:urn").unwrap_err(); 138 | let original_err_fmt = err.to_string(); 139 | let source_err_fmt = Error::from(err).source().unwrap().to_string(); 140 | assert_eq!(source_err_fmt, original_err_fmt); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # onedrive-api 2 | //! 3 | //! `onedrive-api` crate provides middle-level HTTP APIs [`OneDrive`][one_drive] to the 4 | //! [OneDrive][ms_onedrive] API through [Microsoft Graph][ms_graph], and also [`Auth`][auth] 5 | //! with utilities for OAuth2. 6 | //! 7 | //! ## Example 8 | //! ``` 9 | //! use onedrive_api::{OneDrive, FileName, DriveLocation, ItemLocation}; 10 | //! use reqwest::Client; 11 | //! 12 | //! # async fn run() -> onedrive_api::Result<()> { 13 | //! let client = Client::new(); 14 | //! let drive = OneDrive::new( 15 | //! "<...TOKEN...>", // Login token to Microsoft Graph. 16 | //! DriveLocation::me(), 17 | //! ); 18 | //! 19 | //! let folder_item = drive 20 | //! .create_folder( 21 | //! ItemLocation::root(), 22 | //! FileName::new("test_folder").unwrap(), 23 | //! ) 24 | //! .await?; 25 | //! 26 | //! drive 27 | //! .upload_small( 28 | //! folder_item.id.as_ref().unwrap(), 29 | //! &b"Hello, world"[..], 30 | //! ) 31 | //! .await?; 32 | //! 33 | //! # Ok(()) 34 | //! # } 35 | //! ``` 36 | //! 37 | //! # Features 38 | //! - `beta` 39 | //! 40 | //! Most of Microsoft APIs used in this crate are stable. 41 | //! But there are also some beta APIs, which are subject to change and 42 | //! is not suggested to be used in production application. 43 | //! Microsoft references of beta APIs usually contain a `(beta)` suffix in title. 44 | //! 45 | //! To avoid breakage, we put beta APIs and related resources under feature gate `beta`. 46 | //! They may change to follow Microsoft API references **ANYTIME**, 47 | //! and do **NOT** follow the semantic version of this crate. 48 | //! 49 | //! Be carefully using it and **do NOT use it in production**. 50 | //! 51 | //! [ms_onedrive]: https://products.office.com/en-us/onedrive/online-cloud-storage 52 | //! [ms_graph]: https://docs.microsoft.com/graph/overview 53 | //! [one_drive]: ./struct.OneDrive.html 54 | //! [auth]: ./struct.Auth.html 55 | //! [api]: ./trait.Api.html 56 | //! [api_execute]: ./trait.Api.html#tymethod.execute 57 | //! [client]: ./trait.Client.html 58 | // #![deny(warnings)] 59 | #![deny(missing_debug_implementations)] 60 | #![deny(missing_docs)] 61 | use serde::{de, Serialize}; 62 | 63 | mod auth; 64 | mod error; 65 | mod onedrive; 66 | pub mod option; 67 | pub mod resource; 68 | mod util; 69 | 70 | pub use self::{ 71 | auth::{Auth, ClientCredential, Permission, Tenant, TokenResponse}, 72 | error::{Error, Result}, 73 | onedrive::{ 74 | CopyProgressMonitor, ListChildrenFetcher, OneDrive, TrackChangeFetcher, UploadSession, 75 | UploadSessionMeta, 76 | }, 77 | resource::{DriveId, ItemId, Tag}, 78 | util::{DriveLocation, FileName, ItemLocation}, 79 | }; 80 | 81 | #[cfg(feature = "beta")] 82 | pub use self::onedrive::{CopyProgress, CopyStatus}; 83 | 84 | /// The conflict resolution behavior for actions that create a new item. 85 | /// 86 | /// # See also 87 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/resources/driveitem?view=graph-rest-1.0#instance-attributes) 88 | #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] 89 | #[serde(rename_all = "camelCase")] 90 | pub enum ConflictBehavior { 91 | /// Make the request fail. Usually cause HTTP 409 CONFLICT. 92 | Fail, 93 | /// **DANGER**: Replace the existing item. 94 | Replace, 95 | /// Rename the newly created item to another name. 96 | /// 97 | /// The new name is not specified and usually can be retrieved from the response. 98 | Rename, 99 | } 100 | 101 | /// A half-open byte range `start..end` or `start..`. 102 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 103 | pub struct ExpectRange { 104 | /// The lower bound of the range (inclusive). 105 | pub start: u64, 106 | /// The optional upper bound of the range (exclusive). 107 | pub end: Option, 108 | } 109 | 110 | impl<'de> de::Deserialize<'de> for ExpectRange { 111 | fn deserialize>( 112 | deserializer: D, 113 | ) -> std::result::Result { 114 | struct Visitor; 115 | 116 | impl de::Visitor<'_> for Visitor { 117 | type Value = ExpectRange; 118 | 119 | fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 120 | write!(f, "Expect Range") 121 | } 122 | 123 | fn visit_str(self, v: &str) -> std::result::Result { 124 | let parse = || -> Option { 125 | let mut it = v.split('-'); 126 | let start = it.next()?.parse().ok()?; 127 | let end = match it.next()? { 128 | "" => None, 129 | s => { 130 | let end = s.parse::().ok()?.checked_add(1)?; // Exclusive. 131 | if end <= start { 132 | return None; 133 | } 134 | Some(end) 135 | } 136 | }; 137 | if it.next().is_some() { 138 | return None; 139 | } 140 | 141 | Some(ExpectRange { start, end }) 142 | }; 143 | match parse() { 144 | Some(v) => Ok(v), 145 | None => Err(E::invalid_value( 146 | de::Unexpected::Str(v), 147 | &"`{lower}-` or `{lower}-{upper}`", 148 | )), 149 | } 150 | } 151 | } 152 | 153 | deserializer.deserialize_str(Visitor) 154 | } 155 | } 156 | 157 | #[cfg(test)] 158 | mod tests { 159 | use super::*; 160 | 161 | #[test] 162 | fn test_range_parsing() { 163 | let max = format!("0-{}", u64::MAX - 1); 164 | let overflow = format!("0-{}", u64::MAX); 165 | let cases = [ 166 | ( 167 | "42-196", 168 | Some(ExpectRange { 169 | start: 42, 170 | end: Some(197), 171 | }), 172 | ), // [left, right) 173 | ( 174 | "418-", 175 | Some(ExpectRange { 176 | start: 418, 177 | end: None, 178 | }), 179 | ), 180 | ("", None), 181 | ("42-4", None), 182 | ("-9", None), 183 | ("-", None), 184 | ("1-2-3", None), 185 | ("0--2", None), 186 | ("-1-2", None), 187 | ( 188 | &max, 189 | Some(ExpectRange { 190 | start: 0, 191 | end: Some(u64::MAX), 192 | }), 193 | ), 194 | (&overflow, None), 195 | ]; 196 | 197 | for &(s, ref expect) in &cases { 198 | let ret = serde_json::from_str(&serde_json::to_string(s).unwrap()); 199 | assert_eq!( 200 | ret.as_ref().ok(), 201 | expect.as_ref(), 202 | "Failed: Got {ret:?} on {s:?}", 203 | ); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](https://semver.org/). 7 | 8 | ## Unreleased 9 | 10 | ## v0.10.2 11 | 12 | ### Changed 13 | 14 | - MSRV is bumped to 1.66.1. 15 | 16 | ### Others 17 | 18 | - Update to `strum` 0.27 and `rand` 0.9 (dev-dependency). 19 | 20 | ## v0.10.1 21 | 22 | ### Changed 23 | 24 | - Fixed typos. 25 | 26 | ### Others 27 | 28 | - Update `thiserror` to 2 and various other dependencies. 29 | 30 | ## v0.10.0 31 | 32 | ### Added 33 | 34 | - `impl Clone for OneDrive`. 35 | - Configurable `Tenant` for `Auth`, this allows login into applications that 36 | accept only personal accounts. 37 | - Expose `Auth::client`. 38 | - `Retry-After` response header on rate limited or service unavailability is 39 | now parsed and returned via `Error::retry_after`. 40 | - `DriveItem::download_url` (`@microsoft.graph.downloadUrl`) can now be 41 | selected in `$select=`. 42 | 43 | ### Changed 44 | 45 | - `Debug` impls of `OneDrive` and `TokenResponse` now hide tokens. 46 | - Update to reqwest 0.12. 47 | - `Error::source` now correctly returns its inner `reqwest::Error`. 48 | - `Auth::code_auth_url` now returns a `url::Url` instead of a `String`. 49 | - `Auth::login_with_{code,refresh_token}` now accepts `ClientCredential` which 50 | supports no credential, client secret and certificate assertion. 51 | 52 | ### Removed 53 | 54 | - Local assertions for session upload length limit, small upload length limit. 55 | They are only enforced by the server. 56 | 57 | ## v0.9.1 58 | 59 | ### Added 60 | 61 | - Add `create_drive_item` for `driveItem` creation with custom attributes. [#6] 62 | 63 | [#6]: https://github.com/oxalica/onedrive-api/pull/6 64 | 65 | ### Changed 66 | 67 | - Bump to edition 2021 and set MSRV as 1.64. 68 | - Many functions are marked with `#[must_use]`. 69 | 70 | ### Internal 71 | 72 | - `Cargo.lock` is unignored and checked in Git. 73 | - Rewrite CI and replace archived dependencies. 74 | - Update dependencies and relax version specifications to only major digit. 75 | 76 | ## v0.9.0 77 | 78 | ### API changes 79 | - Accept `impl Into` in parameters of `OneDrive::*`, which previously only accept `String`. 80 | - `resource::*Field::raw_name` takes `self` instead of `&self`, since they are now `Copy`. 81 | 82 | ### Others 83 | - Update dependencies. 84 | - Derive more std traits for resource and option types, including `Clone`, `Copy`, `PartialEq` and `Eq`. 85 | - More `#[must_use]` are added to warn incorrect usages. 86 | - Documentation fixes. 87 | 88 | ## v0.8.1 89 | 90 | ### Features 91 | - Support initial state of upload sessions (`OneDrive::new_upload_session_with_initial_option`) 92 | 93 | ## v0.8.0 94 | 95 | ### Breaking Changes 96 | - Split metadata out from `UploadSession` and make it works without `OneDrive` instance since it doesn't 97 | require token. 98 | 99 | - New struct `UploadSessionMeta` 100 | - `file_size` is separated from `UploadSession` now and is required in each `UploadSession::upload_part` 101 | call. 102 | - `OneDrive::get_upload_session` is moved to `UploadSession::get_meta`. 103 | 104 | ### Features 105 | - Add method `OneDrive::client` to get the `Client` used to construct the instance. 106 | - Expose constants `OneDrive::UPLOAD_SMALL_MAX_SIZE` and `UploadSession::MAX_PART_SIZE`. 107 | 108 | ### Fixes 109 | - Fix deserialization error of `OneDrive::upload_small` when uploading empty data. 110 | 111 | ## v0.7.0 112 | 113 | ### Breaking Changes 114 | - Limit tracking API to root folders only since they are undocumented and doesn't work in some cases. 115 | 116 | These API are affected: 117 | - `track_changes_from_{initial,delta_url}{,_with_option}` -> `track_root_changes*` 118 | - `get_latest_delta_url{,_with_option}` -> `get_root_latest_delta_url*` 119 | 120 | The new API works only for root folders, and the previous `folder` parameter is removed. 121 | 122 | ### Others 123 | - Update dependencies to `tokio` 1.0 ecosystem. 124 | 125 | 126 | ## v0.6.3 127 | 128 | ### Fixes 129 | - Revert `track_changes_from_delta_url_with_option` since it will cause duplicated query parameters. 130 | Instead, we introduced `get_latest_delta_url_with_option` for setting options at beginning. 131 | 132 | ## v0.6.2 (Yanked) 133 | 134 | ### Features 135 | - Add missing `track_changes_from_delta_url_with_option` for customizing `track_changes_from_delta_url`. 136 | - Add method `raw_name` for field descriptor enums to get raw camelCased name used in underlying requests. 137 | - Add getter `client_id/permission/redirect_uri` for `Auth`. 138 | 139 | ### Others 140 | - Bump dependencies. 141 | - Use new rustc features to simplify codes. 142 | 143 | ## v0.6.1 144 | 145 | ### Features 146 | - Default features of `reqwest` can be disabled by `default-features = false` 147 | to allow switching to non-default tls implementation. 148 | - Enable gzip by default. 149 | - New API: `get_item_download_url[_with_option]` 150 | - New variant of `ItemLocation`: locating an child by name under an `ItemId`. 151 | 152 | ### Fixes 153 | - `options::*` are now `Send + Sync` 154 | 155 | ## v0.6.0 156 | 157 | ### Huge Breaking Changes 158 | - Sweet `reqwest` again. Drop `api::*` indroduced in `0.5.*`. 159 | - Everything is `async` now. 160 | - Beta APIs are now under feature gate `beta`. 161 | - `onedrive_api::auth` 162 | - `async`! 163 | - `Authentication` -> `Auth`, and remove methods of token auth flow. 164 | - `Token` -> `TokenResponse`, and include all fields in Microsoft Reference. 165 | - `token` -> `access_token` to follow the reference. 166 | - `onedrive_api::error` 167 | - Switch to `thiserror`. 168 | - `ErrorObject` -> `ErrorResponse`, and fix types. 169 | - Now also handle OAuth2 error response. 170 | - String wrappers `onedrive_api::{DriveId, ItemId, Tag}` 171 | - Now have fields public. 172 | - `onedrive_api::OneDrive` 173 | - `async`! 174 | - `UploadSession` now stores `file_size`. 175 | Methods `upload_to_session` and `delete_upload_session` are moved to `UploadSession`. 176 | - `*Fetcher` are now pure data struct without references to `OneDrive`. 177 | This makes it easy to store them with `OneDrive` without worries about self-references. 178 | - Other tiny fix-ups and renames. 179 | 180 | ### Features 181 | - `async`! 182 | - Refactor tests and switch to GitHub Actions as CI. 183 | 184 | ### Fixes 185 | - Shrink dependencies. 186 | 187 | ## v0.5.2 188 | 189 | ### Features 190 | - Add new api `OneDrive::update_item[_with_option]` 191 | - Derive `Serialize` and `Default` for resource objects in `onedrive_api::resource` 192 | 193 | ### Fixes 194 | - Tests 195 | 196 | ## v0.5.1 197 | ### Fixes 198 | - Tests 199 | 200 | ## v0.5.0 201 | ### Huge Breaking Changes 202 | - Refactor all APIs with new `Api` and `Client` interfaces and strip dependency to `reqwest`. 203 | See docs for more details. 204 | - `Error::{should_retry,url}` are removed. 205 | - Rename `AuthClient` to `Authentication`, and `DriveClient` to `OneDrive`. 206 | - Rename `UploadSession::{get_url,get_next_expected_ranges,get_expiration_date_time}` to `{upload_url,next_expected_ranges,expiration_date_time}`. 207 | - Rename `ListChildrenFetcher::get_next_url` to `next_url` 208 | - Rename `TrackChangeFetcher::{get_next_url,get_delta_url` to `{next_url,delta_url}` 209 | - Rename `ListChildrenFetcher` and `TrackChangeFetcher` are no longer `Iterator`. 210 | See docs for more details. 211 | 212 | ### Features 213 | - Refactor and add more tests. 214 | - Support custom HTTP backend. 215 | 216 | ### Fixes 217 | - Request changes of beta api `CopyProgressMonitor::fetch_progress` 218 | - Documentations 219 | 220 | ## v0.4.0 221 | ### Breaking Changes 222 | - Renane mod `query_option` to `option`. 223 | - Move `if-match` and `if-none-match` from parameter to `option` 224 | to simplify simple API (without `_with_option`). 225 | 226 | ### Features 227 | - Support `conflict_behavior` in related `with_option` API. 228 | - Support `expiration_date_time` field in `UploadSession`. 229 | - Support tracking asynchronous `copy` operation through `CopyProgressMonitor`. 230 | 231 | ### Fixes 232 | - Fix and add more documentations. 233 | - Maintain mod structure. 234 | 235 | ## v0.3.0 236 | ### Features 237 | - Add all fields available of `resource::{Drive, DriveItem}` in Microsoft Graph Documentation (See documentations of them). 238 | 239 | ### Breaking Changes 240 | - Refact `query_option::{Object, Collection}Option` and change parameter types of relative `DriveClient::*_with_option` methods. 241 | - Remove `resource::{Deleted, ItemReference}`, which are not necessary for using this crate. 242 | If you need more detail from these fields, just manually destruct the `serde_json::Value` fields of `resource::{Drive, DriveItem}`. 243 | 244 | ## v0.2.1 245 | ### Fixes 246 | - Fix documentations and add examples. 247 | 248 | ## v0.2.0 249 | Initial release. 250 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::{Error, Result}, 3 | resource::{DriveId, ErrorResponse, ItemId, OAuth2ErrorResponse}, 4 | }; 5 | use reqwest::{header, RequestBuilder, Response, StatusCode}; 6 | use serde::{de, Deserialize}; 7 | use 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)] 18 | pub struct DriveLocation { 19 | inner: DriveLocationEnum, 20 | } 21 | 22 | #[derive(Clone, Debug)] 23 | enum DriveLocationEnum { 24 | Me, 25 | User(String), 26 | Group(String), 27 | Site(String), 28 | Id(DriveId), 29 | } 30 | 31 | impl 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) -> 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) -> 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) -> 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 | 85 | impl From 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)] 102 | pub struct ItemLocation<'a> { 103 | inner: ItemLocationEnum<'a>, 104 | } 105 | 106 | #[derive(Clone, Copy, Debug)] 107 | enum 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 | 118 | impl<'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 { 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 | 178 | impl<'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)] 186 | pub struct FileName(str); 187 | 188 | impl 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 + ?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 | 215 | impl AsRef for FileName { 216 | fn as_ref(&self) -> &str { 217 | self.as_str() 218 | } 219 | } 220 | 221 | pub(crate) trait ApiPathComponent { 222 | fn extend_into(&self, buf: &mut PathSegmentsMut); 223 | } 224 | 225 | impl 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 | 237 | impl 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 | 251 | impl ApiPathComponent for str { 252 | fn extend_into(&self, buf: &mut PathSegmentsMut) { 253 | buf.push(self); 254 | } 255 | } 256 | 257 | pub(crate) trait RequestBuilderTransformer { 258 | fn trans(self, req: RequestBuilder) -> RequestBuilder; 259 | } 260 | 261 | pub(crate) trait RequestBuilderExt: Sized { 262 | fn apply(self, trans: impl RequestBuilderTransformer) -> Self; 263 | } 264 | 265 | impl RequestBuilderExt for RequestBuilder { 266 | fn apply(self, trans: impl RequestBuilderTransformer) -> Self { 267 | trans.trans(self) 268 | } 269 | } 270 | 271 | type BoxFuture = std::pin::Pin + Send + 'static>>; 272 | 273 | // TODO: Avoid boxing? 274 | pub(crate) trait ResponseExt: Sized { 275 | fn parse(self) -> BoxFuture>; 276 | fn parse_optional(self) -> BoxFuture>>; 277 | fn parse_no_content(self) -> BoxFuture>; 278 | } 279 | 280 | impl ResponseExt for Response { 281 | fn parse(self) -> BoxFuture> { 282 | Box::pin(async move { Ok(handle_error_response(self).await?.json().await?) }) 283 | } 284 | 285 | fn parse_optional(self) -> BoxFuture>> { 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> { 295 | Box::pin(async move { 296 | handle_error_response(self).await?; 297 | Ok(()) 298 | }) 299 | } 300 | } 301 | 302 | pub(crate) async fn handle_error_response(resp: Response) -> Result { 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 | 319 | pub(crate) async fn handle_oauth2_error_response(resp: Response) -> Result { 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 | /// . 332 | /// And HTTP requires it to be a non-negative integer: 333 | /// . 334 | fn parse_retry_after_sec(resp: &Response) -> Option { 335 | resp.headers() 336 | .get(header::RETRY_AFTER)? 337 | .to_str() 338 | .ok()? 339 | .parse() 340 | .ok() 341 | } 342 | -------------------------------------------------------------------------------- /src/option.rs: -------------------------------------------------------------------------------- 1 | //! Configurable options which can be used to customize API behaviors or responses. 2 | //! 3 | //! # Note 4 | //! Some requests do not support all of these parameters, 5 | //! and using them will cause an error. 6 | //! 7 | //! Be careful and read the documentation of API from Microsoft before 8 | //! applying any options. 9 | //! 10 | //! # See also 11 | //! [Microsoft Docs](https://docs.microsoft.com/en-us/graph/query-parameters) 12 | #![allow(clippy::module_name_repetitions)] // Ambiguous if without sufficies. 13 | use crate::{ 14 | resource::{ResourceField, Tag}, 15 | util::RequestBuilderTransformer, 16 | ConflictBehavior, 17 | }; 18 | use reqwest::{header, RequestBuilder}; 19 | use std::{fmt::Write, marker::PhantomData}; 20 | 21 | #[derive(Debug, Default, Clone, PartialEq, Eq)] 22 | struct AccessOption { 23 | if_match: Option, 24 | if_none_match: Option, 25 | } 26 | 27 | impl AccessOption { 28 | fn if_match(mut self, tag: &Tag) -> Self { 29 | self.if_match = Some(tag.0.clone()); 30 | self 31 | } 32 | 33 | fn if_none_match(mut self, tag: &Tag) -> Self { 34 | self.if_none_match = Some(tag.0.clone()); 35 | self 36 | } 37 | } 38 | 39 | impl RequestBuilderTransformer for AccessOption { 40 | fn trans(self, mut req: RequestBuilder) -> RequestBuilder { 41 | if let Some(v) = self.if_match { 42 | req = req.header(header::IF_MATCH, v); 43 | } 44 | if let Some(v) = self.if_none_match { 45 | req = req.header(header::IF_NONE_MATCH, v); 46 | } 47 | req 48 | } 49 | } 50 | 51 | /// Option for GET-like requests to one resource object. 52 | #[derive(Debug, Clone, PartialEq, Eq)] 53 | pub struct ObjectOption { 54 | access_opt: AccessOption, 55 | select_buf: String, 56 | expand_buf: String, 57 | _marker: PhantomData, 58 | } 59 | 60 | impl ObjectOption { 61 | /// Create an empty (default) option. 62 | #[must_use] 63 | pub fn new() -> Self { 64 | Self { 65 | access_opt: AccessOption::default(), 66 | select_buf: String::new(), 67 | expand_buf: String::new(), 68 | _marker: PhantomData, 69 | } 70 | } 71 | 72 | /// Only response if the object matches the `tag`. 73 | /// 74 | /// Will cause HTTP 412 Precondition Failed otherwise. 75 | /// 76 | /// It is usually used for PUT-like requests to assert preconditions, but 77 | /// most of GET-like requests also support it. 78 | /// 79 | /// It will add `If-Match` to the request header. 80 | #[must_use] 81 | pub fn if_match(mut self, tag: &Tag) -> Self { 82 | self.access_opt = self.access_opt.if_match(tag); 83 | self 84 | } 85 | 86 | /// Only response if the object does not match the `tag`. 87 | /// 88 | /// Will cause the relative API returns `None` otherwise. 89 | /// 90 | /// It is usually used for GET-like requests to reduce data transmission if 91 | /// cached data can be reused. 92 | /// 93 | /// This will add `If-None-Match` to the request header. 94 | #[must_use] 95 | pub fn if_none_match(mut self, tag: &Tag) -> Self { 96 | self.access_opt = self.access_opt.if_none_match(tag); 97 | self 98 | } 99 | 100 | /// Select only some fields of the resource object. 101 | /// 102 | /// See documentation of module [`onedrive_api::resource`][resource] for more details. 103 | /// 104 | /// # Note 105 | /// If called more than once, all fields mentioned will be selected. 106 | /// 107 | /// # See also 108 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/query-parameters#select-parameter) 109 | /// 110 | /// [resource]: ../resource/index.html#field-descriptors 111 | #[must_use] 112 | pub fn select(mut self, fields: &[Field]) -> Self { 113 | for sel in fields { 114 | self = self.select_raw(&[sel.__raw_name()]); 115 | } 116 | self 117 | } 118 | 119 | fn select_raw(mut self, fields: &[&str]) -> Self { 120 | for sel in fields { 121 | write!(self.select_buf, ",{sel}").unwrap(); 122 | } 123 | self 124 | } 125 | 126 | /// Expand a field of the resource object. 127 | /// 128 | /// See documentation of module [`onedrive_api::resource`][resource] for more details. 129 | /// 130 | /// # Note 131 | /// If called more than once, all fields mentioned will be expanded. 132 | /// `select_children` should be raw camelCase field names mentioned in Microsoft Docs below. 133 | /// 134 | /// # See also 135 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/query-parameters#expand-parameter) 136 | /// 137 | /// [resource]: ../resource/index.html#field-descriptors 138 | #[must_use] 139 | pub fn expand(self, field: Field, select_children: Option<&[&str]>) -> Self { 140 | self.expand_raw(field.__raw_name(), select_children) 141 | } 142 | 143 | fn expand_raw(mut self, field: &str, select_children: Option<&[&str]>) -> Self { 144 | let buf = &mut self.expand_buf; 145 | write!(buf, ",{field}").unwrap(); 146 | if let Some(children) = select_children { 147 | write!(buf, "($select=").unwrap(); 148 | for sel in children { 149 | write!(buf, "{sel},").unwrap(); 150 | } 151 | write!(buf, ")").unwrap(); 152 | } 153 | self 154 | } 155 | } 156 | 157 | impl RequestBuilderTransformer for ObjectOption { 158 | fn trans(self, mut req: RequestBuilder) -> RequestBuilder { 159 | req = self.access_opt.trans(req); 160 | if let Some(s) = self.select_buf.get(1..) { 161 | req = req.query(&[("$select", s)]); 162 | } 163 | if let Some(s) = self.expand_buf.get(1..) { 164 | req = req.query(&[("$expand", s)]); 165 | } 166 | req 167 | } 168 | } 169 | 170 | impl Default for ObjectOption { 171 | fn default() -> Self { 172 | Self::new() 173 | } 174 | } 175 | 176 | /// Option for GET-like requests for a collection of resource objects. 177 | #[derive(Debug, Clone, PartialEq, Eq)] 178 | pub struct CollectionOption { 179 | obj_option: ObjectOption, 180 | order_buf: Option, 181 | page_size_buf: Option, 182 | get_count_buf: bool, 183 | } 184 | 185 | impl CollectionOption { 186 | /// Create an empty (default) option. 187 | #[must_use] 188 | pub fn new() -> Self { 189 | Self { 190 | obj_option: ObjectOption::default(), 191 | order_buf: None, 192 | page_size_buf: None, 193 | get_count_buf: false, 194 | } 195 | } 196 | 197 | /// Only response if the object matches the `tag`. 198 | /// 199 | /// # See also 200 | /// [`ObjectOption::if_match`][if_match] 201 | /// 202 | /// [if_match]: ./struct.ObjectOption.html#method.if_match 203 | #[must_use] 204 | pub fn if_match(mut self, tag: &Tag) -> Self { 205 | self.obj_option = self.obj_option.if_match(tag); 206 | self 207 | } 208 | 209 | /// Only response if the object does not match the `tag`. 210 | /// 211 | /// # See also 212 | /// [`ObjectOption::if_none_match`][if_none_match] 213 | /// 214 | /// [if_none_match]: ./struct.ObjectOption.html#method.if_none_match 215 | #[must_use] 216 | pub fn if_none_match(mut self, tag: &Tag) -> Self { 217 | self.obj_option = self.obj_option.if_none_match(tag); 218 | self 219 | } 220 | 221 | /// Select only some fields of the resource object. 222 | /// 223 | /// See documentation of module [`onedrive_api::resource`][resource] for more details. 224 | /// 225 | /// # See also 226 | /// [`ObjectOption::select`][select] 227 | /// 228 | /// [select]: ./struct.ObjectOption.html#method.select 229 | /// [resource]: ../resource/index.html#field-descriptors 230 | #[must_use] 231 | pub fn select(mut self, fields: &[Field]) -> Self { 232 | self.obj_option = self.obj_option.select(fields); 233 | self 234 | } 235 | 236 | /// Expand a field of the resource object. 237 | /// 238 | /// See documentation of module [`onedrive_api::resource`][resource] for more details. 239 | /// 240 | /// # See also 241 | /// [`ObjectOption::expand`][expand] 242 | /// 243 | /// [expand]: ./struct.ObjectOption.html#method.expand 244 | /// [resource]: ../resource/index.html#field-descriptors 245 | #[must_use] 246 | pub fn expand(mut self, field: Field, select_children: Option<&[&str]>) -> Self { 247 | self.obj_option = self.obj_option.expand(field, select_children); 248 | self 249 | } 250 | 251 | /// Specify the sort order of the items in response. 252 | /// 253 | /// # Note 254 | /// If called more than once, only the last call make sense. 255 | /// 256 | /// # See also 257 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/query-parameters#orderby-parameter) 258 | #[must_use] 259 | pub fn order_by(mut self, field: Field, order: Order) -> Self { 260 | let order = match order { 261 | Order::Ascending => "asc", 262 | Order::Descending => "desc", 263 | }; 264 | self.order_buf = Some(format!("{} {}", field.__raw_name(), order)); 265 | self 266 | } 267 | 268 | /// Specify the number of items per page. 269 | /// 270 | /// # Note 271 | /// If called more than once, only the last call make sense. 272 | /// 273 | /// # See also 274 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/query-parameters#top-parameter) 275 | #[must_use] 276 | pub fn page_size(mut self, size: usize) -> Self { 277 | self.page_size_buf = Some(size.to_string()); 278 | self 279 | } 280 | 281 | /// Specify to get the number of all items. 282 | /// 283 | /// # Note 284 | /// If called more than once, only the last call make sense. 285 | /// 286 | /// Note that Track Changes API does not support this. Setting it in like 287 | /// [`track_changes_from_initial_with_option`][track_init_opt] will cause a panic. 288 | /// 289 | /// # See also 290 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/query-parameters#count-parameter) 291 | /// 292 | /// [track_init_opt]: ../struct.OneDrive.html#method.track_changes_from_initial_with_option 293 | #[must_use] 294 | pub fn get_count(mut self, get_count: bool) -> Self { 295 | self.get_count_buf = get_count; 296 | self 297 | } 298 | 299 | pub(crate) fn has_get_count(&self) -> bool { 300 | self.get_count_buf 301 | } 302 | } 303 | 304 | impl RequestBuilderTransformer for CollectionOption { 305 | fn trans(self, mut req: RequestBuilder) -> RequestBuilder { 306 | req = self.obj_option.trans(req); 307 | if let Some(s) = &self.order_buf { 308 | req = req.query(&[("$orderby", s)]); 309 | } 310 | if let Some(s) = &self.page_size_buf { 311 | req = req.query(&[("$top", s)]); 312 | } 313 | if self.get_count_buf { 314 | req = req.query(&[("$count", "true")]); 315 | } 316 | req 317 | } 318 | } 319 | 320 | impl Default for CollectionOption { 321 | fn default() -> Self { 322 | Self::new() 323 | } 324 | } 325 | 326 | /// Specify the sorting order. 327 | /// 328 | /// Used in [`CollectionOption::order_by`][order_by]. 329 | /// 330 | /// [order_by]: ./struct.CollectionOption.html#method.order_by 331 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 332 | pub enum Order { 333 | /// Ascending order. 334 | Ascending, 335 | /// Descending order. 336 | Descending, 337 | } 338 | 339 | /// Option for PUT-like requests of `DriveItem`. 340 | #[derive(Debug, Default, Clone, PartialEq, Eq)] 341 | pub struct DriveItemPutOption { 342 | access_opt: AccessOption, 343 | conflict_behavior: Option, 344 | } 345 | 346 | impl DriveItemPutOption { 347 | /// Create an empty (default) option. 348 | #[must_use] 349 | pub fn new() -> Self { 350 | Self::default() 351 | } 352 | 353 | /// Only response if the object matches the `tag`. 354 | /// 355 | /// # See also 356 | /// [`ObjectOption::if_match`][if_match] 357 | /// 358 | /// [if_match]: ./struct.ObjectOption.html#method.if_match 359 | #[must_use] 360 | pub fn if_match(mut self, tag: &Tag) -> Self { 361 | self.access_opt = self.access_opt.if_match(tag); 362 | self 363 | } 364 | 365 | // `if_none_match` is not supported in PUT-like requests. 366 | 367 | /// Specify the behavior if the target item already exists. 368 | /// 369 | /// # Note 370 | /// This not only available for DELETE-like requests. Read the docs first. 371 | /// 372 | /// # See also 373 | /// `@microsoft.graph.conflictBehavior` of `DriveItem` on [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/resources/driveitem?view=graph-rest-1.0#instance-attributes) 374 | #[must_use] 375 | pub fn conflict_behavior(mut self, conflict_behavior: ConflictBehavior) -> Self { 376 | self.conflict_behavior = Some(conflict_behavior); 377 | self 378 | } 379 | 380 | pub(crate) fn get_conflict_behavior(&self) -> Option { 381 | self.conflict_behavior 382 | } 383 | } 384 | 385 | impl RequestBuilderTransformer for DriveItemPutOption { 386 | fn trans(self, req: RequestBuilder) -> RequestBuilder { 387 | self.access_opt.trans(req) 388 | } 389 | } 390 | 391 | #[cfg(test)] 392 | // `#[expect()]` is incompatible with our MSRV. 393 | #[allow(dead_code)] 394 | mod tests { 395 | use super::*; 396 | use crate::resource; 397 | 398 | fn assert_send_sync() {} 399 | 400 | fn assert_object_option_is_send_sync() { 401 | assert_send_sync::>(); 402 | assert_send_sync::>(); 403 | } 404 | 405 | fn assert_collection_option_is_send_sync() { 406 | assert_send_sync::>(); 407 | assert_send_sync::>(); 408 | } 409 | 410 | fn assert_drive_item_put_option_is_send_sync() { 411 | assert_send_sync::(); 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/resource.rs: -------------------------------------------------------------------------------- 1 | //! Resource Objects defined in the OneDrive API. 2 | //! 3 | //! # Field descriptors 4 | //! 5 | //! Resource object `struct`s have field descriper `enum`s representing 6 | //! all controllable fields of it, which may be used 7 | //! in [`onedrive_api::option`][option] to [`select`][select] or [`expand`][expand] it using 8 | //! `with_option` version API of [`OneDrive`][one_drive]. 9 | //! 10 | //! ## Example 11 | //! Here is an example to use [`resource::DriveItemField`][drive_item_field]. 12 | //! ``` 13 | //! use onedrive_api::{OneDrive, ItemLocation, option::ObjectOption}; 14 | //! use onedrive_api::resource::*; 15 | //! 16 | //! # async fn run(drive: &OneDrive) -> onedrive_api::Result<()> { 17 | //! // let drive: OneDrive; 18 | //! let item: Option = drive 19 | //! .get_item_with_option( 20 | //! ItemLocation::root(), 21 | //! ObjectOption::new() 22 | //! .if_none_match(&Tag("".to_owned())) 23 | //! // Only response `id` and `e_tag` to reduce data transmission. 24 | //! .select(&[DriveItemField::id, DriveItemField::e_tag]), 25 | //! ) 26 | //! .await?; 27 | //! match item { 28 | //! None => println!("Tag matched"), 29 | //! Some(item) => { 30 | //! println!("id: {:?}, e_tag: {:?}", item.id.unwrap(), item.e_tag.unwrap()); 31 | //! } 32 | //! } 33 | //! # Ok(()) 34 | //! # } 35 | //! ``` 36 | //! 37 | //! # See also 38 | //! [Microsoft Docs](https://docs.microsoft.com/en-us/onedrive/developer/rest-api/resources/?view=odsp-graph-online) 39 | //! 40 | //! [option]: ../option/index.html 41 | //! [select]: ../option/struct.ObjectOption.html#method.select 42 | //! [expand]: ../option/struct.ObjectOption.html#method.expand 43 | //! [one_drive]: ../struct.OneDrive.html 44 | //! [drive_item_field]: ./enum.DriveItemField.html 45 | use serde::{Deserialize, Serialize}; 46 | 47 | /// A semantic alias for URL string in resource objects. 48 | pub type Url = String; 49 | 50 | /// Boxed raw json value. 51 | pub type JsonValue = Box; 52 | 53 | /// Timestamp string with ISO 8601 format. 54 | pub type TimestampString = String; 55 | 56 | macro_rules! define_string_wrapper { 57 | ($($(#[$meta:meta])* $vis:vis $name:ident;)*) => { $( 58 | $(#[$meta])* 59 | #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] 60 | $vis struct $name(pub String); 61 | 62 | impl $name { 63 | /// View as str. 64 | #[inline] 65 | #[must_use] 66 | pub fn as_str(&self) -> &str { 67 | &self.0 68 | } 69 | } 70 | )* }; 71 | } 72 | 73 | define_string_wrapper! { 74 | /// Wrapper for a unique identifier to a `Drive`. 75 | /// 76 | /// # See also 77 | /// [Microsoft Docs: Drive resource type](https://docs.microsoft.com/en-us/graph/api/resources/drive?view=graph-rest-1.0) 78 | pub DriveId; 79 | 80 | /// Wrapper for a unique identifier for a `DriveItem`. 81 | /// 82 | /// # See also 83 | /// [Microsoft Docs: driveItem resource type](https://docs.microsoft.com/en-us/graph/api/resources/driveitem?view=graph-rest-1.0) 84 | pub ItemId; 85 | 86 | /// Wrapper for a tag representing the state of an item. 87 | /// 88 | /// Used for avoid data transmission when a resource is not modified. 89 | /// 90 | /// The tag from [`DriveItem::c_tag`][c_tag] is for the content of the item, 91 | /// while the one from [`DriveItem::e_tag`][e_tag] is for the entire item (metadata + content). 92 | /// 93 | /// # See also 94 | /// [Microsoft Docs: driveItem resource type](https://docs.microsoft.com/en-us/graph/api/resources/driveitem?view=graph-rest-1.0) 95 | /// 96 | /// [e_tag]: ./struct.DriveItem.html#structfield.e_tag 97 | /// [c_tag]: ./struct.DriveItem.html#structfield.c_tag 98 | pub Tag; 99 | } 100 | 101 | // Used for generalization over any resource field enums in `option`. 102 | #[doc(hidden)] 103 | #[allow(clippy::module_name_repetitions)] // Internal trait. 104 | pub trait ResourceField: Copy { 105 | fn __raw_name(self) -> &'static str; 106 | } 107 | 108 | macro_rules! define_resource_object { 109 | ($( 110 | $(#[$meta:meta])* 111 | $vis:vis struct $struct_name:ident #$field_enum_name:ident { 112 | $( 113 | $(#[$field_meta:meta])* 114 | pub $field_name:ident 115 | $(@$field_rename:literal)? 116 | : Option<$field_ty:ty>, 117 | )* 118 | } 119 | )*) => { 120 | $( 121 | $(#[$meta])* 122 | #[derive(Debug, Default, Clone, Deserialize, Serialize)] 123 | #[serde(rename_all = "camelCase")] 124 | #[non_exhaustive] 125 | $vis struct $struct_name { 126 | $( 127 | #[allow(missing_docs)] 128 | #[serde(skip_serializing_if="Option::is_none")] 129 | $(#[$field_meta])* 130 | $(#[serde(rename = $field_rename)])? 131 | pub $field_name: Option<$field_ty>, 132 | )* 133 | } 134 | 135 | /// Fields descriptors. 136 | /// 137 | /// More details in [mod documentation][mod]. 138 | /// 139 | /// [mod]: ./index.html 140 | #[derive(Debug, Clone, Copy, Eq, PartialEq, strum::VariantNames)] 141 | #[strum(serialize_all = "camelCase")] 142 | #[non_exhaustive] 143 | #[allow(missing_docs, non_camel_case_types)] 144 | $vis enum $field_enum_name { 145 | $( 146 | $(#[strum(serialize = $field_rename)])? 147 | $field_name, 148 | )* 149 | } 150 | 151 | impl $field_enum_name { 152 | /// Get the raw camelCase name of the field. 153 | #[must_use] 154 | pub fn raw_name(self) -> &'static str { 155 | ::VARIANTS[self as usize] 156 | } 157 | } 158 | 159 | impl ResourceField for $field_enum_name { 160 | #[inline] 161 | fn __raw_name(self) -> &'static str { 162 | self.raw_name() 163 | } 164 | } 165 | 166 | )* 167 | }; 168 | } 169 | 170 | define_resource_object! { 171 | /// Drive resource type 172 | /// 173 | /// The drive resource is the top level object representing a user's OneDrive 174 | /// or a document library in SharePoint. 175 | /// 176 | /// # See also 177 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/resources/drive?view=graph-rest-1.0) 178 | pub struct Drive #DriveField { 179 | pub id: Option, 180 | pub created_by: Option, 181 | pub created_date_time: Option, 182 | pub description: Option, 183 | pub drive_type: Option, 184 | pub items: Option>, 185 | pub last_modified_by: Option, 186 | pub last_modified_date_time: Option, 187 | pub name: Option, 188 | pub owner: Option, 189 | pub quota: Option, 190 | pub root: Option, 191 | pub sharepoint_ids: Option, 192 | pub special: Option>, 193 | pub system: Option, 194 | pub web_url: Option, 195 | } 196 | 197 | /// DriveItem resource type 198 | /// 199 | /// The `DriveItem` resource represents a file, folder, or other item stored in a drive. 200 | /// All file system objects in OneDrive and SharePoint are returned as `DriveItem` resources. 201 | /// 202 | /// # See also 203 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/resources/driveitem?view=graph-rest-1.0) 204 | pub struct DriveItem #DriveItemField { 205 | 206 | // Drive item 207 | 208 | pub audio: Option, 209 | pub bundle: Option, 210 | pub content: Option, 211 | pub c_tag: Option, 212 | pub deleted: Option, 213 | pub description: Option, 214 | pub file: Option, 215 | pub file_system_info: Option, 216 | pub folder: Option, 217 | pub image: Option, 218 | pub location: Option, 219 | pub package: Option, 220 | pub photo: Option, 221 | pub publication: Option, 222 | pub remote_item: Option, 223 | pub root: Option, 224 | pub search_result: Option, 225 | pub shared: Option, 226 | pub sharepoint_ids: Option, 227 | pub size: Option, 228 | pub special_folder: Option, 229 | pub video: Option, 230 | pub web_dav_url: Option, 231 | 232 | // Relationships 233 | 234 | pub children: Option>, 235 | pub created_by_user: Option, 236 | pub last_modified_by_user: Option, 237 | pub permissions: Option, 238 | pub thumbnails: Option, 239 | pub versions: Option, 240 | 241 | // Base item 242 | 243 | pub id: Option, 244 | pub created_by: Option, 245 | pub created_date_time: Option, 246 | pub e_tag: Option, 247 | pub last_modified_by: Option, 248 | pub last_modified_date_time: Option, 249 | pub name: Option, 250 | pub parent_reference: Option, 251 | pub web_url: Option, 252 | 253 | // Instance annotations 254 | 255 | // `@microsoft.graph.conflictBehavior` is write-only. 256 | 257 | /// The pre-authorized url for downloading the content. 258 | /// 259 | /// It is **NOT** selectable through [`ObjectOption::select`][select] and 260 | /// only provided in the result of [`OneDrive::get_item`][get_item] 261 | /// (or [`OneDrive::get_item_with_option`][get_item_with_opt]). 262 | /// 263 | /// [select]: ../option/struct.ObjectOption.html#method.select 264 | /// [get_item]: ../struct.OneDrive.html#method.get_item 265 | /// [get_item_with_opt]: ../struct.OneDrive.html#method.get_item_with_option 266 | pub download_url @"@microsoft.graph.downloadUrl": Option, 267 | 268 | // `@microsoft.graph.sourceUrl` is write-only 269 | } 270 | } 271 | 272 | /// The error resource type, returned whenever an error occurs in the processing of a request. 273 | /// 274 | /// Error responses follow the definition in the OData v4 specification for error responses. 275 | /// 276 | /// **This struct is independent with [`OAuth2ErrorResponse`][oauth2_error_response] from OAuth2 API.** 277 | /// 278 | /// It may be contained in [`onedrive_api::Error`][error] returned by storage API 279 | /// (methods of [`OneDrive`][one_drive], [`ListChildrenFetcher`][list_children_fetcher], etc.). 280 | /// 281 | /// # See also 282 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/errors#error-resource-type) 283 | /// 284 | /// [oauth2_error_response]: ./struct.OAuth2ErrorResponse.html 285 | /// [error]: ../struct.Error.html 286 | /// [one_drive]: ../struct.OneDrive.html 287 | /// [list_children_fetcher]: ../struct.ListChildrenFetcher.html 288 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 289 | #[non_exhaustive] 290 | pub struct ErrorResponse { 291 | /// OData `code`. Non-exhaustive. 292 | /// 293 | /// Some possible values of `code` field can be found in: 294 | /// - [Error resource type: code property](https://docs.microsoft.com/en-us/graph/errors#code-property) 295 | /// - [Error codes for authorization endpoint errors](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#error-codes-for-authorization-endpoint-errors) 296 | /// - And maybe more. 297 | pub code: String, 298 | /// OData `message`. Usually to be human-readable. 299 | pub message: String, 300 | /// OData `innererror`. An optional object with additional or more specific error codes. 301 | #[serde(rename = "innererror")] 302 | pub inner_error: Option>, 303 | } 304 | 305 | /// OAuth2 error response. 306 | /// 307 | /// **This struct is independent with [`ErrorResponse`][error_response] from storage API.** 308 | /// 309 | /// It can only be contained in [`onedrive_api::Error`][error] returned by operations 310 | /// about OAuth2 (methods of [`Auth`][auth]). 311 | /// 312 | /// # See also 313 | /// - [Microsoft Docs: Request an authorization code](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#error-response) 314 | /// - [Microsoft Docs: Request an access token](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#error-response-1) 315 | /// - [Microsoft Docs: Refresh the access token](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#error-response-2) 316 | /// 317 | /// [error_response]: ./struct.ErrorResponse.html 318 | /// [error]: ../struct.Error.html 319 | /// [auth]: ../struct.Auth.html 320 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 321 | #[allow(missing_docs)] 322 | #[non_exhaustive] 323 | pub struct OAuth2ErrorResponse { 324 | pub error: String, 325 | pub error_description: String, 326 | pub error_codes: Option>, 327 | pub timestamp: Option, 328 | pub trace_id: Option, 329 | pub correlation_id: Option, 330 | } 331 | 332 | #[cfg(test)] 333 | mod tests { 334 | use super::*; 335 | 336 | #[test] 337 | fn test_raw_name() { 338 | assert_eq!(DriveField::id.raw_name(), "id"); 339 | assert_eq!(DriveField::drive_type.raw_name(), "driveType"); 340 | assert_eq!(DriveField::owner.raw_name(), "owner"); 341 | assert_eq!(DriveField::web_url.raw_name(), "webUrl"); 342 | 343 | assert_eq!(DriveItemField::id.raw_name(), "id"); 344 | assert_eq!( 345 | DriveItemField::file_system_info.raw_name(), 346 | "fileSystemInfo" 347 | ); 348 | assert_eq!(DriveItemField::size.raw_name(), "size"); 349 | assert_eq!(DriveItemField::web_dav_url.raw_name(), "webDavUrl"); 350 | assert_eq!(DriveItemField::web_url.raw_name(), "webUrl"); 351 | 352 | assert_eq!( 353 | DriveItemField::download_url.raw_name(), 354 | "@microsoft.graph.downloadUrl", 355 | ); 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/auth.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::{ 4 | error::{Error, Result}, 5 | util::handle_oauth2_error_response, 6 | }; 7 | use reqwest::Client; 8 | use serde::Deserialize; 9 | use url::Url; 10 | 11 | /// A list of the Microsoft Graph permissions that you want the user to consent to. 12 | /// 13 | /// # See also 14 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/permissions-reference#files-permissions) 15 | #[derive(Clone, Copy, Debug, Default)] 16 | pub struct Permission { 17 | write: bool, 18 | access_shared: bool, 19 | offline_access: bool, 20 | } 21 | 22 | impl Permission { 23 | /// Create a read-only permission. 24 | /// 25 | /// Note that the permission is at least to allow reading. 26 | #[must_use] 27 | pub fn new_read() -> Self { 28 | Self::default() 29 | } 30 | 31 | /// Set the write permission. 32 | #[must_use] 33 | pub fn write(mut self, write: bool) -> Self { 34 | self.write = write; 35 | self 36 | } 37 | 38 | /// Set the permission to the shared files. 39 | #[must_use] 40 | pub fn access_shared(mut self, access_shared: bool) -> Self { 41 | self.access_shared = access_shared; 42 | self 43 | } 44 | 45 | /// Set whether allows offline access. 46 | /// 47 | /// This permission is required to get a [`TokenResponse::refresh_token`] for long time access. 48 | /// 49 | /// # See also 50 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/permissions-reference#delegated-permissions-21) 51 | #[must_use] 52 | pub fn offline_access(mut self, offline_access: bool) -> Self { 53 | self.offline_access = offline_access; 54 | self 55 | } 56 | 57 | #[must_use] 58 | #[rustfmt::skip] 59 | fn to_scope_string(self) -> String { 60 | format!( 61 | "{}{}{}", 62 | if self.write { "files.readwrite" } else { "files.read" }, 63 | if self.access_shared { ".all" } else { "" }, 64 | if self.offline_access { " offline_access" } else { "" }, 65 | ) 66 | } 67 | } 68 | 69 | /// Control who can sign into the application. 70 | /// 71 | /// It must match the target audience configuration of registered application. 72 | /// 73 | /// See: 74 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 75 | pub enum Tenant { 76 | /// For both Microsoft accounts and work or school accounts. 77 | /// 78 | /// # Notes 79 | /// 80 | /// This is only allowed for application with type `AzureADandPersonalMicrosoftAccount` 81 | /// (Accounts in any organizational directory (Any Microsoft Entra directory - Multitenant) and 82 | /// personal Microsoft accounts (e.g. Skype, Xbox)). If the corresponding application by 83 | /// Client ID does not have this type, authentications will fail unconditionally. 84 | /// 85 | /// See: 86 | /// 87 | Common, 88 | /// For work or school accounts only. 89 | Organizations, 90 | /// For Microsoft accounts only. 91 | Consumers, 92 | /// Tenant identifiers such as the tenant ID or domain name. 93 | /// 94 | /// See: 95 | Issuer(String), 96 | } 97 | 98 | impl Tenant { 99 | fn to_issuer(&self) -> &str { 100 | match self { 101 | Tenant::Common => "common", 102 | Tenant::Organizations => "organizations", 103 | Tenant::Consumers => "consumers", 104 | Tenant::Issuer(s) => s, 105 | } 106 | } 107 | } 108 | 109 | /// OAuth2 authentication and authorization basics for Microsoft Graph. 110 | /// 111 | /// # See also 112 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/auth/auth-concepts?view=graph-rest-1.0) 113 | #[derive(Debug, Clone)] 114 | pub struct Auth { 115 | client: Client, 116 | client_id: String, 117 | permission: Permission, 118 | redirect_uri: String, 119 | tenant: Tenant, 120 | } 121 | 122 | impl Auth { 123 | /// Create an new instance for OAuth2 to Microsoft Graph 124 | /// with specified client identifier and permission. 125 | pub fn new( 126 | client_id: impl Into, 127 | permission: Permission, 128 | redirect_uri: impl Into, 129 | tenant: Tenant, 130 | ) -> Self { 131 | Self::new_with_client(Client::new(), client_id, permission, redirect_uri, tenant) 132 | } 133 | 134 | /// Same as [`Auth::new`][auth_new] but with custom `reqwest::Client`. 135 | /// 136 | /// [auth_new]: #method.new 137 | pub fn new_with_client( 138 | client: Client, 139 | client_id: impl Into, 140 | permission: Permission, 141 | redirect_uri: impl Into, 142 | tenant: Tenant, 143 | ) -> Self { 144 | Self { 145 | client, 146 | client_id: client_id.into(), 147 | permission, 148 | redirect_uri: redirect_uri.into(), 149 | tenant, 150 | } 151 | } 152 | 153 | /// Get the `client` used to create this instance. 154 | #[must_use] 155 | pub fn client(&self) -> &Client { 156 | &self.client 157 | } 158 | 159 | /// Get the `client_id` used to create this instance. 160 | #[must_use] 161 | pub fn client_id(&self) -> &str { 162 | &self.client_id 163 | } 164 | 165 | /// Get the `permission` used to create this instance. 166 | #[must_use] 167 | pub fn permission(&self) -> &Permission { 168 | &self.permission 169 | } 170 | 171 | /// Get the `redirect_uri` used to create this instance. 172 | #[must_use] 173 | pub fn redirect_uri(&self) -> &str { 174 | &self.redirect_uri 175 | } 176 | 177 | /// Get the `tenant` used to create this instance. 178 | #[must_use] 179 | pub fn tenant(&self) -> &Tenant { 180 | &self.tenant 181 | } 182 | 183 | #[must_use] 184 | fn endpoint_url(&self, endpoint: &str) -> Url { 185 | let mut url = Url::parse("https://login.microsoftonline.com").unwrap(); 186 | url.path_segments_mut().unwrap().extend([ 187 | self.tenant.to_issuer(), 188 | "oauth2", 189 | "v2.0", 190 | endpoint, 191 | ]); 192 | url 193 | } 194 | 195 | /// Get the URL for web browser for code flow. 196 | /// 197 | /// # See also 198 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/auth-v2-user?view=graph-rest-1.0#authorization-request) 199 | #[must_use] 200 | pub fn code_auth_url(&self) -> Url { 201 | let mut url = self.endpoint_url("authorize"); 202 | url.query_pairs_mut() 203 | .append_pair("client_id", &self.client_id) 204 | .append_pair("scope", &self.permission.to_scope_string()) 205 | .append_pair("redirect_uri", &self.redirect_uri) 206 | .append_pair("response_type", "code"); 207 | url 208 | } 209 | 210 | async fn request_token<'a>( 211 | &self, 212 | require_refresh: bool, 213 | params: impl Iterator, 214 | ) -> Result { 215 | let url = self.endpoint_url("token"); 216 | let params = params.collect::>(); 217 | let resp = self.client.post(url).form(¶ms).send().await?; 218 | 219 | // Handle special error response. 220 | let token_resp: TokenResponse = handle_oauth2_error_response(resp).await?.json().await?; 221 | 222 | if require_refresh && token_resp.refresh_token.is_none() { 223 | return Err(Error::unexpected_response("Missing field `refresh_token`")); 224 | } 225 | 226 | Ok(token_resp) 227 | } 228 | 229 | /// Login using a code. 230 | /// 231 | /// # See also 232 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/auth-v2-user?view=graph-rest-1.0#3-get-a-token) 233 | pub async fn login_with_code( 234 | &self, 235 | code: &str, 236 | client_credential: &ClientCredential, 237 | ) -> Result { 238 | self.request_token( 239 | self.permission.offline_access, 240 | [ 241 | ("client_id", &self.client_id as &str), 242 | ("code", code), 243 | ("grant_type", "authorization_code"), 244 | ("redirect_uri", &self.redirect_uri), 245 | ] 246 | .into_iter() 247 | .chain(client_credential.params()), 248 | ) 249 | .await 250 | } 251 | 252 | /// Login using a refresh token. 253 | /// 254 | /// This requires [`offline_access`][offline_access], and will **ALWAYS** return 255 | /// a new [`refresh_token`][refresh_token] if success. 256 | /// 257 | /// # Panics 258 | /// Panic if the current [`Auth`][auth] is created with no 259 | /// [`offline_access`][offline_access] permission. 260 | /// 261 | /// # See also 262 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/auth-v2-user?view=graph-rest-1.0#5-use-the-refresh-token-to-get-a-new-access-token) 263 | /// 264 | /// [auth]: ./struct.Auth.html 265 | /// [offline_access]: ./struct.Permission.html#method.offline_access 266 | /// [refresh_token]: ./struct.TokenResponse.html#structfield.refresh_token 267 | pub async fn login_with_refresh_token( 268 | &self, 269 | refresh_token: &str, 270 | client_credential: &ClientCredential, 271 | ) -> Result { 272 | assert!( 273 | self.permission.offline_access, 274 | "Refresh token requires offline_access permission." 275 | ); 276 | 277 | self.request_token( 278 | true, 279 | [ 280 | ("client_id", &self.client_id as &str), 281 | ("grant_type", "refresh_token"), 282 | ("redirect_uri", &self.redirect_uri), 283 | ("refresh_token", refresh_token), 284 | ] 285 | .into_iter() 286 | .chain(client_credential.params()), 287 | ) 288 | .await 289 | } 290 | } 291 | 292 | /// Credential of client for code redeemption. 293 | /// 294 | /// See: 295 | /// 296 | #[derive(Default, Clone, PartialEq, Eq)] 297 | #[non_exhaustive] 298 | pub enum ClientCredential { 299 | /// Nothing. 300 | /// 301 | /// This is the usual case for non-confidential native apps. 302 | #[default] 303 | None, 304 | /// The application secret that you created in the app registration portal for your app. 305 | /// 306 | /// Don't use the application secret in a native app or single page app because a 307 | /// `client_secret` can't be reliably stored on devices or web pages. 308 | /// 309 | /// See: 310 | /// 311 | Secret(String), 312 | /// An assertion, which is a JSON web token (JWT), that you need to create and sign with the 313 | /// certificate you registered as credentials for your application. 314 | /// 315 | /// See: 316 | /// 317 | Assertion(String), 318 | } 319 | 320 | impl fmt::Debug for ClientCredential { 321 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 322 | match self { 323 | Self::None => write!(f, "None"), 324 | Self::Secret(_) => f.debug_struct("Secret").finish_non_exhaustive(), 325 | Self::Assertion(_) => f.debug_struct("Assertion").finish_non_exhaustive(), 326 | } 327 | } 328 | } 329 | 330 | impl ClientCredential { 331 | fn params(&self) -> impl Iterator { 332 | let (a, b) = match self { 333 | ClientCredential::None => (None, None), 334 | ClientCredential::Secret(s) => (Some(("client_secret", &**s)), None), 335 | ClientCredential::Assertion(s) => ( 336 | Some(( 337 | "client_assertion_type", 338 | "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 339 | )), 340 | Some(("client_assertion", &**s)), 341 | ), 342 | }; 343 | a.into_iter().chain(b) 344 | } 345 | } 346 | 347 | /// Tokens and some additional data returned by a successful authorization. 348 | /// 349 | /// # See also 350 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/auth-v2-user?view=graph-rest-1.0#token-response) 351 | #[derive(Clone, Deserialize)] 352 | #[non_exhaustive] 353 | pub struct TokenResponse { 354 | /// Indicates the token type value. The only type that Azure AD supports is Bearer. 355 | pub token_type: String, 356 | /// A list of the Microsoft Graph permissions that the `access_token` is valid for. 357 | #[serde(deserialize_with = "space_separated_strings")] 358 | pub scope: Vec, 359 | /// How long the access token is valid (in seconds). 360 | #[serde(rename = "expires_in")] 361 | pub expires_in_secs: u64, 362 | /// The access token used for authorization in requests. 363 | /// 364 | /// # See also 365 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/auth-overview#what-is-an-access-token-and-how-do-i-use-it) 366 | pub access_token: String, 367 | /// The refresh token for refreshing (re-get) an access token when the previous one expired. 368 | /// 369 | /// This is only returned in code auth flow with [`offline_access`][offline_access] permission. 370 | /// 371 | /// # See also 372 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/auth-v2-user?view=graph-rest-1.0#5-use-the-refresh-token-to-get-a-new-access-token) 373 | /// 374 | /// [offline_access]: ./struct.Permission.html#method.offline_access 375 | pub refresh_token: Option, 376 | } 377 | 378 | impl fmt::Debug for TokenResponse { 379 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 380 | f.debug_struct("TokenResponse") 381 | .field("token_type", &self.token_type) 382 | .field("scope", &self.scope) 383 | .field("expires_in_secs", &self.expires_in_secs) 384 | .finish_non_exhaustive() 385 | } 386 | } 387 | 388 | fn space_separated_strings<'de, D>(deserializer: D) -> std::result::Result, D::Error> 389 | where 390 | D: serde::de::Deserializer<'de>, 391 | { 392 | struct Visitor; 393 | 394 | impl serde::de::Visitor<'_> for Visitor { 395 | type Value = Vec; 396 | 397 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 398 | formatter.write_str("space-separated strings") 399 | } 400 | 401 | fn visit_str(self, s: &str) -> std::result::Result 402 | where 403 | E: serde::de::Error, 404 | { 405 | Ok(s.split(' ').map(Into::into).collect()) 406 | } 407 | } 408 | 409 | deserializer.deserialize_str(Visitor) 410 | } 411 | 412 | #[cfg(test)] 413 | mod tests { 414 | use super::*; 415 | 416 | #[test] 417 | fn auth_url() { 418 | let perm = Permission::new_read().write(true).offline_access(true); 419 | let auth = Auth::new( 420 | "some-client-id", 421 | perm, 422 | "http://example.com", 423 | Tenant::Consumers, 424 | ); 425 | assert_eq!( 426 | auth.code_auth_url().as_str(), 427 | "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=some-client-id&scope=files.readwrite+offline_access&redirect_uri=http%3A%2F%2Fexample.com&response_type=code", 428 | ); 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /onedrive-api-test/tests/real_test/main.rs: -------------------------------------------------------------------------------- 1 | //! Test read and write APIs by sending real requests to Microsoft Onedrive 2 | //! with token and `refresh_token` provided. Requires network access. 3 | //! 4 | //! **DANGER:** 5 | //! This may MODIFY YOUR FILES on OneDrive! Although the test is written 6 | //! to avoid overwriting or removing existing data, you may still TAKE SOME RISKS. 7 | //! 8 | //! You should ALWAYS use a new test-only Microsoft account 9 | //! without any important file in its Onedrive. 10 | //! 11 | //! Refresh token should be provided through environment `ONEDRIVE_API_TEST_REFRESH_TOKEN`. 12 | //! Binary target of `onedrive-api-test` is a helper to get it. 13 | #![allow(clippy::redundant_clone)] 14 | use onedrive_api::{option::*, resource::*, *}; 15 | use reqwest::StatusCode; 16 | use serde_json::json; 17 | 18 | mod util; 19 | use util::*; 20 | 21 | use util::get_logined_onedrive as onedrive; 22 | 23 | // 3 requests 24 | #[tokio::test] 25 | async fn test_get_drive() { 26 | let onedrive = onedrive().await; 27 | 28 | // #1 29 | let drive1 = onedrive.get_drive().await.expect("Cannot get drive #1"); 30 | assert!(drive1.quota.is_some()); 31 | assert!(drive1.owner.is_some()); 32 | 33 | let drive_id = drive1.id.as_ref().expect("drive1 has no id"); 34 | 35 | // #2 36 | let drive2 = OneDrive::new(onedrive.access_token(), drive_id.clone()) 37 | .get_drive_with_option(ObjectOption::new().select(&[DriveField::id, DriveField::owner])) 38 | .await 39 | .expect("Cannot get drive #2"); 40 | assert_eq!(&drive1.id, &drive2.id); // Checked to be `Some`. 41 | assert_eq!(&drive1.owner, &drive2.owner); // Checked to be `Some`. 42 | assert!(drive2.quota.is_none(), "drive2 contains unselected `quota`"); 43 | 44 | // #3 45 | assert_eq!( 46 | OneDrive::new( 47 | onedrive.access_token(), 48 | DriveId(format!("{}_inva_lid", drive_id.as_str())), 49 | ) 50 | .get_drive() 51 | .await 52 | .expect_err("Drive id should be invalid") 53 | .status_code(), 54 | // This API returns 400 instead of 404 55 | Some(StatusCode::BAD_REQUEST), 56 | ); 57 | } 58 | 59 | // 3 requests 60 | #[tokio::test] 61 | async fn test_get_item() { 62 | let onedrive = onedrive().await; 63 | 64 | // #1 65 | let item_by_path = onedrive 66 | .get_item(ItemLocation::from_path("/").unwrap()) 67 | .await 68 | .expect("Cannot get item by path"); 69 | let item_id = item_by_path.id.clone().expect("Missing `id`"); 70 | 71 | // #2 72 | let item_by_id = onedrive 73 | .get_item(&item_id) 74 | .await 75 | .expect("Cannot get item by id"); 76 | assert_eq!(item_by_path.id, item_by_id.id); 77 | assert_eq!(item_by_path.e_tag, item_by_id.e_tag); 78 | assert_eq!(item_by_path.name, item_by_id.name); 79 | // Some fields may change since other tests will modify the content of root dir. 80 | 81 | // #3 82 | let item_custom = onedrive 83 | .get_item_with_option(&item_id, ObjectOption::new().select(&[DriveItemField::id])) 84 | .await 85 | .expect("Cannot get item with option") 86 | .expect("No if-none-match"); 87 | assert_eq!(item_custom.id.as_ref(), Some(&item_id), "`id` mismatch",); 88 | assert!(item_custom.size.is_none(), "`size` should not be selected"); 89 | 90 | // `If-None-Match` may be ignored by server. 91 | // So we don't test it. 92 | } 93 | 94 | // 7 requests 95 | #[tokio::test] 96 | async fn test_folder_create_and_list_children() { 97 | fn to_names(v: Vec) -> Vec { 98 | let mut v = v 99 | .into_iter() 100 | .map(|item| item.name.expect("Missing `name`")) 101 | .collect::>(); 102 | v.sort(); 103 | v 104 | } 105 | 106 | let onedrive = onedrive().await; 107 | 108 | let container_name = gen_filename(); 109 | let container_loc = rooted_location(container_name); 110 | let (sub_name1, sub_name2) = (gen_filename(), gen_filename()); 111 | assert!(sub_name1.as_str() < sub_name2.as_str()); // Monotonic 112 | let items_origin = vec![sub_name1.as_str().to_owned(), sub_name2.as_str().to_owned()]; 113 | 114 | // #1 115 | onedrive 116 | .create_folder(ItemLocation::root(), container_name) 117 | .await 118 | .expect("Cannot create folder"); 119 | 120 | onedrive 121 | .create_folder(container_loc, sub_name1) 122 | .await 123 | .expect("Cannot create sub folder 1"); 124 | onedrive 125 | .create_folder(container_loc, sub_name2) 126 | .await 127 | .expect("Cannot create sub folder 2"); 128 | 129 | // #2 130 | let mut fetcher = onedrive 131 | .list_children_with_option( 132 | container_loc, 133 | CollectionOption::new() 134 | .select(&[DriveItemField::name, DriveItemField::e_tag]) 135 | .page_size(1), 136 | ) 137 | .await 138 | .expect("Cannot list children with option") 139 | .expect("No if-none-match"); 140 | 141 | assert!( 142 | fetcher.next_url().is_none(), 143 | "`next_url` should be None before page 1", 144 | ); 145 | 146 | // No request for the first page 147 | let t = std::time::Instant::now(); 148 | let page1 = fetcher 149 | .fetch_next_page(&onedrive) 150 | .await 151 | .expect("Cannot fetch page 1") 152 | .expect("Page 1 should not be None"); 153 | let elapsed = t.elapsed(); 154 | assert!( 155 | elapsed < std::time::Duration::from_millis(1), 156 | "The first page should be cached", 157 | ); 158 | assert_eq!(page1.len(), 1); 159 | 160 | assert!( 161 | fetcher.next_url().is_some(), 162 | "`next_url` should be Some before page 2", 163 | ); 164 | 165 | // #3 166 | let page2 = fetcher 167 | .fetch_next_page(&onedrive) 168 | .await 169 | .expect("Cannot fetch page 2") 170 | .expect("Page 2 should not be None"); 171 | assert_eq!(page2.len(), 1); 172 | 173 | assert!( 174 | fetcher 175 | .fetch_next_page(&onedrive) 176 | .await 177 | .expect("Cannot fetch page 3") 178 | .is_none(), 179 | "Expected to have only 2 pages", 180 | ); 181 | 182 | let mut items_manual = page1; 183 | items_manual.extend(page2); 184 | assert!( 185 | items_manual.iter().all(|c| c.size.is_none()), 186 | "`size` should be not be selected", 187 | ); 188 | let items_manual = to_names(items_manual); 189 | 190 | // #4, #5 191 | let items_shortcut = onedrive 192 | .list_children(container_loc) 193 | .await 194 | .expect("Cannot list children"); 195 | let items_shortcut = to_names(items_shortcut); 196 | 197 | // #6 198 | let items_expand = onedrive 199 | .get_item_with_option( 200 | container_loc, 201 | ObjectOption::new().expand(DriveItemField::children, Some(&["name"])), 202 | ) 203 | .await 204 | .expect("Cannot get item with children") 205 | .expect("No `If-None-Match`") 206 | .children 207 | .expect("Missing `children`"); 208 | let items_expand = to_names(items_expand); 209 | 210 | assert_eq!(items_origin, items_manual); 211 | assert_eq!(items_origin, items_shortcut); 212 | assert_eq!(items_origin, items_expand); 213 | 214 | // #7 215 | onedrive.delete(container_loc).await.unwrap(); 216 | } 217 | 218 | // 4 requests 219 | #[tokio::test] 220 | async fn test_folder_create_and_delete() { 221 | let onedrive = onedrive().await; 222 | 223 | let folder_name = gen_filename(); 224 | let folder_loc = rooted_location(folder_name); 225 | let invalid_path = format!("/{}/invalid", folder_name.as_str()); 226 | let invalid_loc = ItemLocation::from_path(&invalid_path).unwrap(); 227 | 228 | // #1 229 | onedrive 230 | .create_folder(ItemLocation::root(), folder_name) 231 | .await 232 | .expect("Cannot create folder"); 233 | 234 | // #2 235 | assert_eq!( 236 | onedrive 237 | .create_folder(ItemLocation::root(), folder_name) 238 | .await 239 | .expect_err("Re-create folder should fail by default") 240 | .status_code(), 241 | Some(StatusCode::CONFLICT), 242 | ); 243 | 244 | // #3 245 | assert_eq!( 246 | onedrive 247 | .delete(invalid_loc) 248 | .await 249 | .expect_err("Should not delete non-existent folder") 250 | .status_code(), 251 | Some(StatusCode::NOT_FOUND), 252 | ); 253 | 254 | // #4 255 | onedrive.delete(folder_loc).await.unwrap(); 256 | } 257 | 258 | // 4 requests 259 | #[tokio::test] 260 | async fn test_folder_create_and_update() { 261 | const FAKE_TIME: &str = "2017-01-01T00:00:00Z"; 262 | 263 | fn get_bmtime(item: &DriveItem) -> Option<(&str, &str)> { 264 | let fs_info = item.file_system_info.as_ref()?.as_object()?; 265 | Some(( 266 | fs_info.get("createdDateTime")?.as_str()?, 267 | fs_info.get("lastModifiedDateTime")?.as_str()?, 268 | )) 269 | } 270 | 271 | let onedrive = onedrive().await; 272 | 273 | let folder_name = gen_filename(); 274 | let folder_loc = rooted_location(folder_name); 275 | 276 | // #1 277 | let item_before = onedrive 278 | .create_folder(ItemLocation::root(), folder_name) 279 | .await 280 | .expect("Cannot create folder"); 281 | 282 | let (birth_time_before, modified_time_before) = 283 | get_bmtime(&item_before).expect("Invalid file_system_info before update"); 284 | assert_ne!(birth_time_before, FAKE_TIME); 285 | assert_ne!(modified_time_before, FAKE_TIME); 286 | 287 | // #2 288 | let mut patch = DriveItem::default(); 289 | patch.file_system_info = Some(Box::new(json!({ 290 | "createdDateTime": FAKE_TIME, 291 | "lastModifiedDateTime": FAKE_TIME, 292 | }))); 293 | let item_response = onedrive 294 | .update_item(folder_loc, &patch) 295 | .await 296 | .expect("Cannot update folder metadata"); 297 | assert_eq!(get_bmtime(&item_response), Some((FAKE_TIME, FAKE_TIME))); 298 | 299 | // #3 300 | let item_after = onedrive 301 | .get_item(folder_loc) 302 | .await 303 | .expect("Cannot get folder before update"); 304 | assert_eq!(get_bmtime(&item_after), Some((FAKE_TIME, FAKE_TIME))); 305 | 306 | // #4 307 | onedrive.delete(folder_loc).await.unwrap(); 308 | } 309 | 310 | // 6 requests 311 | #[tokio::test] 312 | async fn test_file_upload_small_and_move() { 313 | // Different length, since we use `size` to check if replacement is successful. 314 | const CONTENT1: &[u8] = b"aaa"; 315 | const CONTENT2: &[u8] = b"bbbbbb"; 316 | 317 | let onedrive = onedrive().await; 318 | 319 | let file1_loc = rooted_location(gen_filename()); 320 | let file2_name = gen_filename(); 321 | let file2_loc = rooted_location(file2_name); 322 | 323 | // #1 324 | onedrive 325 | .upload_small(file1_loc, CONTENT1) 326 | .await 327 | .expect("Cannot upload file 1"); 328 | 329 | // #2 330 | onedrive 331 | .upload_small(file2_loc, CONTENT2) 332 | .await 333 | .expect("Cannot upload file 2"); 334 | 335 | // #3 336 | assert_eq!( 337 | onedrive 338 | .move_(file1_loc, ItemLocation::root(), Some(file2_name)) 339 | .await 340 | .expect_err("Should not move with overwrite by default") 341 | .status_code(), 342 | Some(StatusCode::CONFLICT), 343 | ); 344 | 345 | // #4 346 | onedrive 347 | .move_with_option( 348 | file1_loc, 349 | ItemLocation::root(), 350 | Some(file2_name), 351 | DriveItemPutOption::new().conflict_behavior(ConflictBehavior::Replace), 352 | ) 353 | .await 354 | .expect("Cannot move with overwrite"); 355 | 356 | // #5 357 | let size = onedrive 358 | .get_item(file2_loc) 359 | .await 360 | .expect("Cannot get file2") 361 | .size 362 | .expect("Missing `size`"); 363 | assert_eq!( 364 | usize::try_from(size).expect("value overflow"), 365 | // Content is replaced. 366 | CONTENT1.len(), 367 | ); 368 | 369 | // #6 370 | // `file1_loc` is already moved. 371 | onedrive.delete(file2_loc).await.unwrap(); 372 | } 373 | 374 | // 5 requests 375 | #[tokio::test] 376 | async fn test_file_upload_small_and_copy() { 377 | const CONTENT: &[u8] = b"hello, copy"; 378 | const WAIT_TIME: std::time::Duration = std::time::Duration::from_millis(1000); 379 | const MAX_WAIT_COUNT: usize = 5; 380 | 381 | let onedrive = onedrive().await; 382 | 383 | let name1 = gen_filename(); 384 | let name2 = gen_filename(); 385 | let loc1 = rooted_location(name1); 386 | let loc2 = rooted_location(name2); 387 | 388 | // #1 389 | onedrive 390 | .upload_small(loc1, CONTENT) 391 | .await 392 | .expect("Cannot upload file"); 393 | 394 | // #2 395 | let monitor = onedrive 396 | .copy(loc1, ItemLocation::root(), name2) 397 | .await 398 | .expect("Cannot start copy"); 399 | for i in 0.. { 400 | std::thread::sleep(WAIT_TIME); 401 | 402 | // #3 403 | match monitor 404 | .fetch_progress(&onedrive) 405 | .await 406 | .expect("Failed to check `copy` progress") 407 | .status 408 | { 409 | CopyStatus::NotStarted | CopyStatus::InProgress => {} 410 | CopyStatus::Completed => break, 411 | status => panic!("Unexpected fail of `copy`: {status:?}"), 412 | } 413 | 414 | assert!(i < MAX_WAIT_COUNT, "Copy timeout"); 415 | } 416 | 417 | // #4, #5 418 | onedrive.delete(loc2).await.unwrap(); 419 | onedrive.delete(loc1).await.unwrap(); 420 | } 421 | 422 | // 8 requests 423 | #[tokio::test] 424 | async fn test_file_upload_session() { 425 | type Range = std::ops::Range; 426 | const CONTENT: &[u8] = b"12345678"; 427 | const CONTENT_LEN: u64 = CONTENT.len() as u64; 428 | const RANGE1: Range = 0..2; 429 | const RANGE2_ERROR: Range = 6..8; 430 | const RANGE2: Range = 2..8; 431 | 432 | fn as_range_u64(r: Range) -> std::ops::Range { 433 | r.start as u64..r.end as u64 434 | } 435 | 436 | let onedrive = onedrive().await; 437 | let item_loc = rooted_location(gen_filename()); 438 | 439 | // #1 440 | let (sess, meta1) = onedrive 441 | .new_upload_session(item_loc) 442 | .await 443 | .expect("Cannot create upload session"); 444 | 445 | println!( 446 | "Upload session will expire at {:?}", 447 | meta1.expiration_date_time, 448 | ); 449 | 450 | // #2 451 | assert!( 452 | sess.upload_part( 453 | &CONTENT[RANGE1], 454 | as_range_u64(RANGE1), 455 | CONTENT_LEN, 456 | onedrive.client() 457 | ) 458 | .await 459 | .expect("Cannot upload part 1") 460 | .is_none(), 461 | "Uploading part 1 should not complete", 462 | ); 463 | 464 | // #3 465 | let meta2 = sess 466 | .get_meta(onedrive.client()) 467 | .await 468 | .expect("Cannot get metadata of the upload session"); 469 | let next_ranges = &meta2.next_expected_ranges; 470 | assert!( 471 | next_ranges.len() == 1 472 | && next_ranges[0].start == RANGE2.start as u64 473 | && next_ranges[0].end.map_or(true, |x| x == RANGE2.end as u64), 474 | "Invalid `next_expected_ranges`: {next_ranges:?}" 475 | ); 476 | 477 | // #4 478 | assert_eq!( 479 | sess.upload_part( 480 | &CONTENT[RANGE2_ERROR], 481 | as_range_u64(RANGE2_ERROR), 482 | CONTENT_LEN, 483 | onedrive.client(), 484 | ) 485 | .await 486 | .expect_err("Upload wrong range should fail") 487 | .status_code(), 488 | Some(StatusCode::RANGE_NOT_SATISFIABLE), 489 | ); 490 | 491 | // #5 492 | sess.upload_part( 493 | &CONTENT[RANGE2], 494 | as_range_u64(RANGE2), 495 | CONTENT_LEN, 496 | onedrive.client(), 497 | ) 498 | .await 499 | .expect("Failed to upload part 2") 500 | .expect("Uploading should be completed"); 501 | 502 | // #6 503 | let download_url = onedrive.get_item_download_url(item_loc).await.unwrap(); 504 | 505 | // #7 506 | assert_eq!(download(&download_url).await, CONTENT); 507 | 508 | // #8 509 | onedrive.delete(item_loc).await.unwrap(); 510 | } 511 | 512 | // 8 requests 513 | // This test fetch all changes from root folder, which may contains lots of files and take lots of time. 514 | #[tokio::test] 515 | #[ignore] 516 | async fn test_track_changes() { 517 | use std::collections::HashSet; 518 | 519 | let onedrive = onedrive().await; 520 | 521 | let container_name = gen_filename(); 522 | let container_loc = rooted_location(container_name); 523 | 524 | // #1 525 | let container_id = onedrive 526 | .create_folder(ItemLocation::root(), container_name) 527 | .await 528 | .expect("Cannot create container folder") 529 | .id 530 | .expect("Missing `id`"); 531 | 532 | // #2 533 | let folder1_id = onedrive 534 | .create_folder(container_loc, gen_filename()) 535 | .await 536 | .expect("Failed to create folder1") 537 | .id 538 | .expect("Missing `id`"); 539 | 540 | // #3 541 | let folder2_id = onedrive 542 | .create_folder(container_loc, gen_filename()) 543 | .await 544 | .expect("Failed to create folder2") 545 | .id 546 | .expect("Missing `id`"); 547 | 548 | { 549 | // #4 550 | let (initial_changes, _) = onedrive 551 | .track_root_changes_from_initial() 552 | .await 553 | .expect("Cannot track initial changes") 554 | .fetch_all(&onedrive) 555 | .await 556 | .expect("Cannot fetch all initial changes"); 557 | 558 | // Items may duplicate. 559 | // See: https://docs.microsoft.com/en-us/graph/api/driveitem-delta?view=graph-rest-1.0#remarks 560 | let ids = initial_changes 561 | .into_iter() 562 | .map(|item| item.id.expect("Missing `id`")) 563 | .collect::>(); 564 | // We track changes of root directory, so there may be other files. 565 | assert!(ids.contains(&container_id)); 566 | assert!(ids.contains(&folder1_id)); 567 | assert!(ids.contains(&folder2_id)); 568 | } 569 | 570 | // #5 571 | let delta_url = onedrive 572 | .get_root_latest_delta_url() 573 | .await 574 | .expect("Failed to get latest track change delta url"); 575 | 576 | // #6 577 | // Create under folder1 578 | let folder3_id = onedrive 579 | .create_folder(&folder1_id, gen_filename()) 580 | .await 581 | .expect("Failed to create folder3") 582 | .id 583 | .expect("Missing `id`"); 584 | 585 | // `*`: Update path, from tracing root to every changed file 586 | // root* 587 | // |- container* 588 | // |- folder1* 589 | // | |- folder3* 590 | // |- folder2 591 | 592 | { 593 | // #7 594 | let (delta_changes, _) = onedrive 595 | .track_root_changes_from_delta_url(&delta_url) 596 | .await 597 | .expect("Failed to track changes with delta url") 598 | .fetch_all(&onedrive) 599 | .await 600 | .expect("Failed to fetch all changes with delta url"); 601 | 602 | let ids = delta_changes 603 | .into_iter() 604 | .map(|item| item.id.expect("Missing `id`")) 605 | .collect::>(); 606 | // We track changes of root directory, so there may be other changes. 607 | assert!(ids.contains(&container_id)); 608 | assert!(ids.contains(&folder1_id)); 609 | assert!(ids.contains(&folder3_id)); 610 | assert!(!ids.contains(&folder2_id)); // This is not updated. 611 | } 612 | 613 | // #8 614 | onedrive.delete(container_loc).await.unwrap(); 615 | } 616 | 617 | #[tokio::test] 618 | async fn test_auth_error() { 619 | let auth = Auth::new( 620 | "11111111-2222-3333-4444-555555555555", 621 | Permission::new_read().offline_access(true), 622 | "https://login.microsoftonline.com/common/oauth2/nativeclient", 623 | Tenant::Consumers, 624 | ); 625 | 626 | { 627 | let err = auth 628 | .login_with_code( 629 | "M11111111-2222-3333-4444-555555555555", 630 | &ClientCredential::None, 631 | ) 632 | .await 633 | .unwrap_err(); 634 | // Don't know why, but it just replies HTTP `400 Bad Request`. 635 | assert_eq!(err.status_code(), Some(StatusCode::BAD_REQUEST)); 636 | let err_resp = err.oauth2_error_response().unwrap(); 637 | assert_eq!(err_resp.error, "unauthorized_client"); // Invalid `client_id`. 638 | } 639 | 640 | { 641 | let err = auth 642 | .login_with_refresh_token("42", &ClientCredential::None) 643 | .await 644 | .unwrap_err(); 645 | // Don't know why, but it just replies HTTP `400 Bad Request`. 646 | assert_eq!(err.status_code(), Some(StatusCode::BAD_REQUEST)); 647 | let err_resp = err.oauth2_error_response().unwrap(); 648 | assert_eq!(err_resp.error, "invalid_grant"); 649 | } 650 | } 651 | 652 | #[tokio::test] 653 | async fn test_get_drive_error_unauthorized() { 654 | let onedrive = OneDrive::new("42".to_owned(), DriveLocation::me()); 655 | let err = onedrive.get_drive().await.unwrap_err(); 656 | assert_eq!(err.status_code(), Some(StatusCode::UNAUTHORIZED)); 657 | assert_eq!( 658 | err.error_response().unwrap().code, 659 | "InvalidAuthenticationToken", 660 | ); 661 | } 662 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.98" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 25 | 26 | [[package]] 27 | name = "async-compression" 28 | version = "0.4.22" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64" 31 | dependencies = [ 32 | "flate2", 33 | "futures-core", 34 | "memchr", 35 | "pin-project-lite", 36 | "tokio", 37 | ] 38 | 39 | [[package]] 40 | name = "atomic-waker" 41 | version = "1.1.2" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 44 | 45 | [[package]] 46 | name = "autocfg" 47 | version = "1.4.0" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 50 | 51 | [[package]] 52 | name = "backtrace" 53 | version = "0.3.74" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 56 | dependencies = [ 57 | "addr2line", 58 | "cfg-if", 59 | "libc", 60 | "miniz_oxide", 61 | "object", 62 | "rustc-demangle", 63 | "windows-targets 0.52.6", 64 | ] 65 | 66 | [[package]] 67 | name = "base64" 68 | version = "0.22.1" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 71 | 72 | [[package]] 73 | name = "bitflags" 74 | version = "2.9.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 77 | 78 | [[package]] 79 | name = "bumpalo" 80 | version = "3.17.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 83 | 84 | [[package]] 85 | name = "bytes" 86 | version = "1.10.1" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 89 | 90 | [[package]] 91 | name = "cc" 92 | version = "1.2.19" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" 95 | dependencies = [ 96 | "shlex", 97 | ] 98 | 99 | [[package]] 100 | name = "cfg-if" 101 | version = "1.0.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 104 | 105 | [[package]] 106 | name = "core-foundation" 107 | version = "0.9.4" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 110 | dependencies = [ 111 | "core-foundation-sys", 112 | "libc", 113 | ] 114 | 115 | [[package]] 116 | name = "core-foundation-sys" 117 | version = "0.8.7" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 120 | 121 | [[package]] 122 | name = "crc32fast" 123 | version = "1.4.2" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 126 | dependencies = [ 127 | "cfg-if", 128 | ] 129 | 130 | [[package]] 131 | name = "displaydoc" 132 | version = "0.2.5" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 135 | dependencies = [ 136 | "proc-macro2", 137 | "quote", 138 | "syn", 139 | ] 140 | 141 | [[package]] 142 | name = "encoding_rs" 143 | version = "0.8.35" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 146 | dependencies = [ 147 | "cfg-if", 148 | ] 149 | 150 | [[package]] 151 | name = "envy" 152 | version = "0.4.2" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" 155 | dependencies = [ 156 | "serde", 157 | ] 158 | 159 | [[package]] 160 | name = "equivalent" 161 | version = "1.0.2" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 164 | 165 | [[package]] 166 | name = "errno" 167 | version = "0.3.11" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 170 | dependencies = [ 171 | "libc", 172 | "windows-sys 0.59.0", 173 | ] 174 | 175 | [[package]] 176 | name = "fastrand" 177 | version = "2.3.0" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 180 | 181 | [[package]] 182 | name = "flate2" 183 | version = "1.1.1" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 186 | dependencies = [ 187 | "crc32fast", 188 | "miniz_oxide", 189 | ] 190 | 191 | [[package]] 192 | name = "fnv" 193 | version = "1.0.7" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 196 | 197 | [[package]] 198 | name = "foreign-types" 199 | version = "0.3.2" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 202 | dependencies = [ 203 | "foreign-types-shared", 204 | ] 205 | 206 | [[package]] 207 | name = "foreign-types-shared" 208 | version = "0.1.1" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 211 | 212 | [[package]] 213 | name = "form_urlencoded" 214 | version = "1.2.1" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 217 | dependencies = [ 218 | "percent-encoding", 219 | ] 220 | 221 | [[package]] 222 | name = "futures-channel" 223 | version = "0.3.31" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 226 | dependencies = [ 227 | "futures-core", 228 | "futures-sink", 229 | ] 230 | 231 | [[package]] 232 | name = "futures-core" 233 | version = "0.3.31" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 236 | 237 | [[package]] 238 | name = "futures-io" 239 | version = "0.3.31" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 242 | 243 | [[package]] 244 | name = "futures-sink" 245 | version = "0.3.31" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 248 | 249 | [[package]] 250 | name = "futures-task" 251 | version = "0.3.31" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 254 | 255 | [[package]] 256 | name = "futures-util" 257 | version = "0.3.31" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 260 | dependencies = [ 261 | "futures-core", 262 | "futures-io", 263 | "futures-sink", 264 | "futures-task", 265 | "memchr", 266 | "pin-project-lite", 267 | "pin-utils", 268 | "slab", 269 | ] 270 | 271 | [[package]] 272 | name = "getrandom" 273 | version = "0.2.15" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 276 | dependencies = [ 277 | "cfg-if", 278 | "libc", 279 | "wasi 0.11.0+wasi-snapshot-preview1", 280 | ] 281 | 282 | [[package]] 283 | name = "getrandom" 284 | version = "0.3.2" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 287 | dependencies = [ 288 | "cfg-if", 289 | "libc", 290 | "r-efi", 291 | "wasi 0.14.2+wasi-0.2.4", 292 | ] 293 | 294 | [[package]] 295 | name = "gimli" 296 | version = "0.31.1" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 299 | 300 | [[package]] 301 | name = "h2" 302 | version = "0.4.9" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" 305 | dependencies = [ 306 | "atomic-waker", 307 | "bytes", 308 | "fnv", 309 | "futures-core", 310 | "futures-sink", 311 | "http", 312 | "indexmap", 313 | "slab", 314 | "tokio", 315 | "tokio-util", 316 | "tracing", 317 | ] 318 | 319 | [[package]] 320 | name = "hashbrown" 321 | version = "0.15.2" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 324 | 325 | [[package]] 326 | name = "heck" 327 | version = "0.5.0" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 330 | 331 | [[package]] 332 | name = "http" 333 | version = "1.3.1" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 336 | dependencies = [ 337 | "bytes", 338 | "fnv", 339 | "itoa", 340 | ] 341 | 342 | [[package]] 343 | name = "http-body" 344 | version = "1.0.1" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 347 | dependencies = [ 348 | "bytes", 349 | "http", 350 | ] 351 | 352 | [[package]] 353 | name = "http-body-util" 354 | version = "0.1.3" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 357 | dependencies = [ 358 | "bytes", 359 | "futures-core", 360 | "http", 361 | "http-body", 362 | "pin-project-lite", 363 | ] 364 | 365 | [[package]] 366 | name = "httparse" 367 | version = "1.10.1" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 370 | 371 | [[package]] 372 | name = "hyper" 373 | version = "1.6.0" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 376 | dependencies = [ 377 | "bytes", 378 | "futures-channel", 379 | "futures-util", 380 | "h2", 381 | "http", 382 | "http-body", 383 | "httparse", 384 | "itoa", 385 | "pin-project-lite", 386 | "smallvec", 387 | "tokio", 388 | "want", 389 | ] 390 | 391 | [[package]] 392 | name = "hyper-rustls" 393 | version = "0.27.5" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" 396 | dependencies = [ 397 | "futures-util", 398 | "http", 399 | "hyper", 400 | "hyper-util", 401 | "rustls", 402 | "rustls-pki-types", 403 | "tokio", 404 | "tokio-rustls", 405 | "tower-service", 406 | ] 407 | 408 | [[package]] 409 | name = "hyper-tls" 410 | version = "0.6.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 413 | dependencies = [ 414 | "bytes", 415 | "http-body-util", 416 | "hyper", 417 | "hyper-util", 418 | "native-tls", 419 | "tokio", 420 | "tokio-native-tls", 421 | "tower-service", 422 | ] 423 | 424 | [[package]] 425 | name = "hyper-util" 426 | version = "0.1.11" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" 429 | dependencies = [ 430 | "bytes", 431 | "futures-channel", 432 | "futures-util", 433 | "http", 434 | "http-body", 435 | "hyper", 436 | "libc", 437 | "pin-project-lite", 438 | "socket2", 439 | "tokio", 440 | "tower-service", 441 | "tracing", 442 | ] 443 | 444 | [[package]] 445 | name = "icu_collections" 446 | version = "1.5.0" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 449 | dependencies = [ 450 | "displaydoc", 451 | "yoke", 452 | "zerofrom", 453 | "zerovec", 454 | ] 455 | 456 | [[package]] 457 | name = "icu_locid" 458 | version = "1.5.0" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 461 | dependencies = [ 462 | "displaydoc", 463 | "litemap", 464 | "tinystr", 465 | "writeable", 466 | "zerovec", 467 | ] 468 | 469 | [[package]] 470 | name = "icu_locid_transform" 471 | version = "1.5.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 474 | dependencies = [ 475 | "displaydoc", 476 | "icu_locid", 477 | "icu_locid_transform_data", 478 | "icu_provider", 479 | "tinystr", 480 | "zerovec", 481 | ] 482 | 483 | [[package]] 484 | name = "icu_locid_transform_data" 485 | version = "1.5.1" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" 488 | 489 | [[package]] 490 | name = "icu_normalizer" 491 | version = "1.5.0" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 494 | dependencies = [ 495 | "displaydoc", 496 | "icu_collections", 497 | "icu_normalizer_data", 498 | "icu_properties", 499 | "icu_provider", 500 | "smallvec", 501 | "utf16_iter", 502 | "utf8_iter", 503 | "write16", 504 | "zerovec", 505 | ] 506 | 507 | [[package]] 508 | name = "icu_normalizer_data" 509 | version = "1.5.1" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" 512 | 513 | [[package]] 514 | name = "icu_properties" 515 | version = "1.5.1" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 518 | dependencies = [ 519 | "displaydoc", 520 | "icu_collections", 521 | "icu_locid_transform", 522 | "icu_properties_data", 523 | "icu_provider", 524 | "tinystr", 525 | "zerovec", 526 | ] 527 | 528 | [[package]] 529 | name = "icu_properties_data" 530 | version = "1.5.1" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" 533 | 534 | [[package]] 535 | name = "icu_provider" 536 | version = "1.5.0" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 539 | dependencies = [ 540 | "displaydoc", 541 | "icu_locid", 542 | "icu_provider_macros", 543 | "stable_deref_trait", 544 | "tinystr", 545 | "writeable", 546 | "yoke", 547 | "zerofrom", 548 | "zerovec", 549 | ] 550 | 551 | [[package]] 552 | name = "icu_provider_macros" 553 | version = "1.5.0" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 556 | dependencies = [ 557 | "proc-macro2", 558 | "quote", 559 | "syn", 560 | ] 561 | 562 | [[package]] 563 | name = "idna" 564 | version = "1.0.3" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 567 | dependencies = [ 568 | "idna_adapter", 569 | "smallvec", 570 | "utf8_iter", 571 | ] 572 | 573 | [[package]] 574 | name = "idna_adapter" 575 | version = "1.2.0" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 578 | dependencies = [ 579 | "icu_normalizer", 580 | "icu_properties", 581 | ] 582 | 583 | [[package]] 584 | name = "indexmap" 585 | version = "2.9.0" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 588 | dependencies = [ 589 | "equivalent", 590 | "hashbrown", 591 | ] 592 | 593 | [[package]] 594 | name = "ipnet" 595 | version = "2.11.0" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 598 | 599 | [[package]] 600 | name = "is-docker" 601 | version = "0.2.0" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" 604 | dependencies = [ 605 | "once_cell", 606 | ] 607 | 608 | [[package]] 609 | name = "is-wsl" 610 | version = "0.4.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" 613 | dependencies = [ 614 | "is-docker", 615 | "once_cell", 616 | ] 617 | 618 | [[package]] 619 | name = "itoa" 620 | version = "1.0.15" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 623 | 624 | [[package]] 625 | name = "js-sys" 626 | version = "0.3.77" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 629 | dependencies = [ 630 | "once_cell", 631 | "wasm-bindgen", 632 | ] 633 | 634 | [[package]] 635 | name = "libc" 636 | version = "0.2.172" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 639 | 640 | [[package]] 641 | name = "linux-raw-sys" 642 | version = "0.9.4" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 645 | 646 | [[package]] 647 | name = "litemap" 648 | version = "0.7.4" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 651 | 652 | [[package]] 653 | name = "log" 654 | version = "0.4.27" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 657 | 658 | [[package]] 659 | name = "memchr" 660 | version = "2.7.4" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 663 | 664 | [[package]] 665 | name = "mime" 666 | version = "0.3.17" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 669 | 670 | [[package]] 671 | name = "miniz_oxide" 672 | version = "0.8.8" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 675 | dependencies = [ 676 | "adler2", 677 | ] 678 | 679 | [[package]] 680 | name = "mio" 681 | version = "1.0.3" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 684 | dependencies = [ 685 | "libc", 686 | "wasi 0.11.0+wasi-snapshot-preview1", 687 | "windows-sys 0.52.0", 688 | ] 689 | 690 | [[package]] 691 | name = "native-tls" 692 | version = "0.2.13" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" 695 | dependencies = [ 696 | "libc", 697 | "log", 698 | "openssl", 699 | "openssl-probe", 700 | "openssl-sys", 701 | "schannel", 702 | "security-framework", 703 | "security-framework-sys", 704 | "tempfile", 705 | ] 706 | 707 | [[package]] 708 | name = "object" 709 | version = "0.36.7" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 712 | dependencies = [ 713 | "memchr", 714 | ] 715 | 716 | [[package]] 717 | name = "once_cell" 718 | version = "1.21.3" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 721 | 722 | [[package]] 723 | name = "onedrive-api" 724 | version = "0.10.2" 725 | dependencies = [ 726 | "bytes", 727 | "reqwest", 728 | "serde", 729 | "serde_json", 730 | "strum", 731 | "thiserror", 732 | "url", 733 | ] 734 | 735 | [[package]] 736 | name = "onedrive-api-test" 737 | version = "0.0.0" 738 | dependencies = [ 739 | "anyhow", 740 | "envy", 741 | "onedrive-api", 742 | "open", 743 | "pico-args", 744 | "rand", 745 | "reqwest", 746 | "serde", 747 | "serde_json", 748 | "tokio", 749 | ] 750 | 751 | [[package]] 752 | name = "open" 753 | version = "5.3.2" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" 756 | dependencies = [ 757 | "is-wsl", 758 | "libc", 759 | "pathdiff", 760 | ] 761 | 762 | [[package]] 763 | name = "openssl" 764 | version = "0.10.72" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" 767 | dependencies = [ 768 | "bitflags", 769 | "cfg-if", 770 | "foreign-types", 771 | "libc", 772 | "once_cell", 773 | "openssl-macros", 774 | "openssl-sys", 775 | ] 776 | 777 | [[package]] 778 | name = "openssl-macros" 779 | version = "0.1.1" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 782 | dependencies = [ 783 | "proc-macro2", 784 | "quote", 785 | "syn", 786 | ] 787 | 788 | [[package]] 789 | name = "openssl-probe" 790 | version = "0.1.6" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 793 | 794 | [[package]] 795 | name = "openssl-sys" 796 | version = "0.9.107" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" 799 | dependencies = [ 800 | "cc", 801 | "libc", 802 | "pkg-config", 803 | "vcpkg", 804 | ] 805 | 806 | [[package]] 807 | name = "pathdiff" 808 | version = "0.2.3" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" 811 | 812 | [[package]] 813 | name = "percent-encoding" 814 | version = "2.3.1" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 817 | 818 | [[package]] 819 | name = "pico-args" 820 | version = "0.5.0" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" 823 | 824 | [[package]] 825 | name = "pin-project-lite" 826 | version = "0.2.16" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 829 | 830 | [[package]] 831 | name = "pin-utils" 832 | version = "0.1.0" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 835 | 836 | [[package]] 837 | name = "pkg-config" 838 | version = "0.3.32" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 841 | 842 | [[package]] 843 | name = "ppv-lite86" 844 | version = "0.2.21" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 847 | dependencies = [ 848 | "zerocopy", 849 | ] 850 | 851 | [[package]] 852 | name = "proc-macro2" 853 | version = "1.0.95" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 856 | dependencies = [ 857 | "unicode-ident", 858 | ] 859 | 860 | [[package]] 861 | name = "quote" 862 | version = "1.0.40" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 865 | dependencies = [ 866 | "proc-macro2", 867 | ] 868 | 869 | [[package]] 870 | name = "r-efi" 871 | version = "5.2.0" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 874 | 875 | [[package]] 876 | name = "rand" 877 | version = "0.9.1" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 880 | dependencies = [ 881 | "rand_chacha", 882 | "rand_core", 883 | ] 884 | 885 | [[package]] 886 | name = "rand_chacha" 887 | version = "0.9.0" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 890 | dependencies = [ 891 | "ppv-lite86", 892 | "rand_core", 893 | ] 894 | 895 | [[package]] 896 | name = "rand_core" 897 | version = "0.9.3" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 900 | dependencies = [ 901 | "getrandom 0.3.2", 902 | ] 903 | 904 | [[package]] 905 | name = "reqwest" 906 | version = "0.12.15" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" 909 | dependencies = [ 910 | "async-compression", 911 | "base64", 912 | "bytes", 913 | "encoding_rs", 914 | "futures-channel", 915 | "futures-core", 916 | "futures-util", 917 | "h2", 918 | "http", 919 | "http-body", 920 | "http-body-util", 921 | "hyper", 922 | "hyper-rustls", 923 | "hyper-tls", 924 | "hyper-util", 925 | "ipnet", 926 | "js-sys", 927 | "log", 928 | "mime", 929 | "native-tls", 930 | "once_cell", 931 | "percent-encoding", 932 | "pin-project-lite", 933 | "rustls-pemfile", 934 | "serde", 935 | "serde_json", 936 | "serde_urlencoded", 937 | "sync_wrapper", 938 | "system-configuration", 939 | "tokio", 940 | "tokio-native-tls", 941 | "tokio-util", 942 | "tower", 943 | "tower-service", 944 | "url", 945 | "wasm-bindgen", 946 | "wasm-bindgen-futures", 947 | "web-sys", 948 | "windows-registry", 949 | ] 950 | 951 | [[package]] 952 | name = "ring" 953 | version = "0.17.14" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 956 | dependencies = [ 957 | "cc", 958 | "cfg-if", 959 | "getrandom 0.2.15", 960 | "libc", 961 | "untrusted", 962 | "windows-sys 0.52.0", 963 | ] 964 | 965 | [[package]] 966 | name = "rustc-demangle" 967 | version = "0.1.24" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 970 | 971 | [[package]] 972 | name = "rustix" 973 | version = "1.0.5" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 976 | dependencies = [ 977 | "bitflags", 978 | "errno", 979 | "libc", 980 | "linux-raw-sys", 981 | "windows-sys 0.59.0", 982 | ] 983 | 984 | [[package]] 985 | name = "rustls" 986 | version = "0.23.26" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" 989 | dependencies = [ 990 | "once_cell", 991 | "rustls-pki-types", 992 | "rustls-webpki", 993 | "subtle", 994 | "zeroize", 995 | ] 996 | 997 | [[package]] 998 | name = "rustls-pemfile" 999 | version = "2.2.0" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 1002 | dependencies = [ 1003 | "rustls-pki-types", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "rustls-pki-types" 1008 | version = "1.11.0" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" 1011 | 1012 | [[package]] 1013 | name = "rustls-webpki" 1014 | version = "0.103.1" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" 1017 | dependencies = [ 1018 | "ring", 1019 | "rustls-pki-types", 1020 | "untrusted", 1021 | ] 1022 | 1023 | [[package]] 1024 | name = "rustversion" 1025 | version = "1.0.20" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1028 | 1029 | [[package]] 1030 | name = "ryu" 1031 | version = "1.0.20" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1034 | 1035 | [[package]] 1036 | name = "schannel" 1037 | version = "0.1.27" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1040 | dependencies = [ 1041 | "windows-sys 0.59.0", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "security-framework" 1046 | version = "2.11.1" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1049 | dependencies = [ 1050 | "bitflags", 1051 | "core-foundation", 1052 | "core-foundation-sys", 1053 | "libc", 1054 | "security-framework-sys", 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "security-framework-sys" 1059 | version = "2.14.0" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 1062 | dependencies = [ 1063 | "core-foundation-sys", 1064 | "libc", 1065 | ] 1066 | 1067 | [[package]] 1068 | name = "serde" 1069 | version = "1.0.219" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1072 | dependencies = [ 1073 | "serde_derive", 1074 | ] 1075 | 1076 | [[package]] 1077 | name = "serde_derive" 1078 | version = "1.0.219" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1081 | dependencies = [ 1082 | "proc-macro2", 1083 | "quote", 1084 | "syn", 1085 | ] 1086 | 1087 | [[package]] 1088 | name = "serde_json" 1089 | version = "1.0.140" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1092 | dependencies = [ 1093 | "itoa", 1094 | "memchr", 1095 | "ryu", 1096 | "serde", 1097 | ] 1098 | 1099 | [[package]] 1100 | name = "serde_urlencoded" 1101 | version = "0.7.1" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1104 | dependencies = [ 1105 | "form_urlencoded", 1106 | "itoa", 1107 | "ryu", 1108 | "serde", 1109 | ] 1110 | 1111 | [[package]] 1112 | name = "shlex" 1113 | version = "1.3.0" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1116 | 1117 | [[package]] 1118 | name = "slab" 1119 | version = "0.4.9" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1122 | dependencies = [ 1123 | "autocfg", 1124 | ] 1125 | 1126 | [[package]] 1127 | name = "smallvec" 1128 | version = "1.15.0" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1131 | 1132 | [[package]] 1133 | name = "socket2" 1134 | version = "0.5.9" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 1137 | dependencies = [ 1138 | "libc", 1139 | "windows-sys 0.52.0", 1140 | ] 1141 | 1142 | [[package]] 1143 | name = "stable_deref_trait" 1144 | version = "1.2.0" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1147 | 1148 | [[package]] 1149 | name = "strum" 1150 | version = "0.27.1" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" 1153 | dependencies = [ 1154 | "strum_macros", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "strum_macros" 1159 | version = "0.27.1" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" 1162 | dependencies = [ 1163 | "heck", 1164 | "proc-macro2", 1165 | "quote", 1166 | "rustversion", 1167 | "syn", 1168 | ] 1169 | 1170 | [[package]] 1171 | name = "subtle" 1172 | version = "2.6.1" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1175 | 1176 | [[package]] 1177 | name = "syn" 1178 | version = "2.0.100" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 1181 | dependencies = [ 1182 | "proc-macro2", 1183 | "quote", 1184 | "unicode-ident", 1185 | ] 1186 | 1187 | [[package]] 1188 | name = "sync_wrapper" 1189 | version = "1.0.2" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1192 | dependencies = [ 1193 | "futures-core", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "synstructure" 1198 | version = "0.13.1" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1201 | dependencies = [ 1202 | "proc-macro2", 1203 | "quote", 1204 | "syn", 1205 | ] 1206 | 1207 | [[package]] 1208 | name = "system-configuration" 1209 | version = "0.6.1" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 1212 | dependencies = [ 1213 | "bitflags", 1214 | "core-foundation", 1215 | "system-configuration-sys", 1216 | ] 1217 | 1218 | [[package]] 1219 | name = "system-configuration-sys" 1220 | version = "0.6.0" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1223 | dependencies = [ 1224 | "core-foundation-sys", 1225 | "libc", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "tempfile" 1230 | version = "3.19.1" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 1233 | dependencies = [ 1234 | "fastrand", 1235 | "getrandom 0.3.2", 1236 | "once_cell", 1237 | "rustix", 1238 | "windows-sys 0.59.0", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "thiserror" 1243 | version = "2.0.12" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1246 | dependencies = [ 1247 | "thiserror-impl", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "thiserror-impl" 1252 | version = "2.0.12" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1255 | dependencies = [ 1256 | "proc-macro2", 1257 | "quote", 1258 | "syn", 1259 | ] 1260 | 1261 | [[package]] 1262 | name = "tinystr" 1263 | version = "0.7.6" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1266 | dependencies = [ 1267 | "displaydoc", 1268 | "zerovec", 1269 | ] 1270 | 1271 | [[package]] 1272 | name = "tokio" 1273 | version = "1.44.2" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" 1276 | dependencies = [ 1277 | "backtrace", 1278 | "bytes", 1279 | "libc", 1280 | "mio", 1281 | "pin-project-lite", 1282 | "socket2", 1283 | "tokio-macros", 1284 | "windows-sys 0.52.0", 1285 | ] 1286 | 1287 | [[package]] 1288 | name = "tokio-macros" 1289 | version = "2.5.0" 1290 | source = "registry+https://github.com/rust-lang/crates.io-index" 1291 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1292 | dependencies = [ 1293 | "proc-macro2", 1294 | "quote", 1295 | "syn", 1296 | ] 1297 | 1298 | [[package]] 1299 | name = "tokio-native-tls" 1300 | version = "0.3.1" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1303 | dependencies = [ 1304 | "native-tls", 1305 | "tokio", 1306 | ] 1307 | 1308 | [[package]] 1309 | name = "tokio-rustls" 1310 | version = "0.26.2" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 1313 | dependencies = [ 1314 | "rustls", 1315 | "tokio", 1316 | ] 1317 | 1318 | [[package]] 1319 | name = "tokio-util" 1320 | version = "0.7.14" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" 1323 | dependencies = [ 1324 | "bytes", 1325 | "futures-core", 1326 | "futures-sink", 1327 | "pin-project-lite", 1328 | "tokio", 1329 | ] 1330 | 1331 | [[package]] 1332 | name = "tower" 1333 | version = "0.5.2" 1334 | source = "registry+https://github.com/rust-lang/crates.io-index" 1335 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1336 | dependencies = [ 1337 | "futures-core", 1338 | "futures-util", 1339 | "pin-project-lite", 1340 | "sync_wrapper", 1341 | "tokio", 1342 | "tower-layer", 1343 | "tower-service", 1344 | ] 1345 | 1346 | [[package]] 1347 | name = "tower-layer" 1348 | version = "0.3.3" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1351 | 1352 | [[package]] 1353 | name = "tower-service" 1354 | version = "0.3.3" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1357 | 1358 | [[package]] 1359 | name = "tracing" 1360 | version = "0.1.41" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1363 | dependencies = [ 1364 | "pin-project-lite", 1365 | "tracing-core", 1366 | ] 1367 | 1368 | [[package]] 1369 | name = "tracing-core" 1370 | version = "0.1.33" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1373 | dependencies = [ 1374 | "once_cell", 1375 | ] 1376 | 1377 | [[package]] 1378 | name = "try-lock" 1379 | version = "0.2.5" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1382 | 1383 | [[package]] 1384 | name = "unicode-ident" 1385 | version = "1.0.18" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1388 | 1389 | [[package]] 1390 | name = "untrusted" 1391 | version = "0.9.0" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1394 | 1395 | [[package]] 1396 | name = "url" 1397 | version = "2.5.4" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1400 | dependencies = [ 1401 | "form_urlencoded", 1402 | "idna", 1403 | "percent-encoding", 1404 | ] 1405 | 1406 | [[package]] 1407 | name = "utf16_iter" 1408 | version = "1.0.5" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1411 | 1412 | [[package]] 1413 | name = "utf8_iter" 1414 | version = "1.0.4" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1417 | 1418 | [[package]] 1419 | name = "vcpkg" 1420 | version = "0.2.15" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1423 | 1424 | [[package]] 1425 | name = "want" 1426 | version = "0.3.1" 1427 | source = "registry+https://github.com/rust-lang/crates.io-index" 1428 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1429 | dependencies = [ 1430 | "try-lock", 1431 | ] 1432 | 1433 | [[package]] 1434 | name = "wasi" 1435 | version = "0.11.0+wasi-snapshot-preview1" 1436 | source = "registry+https://github.com/rust-lang/crates.io-index" 1437 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1438 | 1439 | [[package]] 1440 | name = "wasi" 1441 | version = "0.14.2+wasi-0.2.4" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1444 | dependencies = [ 1445 | "wit-bindgen-rt", 1446 | ] 1447 | 1448 | [[package]] 1449 | name = "wasm-bindgen" 1450 | version = "0.2.100" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1453 | dependencies = [ 1454 | "cfg-if", 1455 | "once_cell", 1456 | "rustversion", 1457 | "wasm-bindgen-macro", 1458 | ] 1459 | 1460 | [[package]] 1461 | name = "wasm-bindgen-backend" 1462 | version = "0.2.100" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1465 | dependencies = [ 1466 | "bumpalo", 1467 | "log", 1468 | "proc-macro2", 1469 | "quote", 1470 | "syn", 1471 | "wasm-bindgen-shared", 1472 | ] 1473 | 1474 | [[package]] 1475 | name = "wasm-bindgen-futures" 1476 | version = "0.4.50" 1477 | source = "registry+https://github.com/rust-lang/crates.io-index" 1478 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1479 | dependencies = [ 1480 | "cfg-if", 1481 | "js-sys", 1482 | "once_cell", 1483 | "wasm-bindgen", 1484 | "web-sys", 1485 | ] 1486 | 1487 | [[package]] 1488 | name = "wasm-bindgen-macro" 1489 | version = "0.2.100" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1492 | dependencies = [ 1493 | "quote", 1494 | "wasm-bindgen-macro-support", 1495 | ] 1496 | 1497 | [[package]] 1498 | name = "wasm-bindgen-macro-support" 1499 | version = "0.2.100" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1502 | dependencies = [ 1503 | "proc-macro2", 1504 | "quote", 1505 | "syn", 1506 | "wasm-bindgen-backend", 1507 | "wasm-bindgen-shared", 1508 | ] 1509 | 1510 | [[package]] 1511 | name = "wasm-bindgen-shared" 1512 | version = "0.2.100" 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" 1514 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1515 | dependencies = [ 1516 | "unicode-ident", 1517 | ] 1518 | 1519 | [[package]] 1520 | name = "web-sys" 1521 | version = "0.3.77" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1524 | dependencies = [ 1525 | "js-sys", 1526 | "wasm-bindgen", 1527 | ] 1528 | 1529 | [[package]] 1530 | name = "windows-link" 1531 | version = "0.1.1" 1532 | source = "registry+https://github.com/rust-lang/crates.io-index" 1533 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 1534 | 1535 | [[package]] 1536 | name = "windows-registry" 1537 | version = "0.4.0" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" 1540 | dependencies = [ 1541 | "windows-result", 1542 | "windows-strings", 1543 | "windows-targets 0.53.0", 1544 | ] 1545 | 1546 | [[package]] 1547 | name = "windows-result" 1548 | version = "0.3.2" 1549 | source = "registry+https://github.com/rust-lang/crates.io-index" 1550 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 1551 | dependencies = [ 1552 | "windows-link", 1553 | ] 1554 | 1555 | [[package]] 1556 | name = "windows-strings" 1557 | version = "0.3.1" 1558 | source = "registry+https://github.com/rust-lang/crates.io-index" 1559 | checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" 1560 | dependencies = [ 1561 | "windows-link", 1562 | ] 1563 | 1564 | [[package]] 1565 | name = "windows-sys" 1566 | version = "0.52.0" 1567 | source = "registry+https://github.com/rust-lang/crates.io-index" 1568 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1569 | dependencies = [ 1570 | "windows-targets 0.52.6", 1571 | ] 1572 | 1573 | [[package]] 1574 | name = "windows-sys" 1575 | version = "0.59.0" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1578 | dependencies = [ 1579 | "windows-targets 0.52.6", 1580 | ] 1581 | 1582 | [[package]] 1583 | name = "windows-targets" 1584 | version = "0.52.6" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1587 | dependencies = [ 1588 | "windows_aarch64_gnullvm 0.52.6", 1589 | "windows_aarch64_msvc 0.52.6", 1590 | "windows_i686_gnu 0.52.6", 1591 | "windows_i686_gnullvm 0.52.6", 1592 | "windows_i686_msvc 0.52.6", 1593 | "windows_x86_64_gnu 0.52.6", 1594 | "windows_x86_64_gnullvm 0.52.6", 1595 | "windows_x86_64_msvc 0.52.6", 1596 | ] 1597 | 1598 | [[package]] 1599 | name = "windows-targets" 1600 | version = "0.53.0" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 1603 | dependencies = [ 1604 | "windows_aarch64_gnullvm 0.53.0", 1605 | "windows_aarch64_msvc 0.53.0", 1606 | "windows_i686_gnu 0.53.0", 1607 | "windows_i686_gnullvm 0.53.0", 1608 | "windows_i686_msvc 0.53.0", 1609 | "windows_x86_64_gnu 0.53.0", 1610 | "windows_x86_64_gnullvm 0.53.0", 1611 | "windows_x86_64_msvc 0.53.0", 1612 | ] 1613 | 1614 | [[package]] 1615 | name = "windows_aarch64_gnullvm" 1616 | version = "0.52.6" 1617 | source = "registry+https://github.com/rust-lang/crates.io-index" 1618 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1619 | 1620 | [[package]] 1621 | name = "windows_aarch64_gnullvm" 1622 | version = "0.53.0" 1623 | source = "registry+https://github.com/rust-lang/crates.io-index" 1624 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 1625 | 1626 | [[package]] 1627 | name = "windows_aarch64_msvc" 1628 | version = "0.52.6" 1629 | source = "registry+https://github.com/rust-lang/crates.io-index" 1630 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1631 | 1632 | [[package]] 1633 | name = "windows_aarch64_msvc" 1634 | version = "0.53.0" 1635 | source = "registry+https://github.com/rust-lang/crates.io-index" 1636 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 1637 | 1638 | [[package]] 1639 | name = "windows_i686_gnu" 1640 | version = "0.52.6" 1641 | source = "registry+https://github.com/rust-lang/crates.io-index" 1642 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1643 | 1644 | [[package]] 1645 | name = "windows_i686_gnu" 1646 | version = "0.53.0" 1647 | source = "registry+https://github.com/rust-lang/crates.io-index" 1648 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 1649 | 1650 | [[package]] 1651 | name = "windows_i686_gnullvm" 1652 | version = "0.52.6" 1653 | source = "registry+https://github.com/rust-lang/crates.io-index" 1654 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1655 | 1656 | [[package]] 1657 | name = "windows_i686_gnullvm" 1658 | version = "0.53.0" 1659 | source = "registry+https://github.com/rust-lang/crates.io-index" 1660 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 1661 | 1662 | [[package]] 1663 | name = "windows_i686_msvc" 1664 | version = "0.52.6" 1665 | source = "registry+https://github.com/rust-lang/crates.io-index" 1666 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1667 | 1668 | [[package]] 1669 | name = "windows_i686_msvc" 1670 | version = "0.53.0" 1671 | source = "registry+https://github.com/rust-lang/crates.io-index" 1672 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 1673 | 1674 | [[package]] 1675 | name = "windows_x86_64_gnu" 1676 | version = "0.52.6" 1677 | source = "registry+https://github.com/rust-lang/crates.io-index" 1678 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1679 | 1680 | [[package]] 1681 | name = "windows_x86_64_gnu" 1682 | version = "0.53.0" 1683 | source = "registry+https://github.com/rust-lang/crates.io-index" 1684 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 1685 | 1686 | [[package]] 1687 | name = "windows_x86_64_gnullvm" 1688 | version = "0.52.6" 1689 | source = "registry+https://github.com/rust-lang/crates.io-index" 1690 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1691 | 1692 | [[package]] 1693 | name = "windows_x86_64_gnullvm" 1694 | version = "0.53.0" 1695 | source = "registry+https://github.com/rust-lang/crates.io-index" 1696 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 1697 | 1698 | [[package]] 1699 | name = "windows_x86_64_msvc" 1700 | version = "0.52.6" 1701 | source = "registry+https://github.com/rust-lang/crates.io-index" 1702 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1703 | 1704 | [[package]] 1705 | name = "windows_x86_64_msvc" 1706 | version = "0.53.0" 1707 | source = "registry+https://github.com/rust-lang/crates.io-index" 1708 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1709 | 1710 | [[package]] 1711 | name = "wit-bindgen-rt" 1712 | version = "0.39.0" 1713 | source = "registry+https://github.com/rust-lang/crates.io-index" 1714 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1715 | dependencies = [ 1716 | "bitflags", 1717 | ] 1718 | 1719 | [[package]] 1720 | name = "write16" 1721 | version = "1.0.0" 1722 | source = "registry+https://github.com/rust-lang/crates.io-index" 1723 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1724 | 1725 | [[package]] 1726 | name = "writeable" 1727 | version = "0.5.5" 1728 | source = "registry+https://github.com/rust-lang/crates.io-index" 1729 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1730 | 1731 | [[package]] 1732 | name = "yoke" 1733 | version = "0.7.5" 1734 | source = "registry+https://github.com/rust-lang/crates.io-index" 1735 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1736 | dependencies = [ 1737 | "serde", 1738 | "stable_deref_trait", 1739 | "yoke-derive", 1740 | "zerofrom", 1741 | ] 1742 | 1743 | [[package]] 1744 | name = "yoke-derive" 1745 | version = "0.7.5" 1746 | source = "registry+https://github.com/rust-lang/crates.io-index" 1747 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1748 | dependencies = [ 1749 | "proc-macro2", 1750 | "quote", 1751 | "syn", 1752 | "synstructure", 1753 | ] 1754 | 1755 | [[package]] 1756 | name = "zerocopy" 1757 | version = "0.8.24" 1758 | source = "registry+https://github.com/rust-lang/crates.io-index" 1759 | checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 1760 | dependencies = [ 1761 | "zerocopy-derive", 1762 | ] 1763 | 1764 | [[package]] 1765 | name = "zerocopy-derive" 1766 | version = "0.8.24" 1767 | source = "registry+https://github.com/rust-lang/crates.io-index" 1768 | checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 1769 | dependencies = [ 1770 | "proc-macro2", 1771 | "quote", 1772 | "syn", 1773 | ] 1774 | 1775 | [[package]] 1776 | name = "zerofrom" 1777 | version = "0.1.5" 1778 | source = "registry+https://github.com/rust-lang/crates.io-index" 1779 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 1780 | dependencies = [ 1781 | "zerofrom-derive", 1782 | ] 1783 | 1784 | [[package]] 1785 | name = "zerofrom-derive" 1786 | version = "0.1.6" 1787 | source = "registry+https://github.com/rust-lang/crates.io-index" 1788 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1789 | dependencies = [ 1790 | "proc-macro2", 1791 | "quote", 1792 | "syn", 1793 | "synstructure", 1794 | ] 1795 | 1796 | [[package]] 1797 | name = "zeroize" 1798 | version = "1.8.1" 1799 | source = "registry+https://github.com/rust-lang/crates.io-index" 1800 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1801 | 1802 | [[package]] 1803 | name = "zerovec" 1804 | version = "0.10.4" 1805 | source = "registry+https://github.com/rust-lang/crates.io-index" 1806 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1807 | dependencies = [ 1808 | "yoke", 1809 | "zerofrom", 1810 | "zerovec-derive", 1811 | ] 1812 | 1813 | [[package]] 1814 | name = "zerovec-derive" 1815 | version = "0.10.3" 1816 | source = "registry+https://github.com/rust-lang/crates.io-index" 1817 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1818 | dependencies = [ 1819 | "proc-macro2", 1820 | "quote", 1821 | "syn", 1822 | ] 1823 | -------------------------------------------------------------------------------- /src/onedrive.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::default_trait_access)] // Forwarding default options is allowed. 2 | use crate::{ 3 | error::{Error, Result}, 4 | option::{CollectionOption, DriveItemPutOption, ObjectOption}, 5 | resource::{Drive, DriveField, DriveItem, DriveItemField, TimestampString}, 6 | util::{ 7 | handle_error_response, ApiPathComponent, DriveLocation, FileName, ItemLocation, 8 | RequestBuilderExt as _, ResponseExt as _, 9 | }, 10 | {ConflictBehavior, ExpectRange}, 11 | }; 12 | use bytes::Bytes; 13 | use reqwest::{header, Client}; 14 | use serde::{Deserialize, Serialize}; 15 | use serde_json::json; 16 | use std::fmt; 17 | use url::Url; 18 | 19 | macro_rules! api_url { 20 | ($($seg:expr),* $(,)?) => {{ 21 | let mut url = Url::parse("https://graph.microsoft.com/v1.0").unwrap(); 22 | { 23 | let mut buf = url.path_segments_mut().unwrap(); 24 | $(ApiPathComponent::extend_into($seg, &mut buf);)* 25 | } // End borrowing of `url` 26 | url 27 | }}; 28 | } 29 | 30 | /// TODO: More efficient impl. 31 | macro_rules! api_path { 32 | ($item:expr) => {{ 33 | let mut url = Url::parse("path:///drive").unwrap(); 34 | let item: &ItemLocation = $item; 35 | ApiPathComponent::extend_into(item, &mut url.path_segments_mut().unwrap()); 36 | url 37 | } 38 | .path()}; 39 | } 40 | 41 | /// The authorized client to access OneDrive resources in a specified Drive. 42 | #[derive(Clone)] 43 | pub struct OneDrive { 44 | client: Client, 45 | token: String, 46 | drive: DriveLocation, 47 | } 48 | 49 | impl fmt::Debug for OneDrive { 50 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 51 | f.debug_struct("OneDrive") 52 | .field("client", &self.client) 53 | // Skip `token`. 54 | .field("drive", &self.drive) 55 | .finish_non_exhaustive() 56 | } 57 | } 58 | 59 | impl OneDrive { 60 | /// Create a new OneDrive instance with access token given to perform operations in a Drive. 61 | /// 62 | /// # Panics 63 | /// It panics if the underlying `reqwest::Client` cannot be created. 64 | pub fn new(access_token: impl Into, drive: impl Into) -> Self { 65 | let client = Client::builder() 66 | .redirect(reqwest::redirect::Policy::none()) 67 | .gzip(true) 68 | .build() 69 | .unwrap(); 70 | Self::new_with_client(client, access_token, drive.into()) 71 | } 72 | 73 | /// Same as [`OneDrive::new`] but with custom `reqwest::Client`. 74 | /// 75 | /// # Note 76 | /// The given `client` should have redirection disabled to 77 | /// make [`get_item_download_url[_with_option]`][get_url] work properly. 78 | /// See also the docs of [`get_item_download_url[_with_option]`][get_url]. 79 | /// 80 | /// [`OneDrive::new`]: #method.new 81 | /// [get_url]: #method.get_item_download_url_with_option 82 | pub fn new_with_client( 83 | client: Client, 84 | access_token: impl Into, 85 | drive: impl Into, 86 | ) -> Self { 87 | OneDrive { 88 | client, 89 | token: access_token.into(), 90 | drive: drive.into(), 91 | } 92 | } 93 | 94 | /// Get the `reqwest::Client` used to create the OneDrive instance. 95 | #[must_use] 96 | pub fn client(&self) -> &Client { 97 | &self.client 98 | } 99 | 100 | /// Get the access token used to create the OneDrive instance. 101 | #[must_use] 102 | pub fn access_token(&self) -> &str { 103 | &self.token 104 | } 105 | 106 | /// Get current `Drive`. 107 | /// 108 | /// Retrieve the properties and relationships of a [`resource::Drive`][drive] resource. 109 | /// 110 | /// # See also 111 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/drive-get?view=graph-rest-1.0) 112 | /// 113 | /// [drive]: ./resource/struct.Drive.html 114 | pub async fn get_drive_with_option(&self, option: ObjectOption) -> Result { 115 | self.client 116 | .get(api_url![&self.drive]) 117 | .apply(option) 118 | .bearer_auth(&self.token) 119 | .send() 120 | .await? 121 | .parse() 122 | .await 123 | } 124 | 125 | /// Shortcut to `get_drive_with_option` with default parameters. 126 | /// 127 | /// # See also 128 | /// [`get_drive_with_option`][with_opt] 129 | /// 130 | /// [with_opt]: #method.get_drive_with_option 131 | pub async fn get_drive(&self) -> Result { 132 | self.get_drive_with_option(Default::default()).await 133 | } 134 | 135 | /// List children of a `DriveItem`. 136 | /// 137 | /// Retrieve a collection of [`resource::DriveItem`][drive_item]s in the children relationship 138 | /// of the given one. 139 | /// 140 | /// # Response 141 | /// If successful, respond a fetcher for fetching changes from initial state (empty) to the snapshot of 142 | /// current states. See [`ListChildrenFetcher`][fetcher] for more details. 143 | /// 144 | /// If [`if_none_match`][if_none_match] is set and it matches the item tag, return an `None`. 145 | /// 146 | /// # See also 147 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-list-children?view=graph-rest-1.0) 148 | /// 149 | /// [drive_item]: ./resource/struct.DriveItem.html 150 | /// [if_none_match]: ./option/struct.CollectionOption.html#method.if_none_match 151 | /// [fetcher]: ./struct.ListChildrenFetcher.html 152 | pub async fn list_children_with_option<'a>( 153 | &self, 154 | item: impl Into>, 155 | option: CollectionOption, 156 | ) -> Result> { 157 | let opt_resp = self 158 | .client 159 | .get(api_url![&self.drive, &item.into(), "children"]) 160 | .apply(option) 161 | .bearer_auth(&self.token) 162 | .send() 163 | .await? 164 | .parse_optional() 165 | .await?; 166 | 167 | Ok(opt_resp.map(ListChildrenFetcher::new)) 168 | } 169 | 170 | /// Shortcut to `list_children_with_option` with default params, 171 | /// and fetch and collect all children. 172 | /// 173 | /// # See also 174 | /// [`list_children_with_option`][with_opt] 175 | /// 176 | /// [with_opt]: #method.list_children_with_option 177 | pub async fn list_children<'a>( 178 | &self, 179 | item: impl Into>, 180 | ) -> Result> { 181 | self.list_children_with_option(item, Default::default()) 182 | .await? 183 | .ok_or_else(|| Error::unexpected_response("Unexpected empty response"))? 184 | .fetch_all(self) 185 | .await 186 | } 187 | 188 | /// Get a `DriveItem` resource. 189 | /// 190 | /// Retrieve the metadata for a [`resource::DriveItem`][drive_item] by file system path or ID. 191 | /// 192 | /// # Errors 193 | /// Will return `Ok(None)` if [`if_none_match`][if_none_match] is set and it matches the item tag. 194 | /// 195 | /// # See also 196 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-get?view=graph-rest-1.0) 197 | /// 198 | /// [drive_item]: ./resource/struct.DriveItem.html 199 | /// [if_none_match]: ./option/struct.CollectionOption.html#method.if_none_match 200 | pub async fn get_item_with_option<'a>( 201 | &self, 202 | item: impl Into>, 203 | option: ObjectOption, 204 | ) -> Result> { 205 | self.client 206 | .get(api_url![&self.drive, &item.into()]) 207 | .apply(option) 208 | .bearer_auth(&self.token) 209 | .send() 210 | .await? 211 | .parse_optional() 212 | .await 213 | } 214 | 215 | /// Shortcut to `get_item_with_option` with default parameters. 216 | /// 217 | /// # See also 218 | /// [`get_item_with_option`][with_opt] 219 | /// 220 | /// [with_opt]: #method.get_item_with_option 221 | pub async fn get_item<'a>(&self, item: impl Into>) -> Result { 222 | self.get_item_with_option(item, Default::default()) 223 | .await? 224 | .ok_or_else(|| Error::unexpected_response("Unexpected empty response")) 225 | } 226 | 227 | /// Get a pre-authorized download URL for a file. 228 | /// 229 | /// The URL returned is only valid for a short period of time (a few minutes). 230 | /// 231 | /// # Note 232 | /// This API only works with reqwest redirection disabled, which is the default option set by 233 | /// [`OneDrive::new()`][new]. 234 | /// If the `OneDrive` instance is created by [`new_with_client()`][new_with_client], 235 | /// be sure the `reqwest::Client` has redirection disabled. 236 | /// 237 | /// Only `If-None-Match` is supported in `option`. 238 | /// 239 | /// # See also 240 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-get-content?view=graph-rest-1.0&tabs=http) 241 | /// 242 | /// [new]: #method.new 243 | /// [new_with_client]: #method.new_with_client 244 | pub async fn get_item_download_url_with_option<'a>( 245 | &self, 246 | item: impl Into>, 247 | option: ObjectOption, 248 | ) -> Result { 249 | let raw_resp = self 250 | .client 251 | .get(api_url![&self.drive, &item.into(), "content"]) 252 | .apply(option) 253 | .bearer_auth(&self.token) 254 | .send() 255 | .await?; 256 | let url = handle_error_response(raw_resp) 257 | .await? 258 | .headers() 259 | .get(header::LOCATION) 260 | .ok_or_else(|| { 261 | Error::unexpected_response( 262 | "Header `Location` not exists in response of `get_item_download_url`", 263 | ) 264 | })? 265 | .to_str() 266 | .map_err(|_| Error::unexpected_response("Invalid string header `Location`"))? 267 | .to_owned(); 268 | Ok(url) 269 | } 270 | 271 | /// Shortcut to [`get_item_download_url_with_option`] with default options. 272 | /// 273 | /// # See also 274 | /// [`get_item_download_url_with_option`] 275 | /// 276 | /// [`get_item_download_url_with_option`]: #method.get_item_downloda_url_with_option 277 | pub async fn get_item_download_url<'a>( 278 | &self, 279 | item: impl Into>, 280 | ) -> Result { 281 | self.get_item_download_url_with_option(item.into(), Default::default()) 282 | .await 283 | } 284 | 285 | /// Create a new [`DriveItem`][drive_item] allowing to set supported attributes. 286 | /// [`DriveItem`][drive_item] resources have facets modeled as properties that provide data 287 | /// about the [`DriveItem`][drive_item]'s identities and capabilities. You must provide one 288 | /// of the following facets to create an item: `bundle`, `file`, `folder`, `remote_item`. 289 | /// 290 | /// # Errors 291 | /// * Will result in `Err` with HTTP `409 CONFLICT` if [`conflict_behavior`][conflict_behavior] 292 | /// is set to [`Fail`][conflict_fail] and the target already exists. 293 | /// * Will result in `Err` with HTTP `400 BAD REQUEST` if facets are not properly set. 294 | /// 295 | /// # See also 296 | /// 297 | /// [Microsoft Docs](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children?view=graph-rest-1.0) 298 | /// 299 | /// [with_opt]: #method.create_folder_with_option 300 | /// [drive_item]: ./resource/struct.DriveItem.html 301 | /// [conflict_behavior]: ./option/struct.DriveItemPutOption.html#method.conflict_behavior 302 | /// [conflict_fail]: ./enum.ConflictBehavior.html#variant.Fail 303 | pub async fn create_drive_item<'a>( 304 | &self, 305 | parent_item: impl Into>, 306 | drive_item: DriveItem, 307 | option: DriveItemPutOption, 308 | ) -> Result { 309 | #[derive(Serialize)] 310 | struct Req { 311 | #[serde(rename = "@microsoft.graph.conflictBehavior")] 312 | conflict_behavior: ConflictBehavior, 313 | #[serde(flatten)] 314 | drive_item: DriveItem, 315 | } 316 | 317 | let conflict_behavior = option 318 | .get_conflict_behavior() 319 | .unwrap_or(ConflictBehavior::Fail); 320 | 321 | self.client 322 | .post(api_url![&self.drive, &parent_item.into(), "children"]) 323 | .bearer_auth(&self.token) 324 | .apply(option) 325 | .json(&Req { 326 | conflict_behavior, 327 | drive_item, 328 | }) 329 | .send() 330 | .await? 331 | .parse() 332 | .await 333 | } 334 | 335 | /// Create a new folder under an `DriveItem` 336 | /// 337 | /// Create a new folder [`DriveItem`][drive_item] with a specified parent item or path. 338 | /// 339 | /// # Errors 340 | /// Will result in `Err` with HTTP `409 CONFLICT` if [`conflict_behavior`][conflict_behavior] 341 | /// is set to [`Fail`][conflict_fail] and the target already exists. 342 | /// 343 | /// # See also 344 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-post-children?view=graph-rest-1.0) 345 | /// 346 | /// [drive_item]: ./resource/struct.DriveItem.html 347 | /// [conflict_behavior]: ./option/struct.DriveItemPutOption.html#method.conflict_behavior 348 | /// [conflict_fail]: ./enum.ConflictBehavior.html#variant.Fail 349 | pub async fn create_folder_with_option<'a>( 350 | &self, 351 | parent_item: impl Into>, 352 | name: &FileName, 353 | option: DriveItemPutOption, 354 | ) -> Result { 355 | let drive_item = DriveItem { 356 | name: Some(name.as_str().to_string()), 357 | folder: Some(json!({}).into()), 358 | ..Default::default() 359 | }; 360 | 361 | self.create_drive_item(parent_item, drive_item, option) 362 | .await 363 | } 364 | 365 | /// Shortcut to `create_folder_with_option` with default options. 366 | /// 367 | /// # See also 368 | /// [`create_folder_with_option`][with_opt] 369 | /// 370 | /// [with_opt]: #method.create_folder_with_option 371 | pub async fn create_folder<'a>( 372 | &self, 373 | parent_item: impl Into>, 374 | name: &FileName, 375 | ) -> Result { 376 | self.create_folder_with_option(parent_item, name, Default::default()) 377 | .await 378 | } 379 | 380 | /// Update `DriveItem` properties 381 | /// 382 | /// Update the metadata for a [`DriveItem`][drive_item]. 383 | /// 384 | /// If you want to rename or move an [`DriveItem`][drive_item] to another place, 385 | /// you should use [`move_`][move_] (or [`move_with_option`][move_with_opt]) instead of this, which is a wrapper 386 | /// to this API endpoint to make things easier. 387 | /// 388 | /// # See also 389 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-update?view=graph-rest-1.0) 390 | /// 391 | /// [drive_item]: ./resource/struct.DriveItem.html 392 | /// [move_]: #method.move_ 393 | /// [move_with_opt]: #method.move_with_option 394 | pub async fn update_item_with_option<'a>( 395 | &self, 396 | item: impl Into>, 397 | patch: &DriveItem, 398 | option: ObjectOption, 399 | ) -> Result { 400 | self.client 401 | .patch(api_url![&self.drive, &item.into()]) 402 | .bearer_auth(&self.token) 403 | .apply(option) 404 | .json(patch) 405 | .send() 406 | .await? 407 | .parse() 408 | .await 409 | } 410 | 411 | /// Shortcut to `update_item_with_option` with default options. 412 | /// 413 | /// # See also 414 | /// [`update_item_with_option`][with_opt] 415 | /// 416 | /// [with_opt]: #method.update_item_with_option 417 | pub async fn update_item<'a>( 418 | &self, 419 | item: impl Into>, 420 | patch: &DriveItem, 421 | ) -> Result { 422 | self.update_item_with_option(item, patch, Default::default()) 423 | .await 424 | } 425 | 426 | /// The upload size limit of [`upload_small`]. 427 | /// 428 | /// The value is from 429 | /// [OneDrive Developer documentation](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content?view=odsp-graph-online) 430 | /// (4MB) which is smaller than 431 | /// [Microsoft Graph documentation](https://docs.microsoft.com/en-us/graph/api/driveitem-put-content?view=graph-rest-1.0) 432 | /// (250MB). 433 | /// The exact limit is unknown. Here we chose the smaller one as a reference. 434 | /// 435 | /// [`upload_small`]: #method.upload_small 436 | pub const UPLOAD_SMALL_MAX_SIZE: usize = 4_000_000; // 4 MB 437 | 438 | /// Upload or replace the contents of a `DriveItem` file. 439 | /// 440 | /// The simple upload API allows you to provide the contents of a new file or 441 | /// update the contents of an existing file in a single API call. This method 442 | /// only supports files up to [`Self::UPLOAD_SMALL_MAX_SIZE`]. The length is not checked 443 | /// locally and request will still be sent for large data. 444 | /// 445 | /// # See also 446 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-put-content?view=graph-rest-1.0) 447 | /// 448 | /// [drive_item]: ./resource/struct.DriveItem.html 449 | pub async fn upload_small<'a>( 450 | &self, 451 | item: impl Into>, 452 | data: impl Into, 453 | ) -> Result { 454 | let data = data.into(); 455 | self.client 456 | .put(api_url![&self.drive, &item.into(), "content"]) 457 | .bearer_auth(&self.token) 458 | .header(header::CONTENT_TYPE, "application/octet-stream") 459 | .header(header::CONTENT_LENGTH, data.len().to_string()) 460 | .body(data) 461 | .send() 462 | .await? 463 | .parse() 464 | .await 465 | } 466 | 467 | /// Create an upload session. 468 | /// 469 | /// Create an upload session to allow your app to upload files up to 470 | /// the maximum file size. An upload session allows your app to 471 | /// upload ranges of the file in sequential API requests, which allows 472 | /// the transfer to be resumed if a connection is dropped 473 | /// while the upload is in progress. 474 | /// 475 | /// # Errors 476 | /// Will return `Err` with HTTP `412 PRECONDITION_FAILED` if [`if_match`][if_match] is set 477 | /// but does not match the item. 478 | /// 479 | /// # Note 480 | /// [`conflict_behavior`][conflict_behavior] is supported. 481 | /// 482 | /// # See also 483 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#create-an-upload-session) 484 | /// 485 | /// [if_match]: ./option/struct.CollectionOption.html#method.if_match 486 | /// [conflict_behavior]: ./option/struct.DriveItemPutOption.html#method.conflict_behavior 487 | /// [upload_sess]: ./struct.UploadSession.html 488 | /// [upload_part]: ./struct.UploadSession.html#method.upload_part 489 | pub async fn new_upload_session_with_initial_option<'a>( 490 | &self, 491 | item: impl Into>, 492 | initial: &DriveItem, 493 | option: DriveItemPutOption, 494 | ) -> Result<(UploadSession, UploadSessionMeta)> { 495 | #[derive(Serialize)] 496 | struct Item<'a> { 497 | #[serde(rename = "@microsoft.graph.conflictBehavior")] 498 | conflict_behavior: ConflictBehavior, 499 | #[serde(flatten)] 500 | initial: &'a DriveItem, 501 | } 502 | 503 | #[derive(Serialize)] 504 | struct Req<'a> { 505 | item: Item<'a>, 506 | } 507 | 508 | #[derive(Deserialize)] 509 | #[serde(rename_all = "camelCase")] 510 | struct Resp { 511 | upload_url: String, 512 | #[serde(flatten)] 513 | meta: UploadSessionMeta, 514 | } 515 | 516 | let conflict_behavior = option 517 | .get_conflict_behavior() 518 | .unwrap_or(ConflictBehavior::Fail); 519 | let resp: Resp = self 520 | .client 521 | .post(api_url![&self.drive, &item.into(), "createUploadSession"]) 522 | .apply(option) 523 | .bearer_auth(&self.token) 524 | .json(&Req { 525 | item: Item { 526 | conflict_behavior, 527 | initial, 528 | }, 529 | }) 530 | .send() 531 | .await? 532 | .parse() 533 | .await?; 534 | 535 | Ok(( 536 | UploadSession { 537 | upload_url: resp.upload_url, 538 | }, 539 | resp.meta, 540 | )) 541 | } 542 | 543 | /// Shortcut to [`new_upload_session_with_initial_option`] without initial attributes. 544 | /// 545 | /// [`new_upload_session_with_initial_option`]: #method.new_upload_session_with_initial_option 546 | pub async fn new_upload_session_with_option<'a>( 547 | &self, 548 | item: impl Into>, 549 | option: DriveItemPutOption, 550 | ) -> Result<(UploadSession, UploadSessionMeta)> { 551 | let initial = DriveItem::default(); 552 | self.new_upload_session_with_initial_option(item, &initial, option) 553 | .await 554 | } 555 | 556 | /// Shortcut to [`new_upload_session_with_option`] with `ConflictBehavior::Fail`. 557 | /// 558 | /// [`new_upload_session_with_option`]: #method.new_upload_session_with_option 559 | pub async fn new_upload_session<'a>( 560 | &self, 561 | item: impl Into>, 562 | ) -> Result<(UploadSession, UploadSessionMeta)> { 563 | self.new_upload_session_with_option(item, Default::default()) 564 | .await 565 | } 566 | 567 | /// Copy a `DriveItem`. 568 | /// 569 | /// Asynchronously creates a copy of an driveItem (including any children), 570 | /// under a new parent item or with a new name. 571 | /// 572 | /// # Note 573 | /// The conflict behavior is not mentioned in Microsoft Docs, and cannot be specified. 574 | /// 575 | /// But it seems to behave as [`Rename`][conflict_rename] if the destination folder is just the current 576 | /// parent folder, and [`Fail`][conflict_fail] otherwise. 577 | /// 578 | /// # See also 579 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-copy?view=graph-rest-1.0) 580 | /// 581 | /// [conflict_rename]: ./enum.ConflictBehavior.html#variant.Rename 582 | /// [conflict_fail]: ./enum.ConflictBehavior.html#variant.Fail 583 | pub async fn copy<'a, 'b>( 584 | &self, 585 | source_item: impl Into>, 586 | dest_folder: impl Into>, 587 | dest_name: &FileName, 588 | ) -> Result { 589 | #[derive(Serialize)] 590 | #[serde(rename_all = "camelCase")] 591 | struct Req<'a> { 592 | parent_reference: ItemReference<'a>, 593 | name: &'a str, 594 | } 595 | 596 | let raw_resp = self 597 | .client 598 | .post(api_url![&self.drive, &source_item.into(), "copy"]) 599 | .bearer_auth(&self.token) 600 | .json(&Req { 601 | parent_reference: ItemReference { 602 | path: api_path!(&dest_folder.into()), 603 | }, 604 | name: dest_name.as_str(), 605 | }) 606 | .send() 607 | .await?; 608 | 609 | let url = handle_error_response(raw_resp) 610 | .await? 611 | .headers() 612 | .get(header::LOCATION) 613 | .ok_or_else(|| { 614 | Error::unexpected_response("Header `Location` not exists in response of `copy`") 615 | })? 616 | .to_str() 617 | .map_err(|_| Error::unexpected_response("Invalid string header `Location`"))? 618 | .to_owned(); 619 | 620 | Ok(CopyProgressMonitor::from_monitor_url(url)) 621 | } 622 | 623 | /// Move a `DriveItem` to a new folder. 624 | /// 625 | /// This is a special case of the Update method. Your app can combine 626 | /// moving an item to a new container and updating other properties of 627 | /// the item into a single request. 628 | /// 629 | /// Note: Items cannot be moved between Drives using this request. 630 | /// 631 | /// # Note 632 | /// [`conflict_behavior`][conflict_behavior] is supported. 633 | /// 634 | /// # Errors 635 | /// Will return `Err` with HTTP `412 PRECONDITION_FAILED` if [`if_match`][if_match] is set 636 | /// but it does not match the item. 637 | /// 638 | /// # See also 639 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-move?view=graph-rest-1.0) 640 | /// 641 | /// [conflict_behavior]: ./option/struct.DriveItemPutOption.html#method.conflict_behavior 642 | /// [if_match]: ./option/struct.CollectionOption.html#method.if_match 643 | pub async fn move_with_option<'a, 'b>( 644 | &self, 645 | source_item: impl Into>, 646 | dest_folder: impl Into>, 647 | dest_name: Option<&FileName>, 648 | option: DriveItemPutOption, 649 | ) -> Result { 650 | #[derive(Serialize)] 651 | #[serde(rename_all = "camelCase")] 652 | struct Req<'a> { 653 | parent_reference: ItemReference<'a>, 654 | name: Option<&'a str>, 655 | #[serde(rename = "@microsoft.graph.conflictBehavior")] 656 | conflict_behavior: ConflictBehavior, 657 | } 658 | 659 | let conflict_behavior = option 660 | .get_conflict_behavior() 661 | .unwrap_or(ConflictBehavior::Fail); 662 | self.client 663 | .patch(api_url![&self.drive, &source_item.into()]) 664 | .bearer_auth(&self.token) 665 | .apply(option) 666 | .json(&Req { 667 | parent_reference: ItemReference { 668 | path: api_path!(&dest_folder.into()), 669 | }, 670 | name: dest_name.map(FileName::as_str), 671 | conflict_behavior, 672 | }) 673 | .send() 674 | .await? 675 | .parse() 676 | .await 677 | } 678 | 679 | /// Shortcut to `move_with_option` with `ConflictBehavior::Fail`. 680 | /// 681 | /// # See also 682 | /// [`move_with_option`][with_opt] 683 | /// 684 | /// [with_opt]: #method.move_with_option 685 | pub async fn move_<'a, 'b>( 686 | &self, 687 | source_item: impl Into>, 688 | dest_folder: impl Into>, 689 | dest_name: Option<&FileName>, 690 | ) -> Result { 691 | self.move_with_option(source_item, dest_folder, dest_name, Default::default()) 692 | .await 693 | } 694 | 695 | /// Delete a `DriveItem`. 696 | /// 697 | /// Delete a [`DriveItem`][drive_item] by using its ID or path. Note that deleting items using 698 | /// this method will move the items to the recycle bin instead of permanently 699 | /// deleting the item. 700 | /// 701 | /// # Error 702 | /// Will result in error with HTTP `412 PRECONDITION_FAILED` if [`if_match`][if_match] is set but 703 | /// does not match the item. 704 | /// 705 | /// # Panics 706 | /// [`conflict_behavior`][conflict_behavior] is **NOT** supported. Set it will cause a panic. 707 | /// 708 | /// # See also 709 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-delete?view=graph-rest-1.0) 710 | /// 711 | /// [drive_item]: ./resource/struct.DriveItem.html 712 | /// [if_match]: ./option/struct.CollectionOption.html#method.if_match 713 | /// [conflict_behavior]: ./option/struct.DriveItemPutOption.html#method.conflict_behavior 714 | pub async fn delete_with_option<'a>( 715 | &self, 716 | item: impl Into>, 717 | option: DriveItemPutOption, 718 | ) -> Result<()> { 719 | assert!( 720 | option.get_conflict_behavior().is_none(), 721 | "`conflict_behavior` is not supported by `delete[_with_option]`", 722 | ); 723 | 724 | self.client 725 | .delete(api_url![&self.drive, &item.into()]) 726 | .bearer_auth(&self.token) 727 | .apply(option) 728 | .send() 729 | .await? 730 | .parse_no_content() 731 | .await 732 | } 733 | 734 | /// Shortcut to `delete_with_option`. 735 | /// 736 | /// # See also 737 | /// [`delete_with_option`][with_opt] 738 | /// 739 | /// [with_opt]: #method.delete_with_option 740 | pub async fn delete<'a>(&self, item: impl Into>) -> Result<()> { 741 | self.delete_with_option(item, Default::default()).await 742 | } 743 | 744 | /// Track changes for root folder from initial state (empty state) to snapshot of current states. 745 | /// 746 | /// This method allows your app to track changes to a drive and its children over time. 747 | /// Deleted items are returned with the deleted facet. Items with this property set 748 | /// should be removed from your local state. 749 | /// 750 | /// Note: you should only delete a folder locally if it is empty after 751 | /// syncing all the changes. 752 | /// 753 | /// # Panics 754 | /// Track Changes API does not support [`$count=true` query parameter][dollar_count]. 755 | /// If [`CollectionOption::get_count`][opt_get_count] is set in option, it will panic. 756 | /// 757 | /// # Results 758 | /// Return a fetcher for fetching changes from initial state (empty) to the snapshot of 759 | /// current states. See [`TrackChangeFetcher`][fetcher] for more details. 760 | /// 761 | /// # See also 762 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-delta?view=graph-rest-1.0) 763 | /// 764 | /// [fetcher]: ./struct.TrackChangeFetcher.html 765 | /// [dollar_count]: https://docs.microsoft.com/en-us/graph/query-parameters#count-parameter 766 | /// [opt_get_count]: ./option/struct.CollectionOption.html#method.get_count 767 | pub async fn track_root_changes_from_initial_with_option( 768 | &self, 769 | option: CollectionOption, 770 | ) -> Result { 771 | assert!( 772 | !option.has_get_count(), 773 | "`get_count` is not supported by Track Changes API", 774 | ); 775 | let resp = self 776 | .client 777 | .get(api_url![&self.drive, "root", "delta"]) 778 | .apply(option) 779 | .bearer_auth(&self.token) 780 | .send() 781 | .await? 782 | .parse() 783 | .await?; 784 | Ok(TrackChangeFetcher::new(resp)) 785 | } 786 | 787 | /// Shortcut to `track_root_changes_from_initial_with_option` with default parameters. 788 | /// 789 | /// # See also 790 | /// [`track_root_changes_from_initial_with_option`][with_opt] 791 | /// 792 | /// [with_opt]: #method.track_root_changes_from_initial_with_option 793 | pub async fn track_root_changes_from_initial(&self) -> Result { 794 | self.track_root_changes_from_initial_with_option(Default::default()) 795 | .await 796 | } 797 | 798 | /// Track changes for root folder from snapshot (delta url) to snapshot of current states. 799 | /// 800 | /// # Note 801 | /// There is no `with_option` version of this function. Since delta URL already carries 802 | /// query parameters when you get it. The initial parameters will be automatically used 803 | /// in all following requests through delta URL. 804 | pub async fn track_root_changes_from_delta_url( 805 | &self, 806 | delta_url: &str, 807 | ) -> Result { 808 | let resp: DriveItemCollectionResponse = self 809 | .client 810 | .get(delta_url) 811 | .bearer_auth(&self.token) 812 | .send() 813 | .await? 814 | .parse() 815 | .await?; 816 | Ok(TrackChangeFetcher::new(resp)) 817 | } 818 | 819 | /// Get a delta url representing the snapshot of current states of root folder. 820 | /// 821 | /// The delta url can be used in [`track_root_changes_from_delta_url`][track_from_delta] later 822 | /// to get diffs between two snapshots of states. 823 | /// 824 | /// Note that options (query parameters) are saved in delta url, so they are applied to all later 825 | /// requests by `track_changes_from_delta_url` without need for specifying them every time. 826 | /// 827 | /// # Panics 828 | /// Track Changes API does not support [`$count=true` query parameter][dollar_count]. 829 | /// If [`CollectionOption::get_count`][opt_get_count] is set in option, it will panic. 830 | /// 831 | /// # See also 832 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-delta?view=graph-rest-1.0#retrieving-the-current-deltalink) 833 | /// 834 | /// [track_from_delta]: #method.track_root_changes_from_delta_url 835 | /// [dollar_count]: https://docs.microsoft.com/en-us/graph/query-parameters#count-parameter 836 | /// [opt_get_count]: ./option/struct.CollectionOption.html#method.get_count 837 | pub async fn get_root_latest_delta_url_with_option( 838 | &self, 839 | option: CollectionOption, 840 | ) -> Result { 841 | assert!( 842 | !option.has_get_count(), 843 | "`get_count` is not supported by Track Changes API", 844 | ); 845 | self.client 846 | .get(api_url![&self.drive, "root", "delta"]) 847 | .query(&[("token", "latest")]) 848 | .apply(option) 849 | .bearer_auth(&self.token) 850 | .send() 851 | .await? 852 | .parse::() 853 | .await? 854 | .delta_url 855 | .ok_or_else(|| { 856 | Error::unexpected_response( 857 | "Missing field `@odata.deltaLink` for getting latest delta", 858 | ) 859 | }) 860 | } 861 | 862 | /// Shortcut to `get_root_latest_delta_url_with_option` with default parameters. 863 | /// 864 | /// # See also 865 | /// [`get_root_latest_delta_url_with_option`][with_opt] 866 | /// 867 | /// [with_opt]: #method.get_root_latest_delta_url_with_option 868 | pub async fn get_root_latest_delta_url(&self) -> Result { 869 | self.get_root_latest_delta_url_with_option(Default::default()) 870 | .await 871 | } 872 | } 873 | 874 | /// The monitor for checking the progress of a asynchronous `copy` operation. 875 | /// 876 | /// # Notes 877 | /// This struct is always present. But since retrieving copy progress requires beta API, 878 | /// it is useless due to the lack of method `fetch_progress` if feature `beta` is not enabled. 879 | /// 880 | /// # See also 881 | /// [`OneDrive::copy`][copy] 882 | /// 883 | /// [Microsoft docs](https://docs.microsoft.com/en-us/graph/long-running-actions-overview) 884 | /// 885 | /// [copy]: ./struct.OneDrive.html#method.copy 886 | #[derive(Debug, Clone)] 887 | pub struct CopyProgressMonitor { 888 | monitor_url: String, 889 | } 890 | 891 | /// The progress of a asynchronous `copy` operation. (Beta) 892 | /// 893 | /// # See also 894 | /// [Microsoft Docs Beta](https://docs.microsoft.com/en-us/graph/api/resources/asyncjobstatus?view=graph-rest-beta) 895 | #[cfg(feature = "beta")] 896 | #[allow(missing_docs)] 897 | #[derive(Debug, Clone, Deserialize)] 898 | #[non_exhaustive] 899 | #[serde(rename_all = "camelCase")] 900 | pub struct CopyProgress { 901 | pub percentage_complete: f64, 902 | pub status: CopyStatus, 903 | } 904 | 905 | /// The status of a `copy` operation. (Beta) 906 | /// 907 | /// # See also 908 | /// [`CopyProgress`][copy_progress] 909 | /// 910 | /// [Microsoft Docs Beta](https://docs.microsoft.com/en-us/graph/api/resources/asyncjobstatus?view=graph-rest-beta#json-representation) 911 | /// 912 | /// [copy_progress]: ./struct.CopyProgress.html 913 | #[cfg(feature = "beta")] 914 | #[allow(missing_docs)] 915 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] 916 | #[serde(rename_all = "camelCase")] 917 | #[non_exhaustive] 918 | pub enum CopyStatus { 919 | NotStarted, 920 | InProgress, 921 | Completed, 922 | Updating, 923 | Failed, 924 | DeletePending, 925 | DeleteFailed, 926 | Waiting, 927 | } 928 | 929 | impl CopyProgressMonitor { 930 | /// Make a progress monitor using existing `monitor_url`. 931 | /// 932 | /// `monitor_url` should be got from [`CopyProgressMonitor::monitor_url`][monitor_url] 933 | /// 934 | /// [monitor_url]: #method.monitor_url 935 | pub fn from_monitor_url(monitor_url: impl Into) -> Self { 936 | Self { 937 | monitor_url: monitor_url.into(), 938 | } 939 | } 940 | 941 | /// Get the monitor url. 942 | #[must_use] 943 | pub fn monitor_url(&self) -> &str { 944 | &self.monitor_url 945 | } 946 | 947 | /// Fetch the `copy` progress. (Beta) 948 | /// 949 | /// # See also 950 | /// [`CopyProgress`][copy_progress] 951 | /// 952 | /// [copy_progress]: ./struct.CopyProgress.html 953 | #[cfg(feature = "beta")] 954 | pub async fn fetch_progress(&self, onedrive: &OneDrive) -> Result { 955 | // No bearer auth. 956 | onedrive 957 | .client 958 | .get(&self.monitor_url) 959 | .send() 960 | .await? 961 | .parse() 962 | .await 963 | } 964 | } 965 | 966 | #[derive(Debug, Deserialize)] 967 | struct DriveItemCollectionResponse { 968 | value: Option>, 969 | #[serde(rename = "@odata.nextLink")] 970 | next_url: Option, 971 | #[serde(rename = "@odata.deltaLink")] 972 | delta_url: Option, 973 | } 974 | 975 | #[derive(Debug)] 976 | struct DriveItemFetcher { 977 | last_response: DriveItemCollectionResponse, 978 | } 979 | 980 | impl DriveItemFetcher { 981 | fn new(first_response: DriveItemCollectionResponse) -> Self { 982 | Self { 983 | last_response: first_response, 984 | } 985 | } 986 | 987 | fn resume_from(next_url: impl Into) -> Self { 988 | Self::new(DriveItemCollectionResponse { 989 | value: None, 990 | next_url: Some(next_url.into()), 991 | delta_url: None, 992 | }) 993 | } 994 | 995 | fn next_url(&self) -> Option<&str> { 996 | // Return `None` for the first page, or it will 997 | // lost items of the first page when resumed. 998 | match &self.last_response { 999 | DriveItemCollectionResponse { 1000 | value: None, 1001 | next_url: Some(next_url), 1002 | .. 1003 | } => Some(next_url), 1004 | _ => None, 1005 | } 1006 | } 1007 | 1008 | fn delta_url(&self) -> Option<&str> { 1009 | self.last_response.delta_url.as_deref() 1010 | } 1011 | 1012 | async fn fetch_next_page(&mut self, onedrive: &OneDrive) -> Result>> { 1013 | if let Some(items) = self.last_response.value.take() { 1014 | return Ok(Some(items)); 1015 | } 1016 | let url = match self.last_response.next_url.as_ref() { 1017 | None => return Ok(None), 1018 | Some(url) => url, 1019 | }; 1020 | self.last_response = onedrive 1021 | .client 1022 | .get(url) 1023 | .bearer_auth(&onedrive.token) 1024 | .send() 1025 | .await? 1026 | .parse() 1027 | .await?; 1028 | Ok(Some(self.last_response.value.take().unwrap_or_default())) 1029 | } 1030 | 1031 | async fn fetch_all(mut self, onedrive: &OneDrive) -> Result<(Vec, Option)> { 1032 | let mut buf = vec![]; 1033 | while let Some(items) = self.fetch_next_page(onedrive).await? { 1034 | buf.extend(items); 1035 | } 1036 | Ok((buf, self.delta_url().map(Into::into))) 1037 | } 1038 | } 1039 | 1040 | /// The page fetcher for listing children 1041 | /// 1042 | /// # See also 1043 | /// [`OneDrive::list_children_with_option`][list_children_with_opt] 1044 | /// 1045 | /// [list_children_with_opt]: ./struct.OneDrive.html#method.list_children_with_option 1046 | #[derive(Debug)] 1047 | pub struct ListChildrenFetcher { 1048 | fetcher: DriveItemFetcher, 1049 | } 1050 | 1051 | impl ListChildrenFetcher { 1052 | fn new(first_response: DriveItemCollectionResponse) -> Self { 1053 | Self { 1054 | fetcher: DriveItemFetcher::new(first_response), 1055 | } 1056 | } 1057 | 1058 | /// Resume a fetching process from url from 1059 | /// [`ListChildrenFetcher::next_url`][next_url]. 1060 | /// 1061 | /// [next_url]: #method.next_url 1062 | #[must_use] 1063 | pub fn resume_from(next_url: impl Into) -> Self { 1064 | Self { 1065 | fetcher: DriveItemFetcher::resume_from(next_url), 1066 | } 1067 | } 1068 | 1069 | /// Try to get the url to the next page. 1070 | /// 1071 | /// Used for resuming the fetching progress. 1072 | /// 1073 | /// # Error 1074 | /// Will success only if there are more pages and the first page is already read. 1075 | /// 1076 | /// # Note 1077 | /// The first page data from [`OneDrive::list_children_with_option`][list_children_with_opt] 1078 | /// will be cached and have no idempotent url to resume/re-fetch. 1079 | /// 1080 | /// [list_children_with_opt]: ./struct.OneDrive.html#method.list_children_with_option 1081 | #[must_use] 1082 | pub fn next_url(&self) -> Option<&str> { 1083 | self.fetcher.next_url() 1084 | } 1085 | 1086 | /// Fetch the next page, or `None` if reaches the end. 1087 | pub async fn fetch_next_page(&mut self, onedrive: &OneDrive) -> Result>> { 1088 | self.fetcher.fetch_next_page(onedrive).await 1089 | } 1090 | 1091 | /// Fetch all rest pages and collect all items. 1092 | /// 1093 | /// # Errors 1094 | /// 1095 | /// Any error occurs when fetching will lead to an failure, and 1096 | /// all progress will be lost. 1097 | pub async fn fetch_all(self, onedrive: &OneDrive) -> Result> { 1098 | self.fetcher 1099 | .fetch_all(onedrive) 1100 | .await 1101 | .map(|(items, _)| items) 1102 | } 1103 | } 1104 | 1105 | /// The page fetcher for tracking operations with `Iterator` interface. 1106 | /// 1107 | /// # See also 1108 | /// [`OneDrive::track_changes_from_initial`][track_initial] 1109 | /// 1110 | /// [`OneDrive::track_changes_from_delta_url`][track_delta] 1111 | /// 1112 | /// [track_initial]: ./struct.OneDrive.html#method.track_changes_from_initial_with_option 1113 | /// [track_delta]: ./struct.OneDrive.html#method.track_changes_from_delta_url 1114 | #[derive(Debug)] 1115 | pub struct TrackChangeFetcher { 1116 | fetcher: DriveItemFetcher, 1117 | } 1118 | 1119 | impl TrackChangeFetcher { 1120 | fn new(first_response: DriveItemCollectionResponse) -> Self { 1121 | Self { 1122 | fetcher: DriveItemFetcher::new(first_response), 1123 | } 1124 | } 1125 | 1126 | /// Resume a fetching process from url. 1127 | /// 1128 | /// The url should be from [`TrackChangeFetcher::next_url`][next_url]. 1129 | /// 1130 | /// [next_url]: #method.next_url 1131 | #[must_use] 1132 | pub fn resume_from(next_url: impl Into) -> Self { 1133 | Self { 1134 | fetcher: DriveItemFetcher::resume_from(next_url), 1135 | } 1136 | } 1137 | 1138 | /// Try to get the url to the next page. 1139 | /// 1140 | /// Used for resuming the fetching progress. 1141 | /// 1142 | /// # Error 1143 | /// Will success only if there are more pages and the first page is already read. 1144 | /// 1145 | /// # Note 1146 | /// The first page data from 1147 | /// [`OneDrive::track_changes_from_initial_with_option`][track_initial] 1148 | /// will be cached and have no idempotent url to resume/re-fetch. 1149 | /// 1150 | /// [track_initial]: ./struct.OneDrive.html#method.track_changes_from_initial 1151 | #[must_use] 1152 | pub fn next_url(&self) -> Option<&str> { 1153 | self.fetcher.next_url() 1154 | } 1155 | 1156 | /// Try to the delta url representing a snapshot of current track change operation. 1157 | /// 1158 | /// Used for tracking changes from this snapshot (rather than initial) later, 1159 | /// using [`OneDrive::track_changes_from_delta_url`][track_delta]. 1160 | /// 1161 | /// # Error 1162 | /// Will success only if there are no more pages. 1163 | /// 1164 | /// # See also 1165 | /// [`OneDrive::track_changes_from_delta_url`][track_delta] 1166 | /// 1167 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-delta?view=graph-rest-1.0#example-last-page-in-a-set) 1168 | /// 1169 | /// [track_delta]: ./struct.OneDrive.html#method.track_changes_from_delta_url 1170 | #[must_use] 1171 | pub fn delta_url(&self) -> Option<&str> { 1172 | self.fetcher.delta_url() 1173 | } 1174 | 1175 | /// Fetch the next page, or `None` if reaches the end. 1176 | pub async fn fetch_next_page(&mut self, onedrive: &OneDrive) -> Result>> { 1177 | self.fetcher.fetch_next_page(onedrive).await 1178 | } 1179 | 1180 | /// Fetch all rest pages, collect all items, and also return `delta_url`. 1181 | /// 1182 | /// # Errors 1183 | /// 1184 | /// Any error occurs when fetching will lead to an failure, and 1185 | /// all progress will be lost. 1186 | pub async fn fetch_all(self, onedrive: &OneDrive) -> Result<(Vec, String)> { 1187 | let (items, opt_delta_url) = self.fetcher.fetch_all(onedrive).await?; 1188 | let delta_url = opt_delta_url.ok_or_else(|| { 1189 | Error::unexpected_response("Missing `@odata.deltaLink` for the last page") 1190 | })?; 1191 | Ok((items, delta_url)) 1192 | } 1193 | } 1194 | 1195 | #[derive(Serialize)] 1196 | struct ItemReference<'a> { 1197 | path: &'a str, 1198 | } 1199 | 1200 | /// An upload session for resumable file uploading process. 1201 | /// 1202 | /// # See also 1203 | /// [`OneDrive::new_upload_session`][get_session] 1204 | /// 1205 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/resources/uploadsession?view=graph-rest-1.0) 1206 | /// 1207 | /// [get_session]: ./struct.OneDrive.html#method.new_upload_session 1208 | #[derive(Debug)] 1209 | pub struct UploadSession { 1210 | upload_url: String, 1211 | } 1212 | 1213 | /// Metadata of an in-progress upload session 1214 | /// 1215 | /// # See also 1216 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#resuming-an-in-progress-upload) 1217 | #[derive(Debug, Deserialize)] 1218 | #[serde(rename_all = "camelCase")] 1219 | #[non_exhaustive] 1220 | pub struct UploadSessionMeta { 1221 | /// Get a collection of byte ranges that the server is missing for the file. 1222 | /// 1223 | /// Used for determine what to upload when resuming a session. 1224 | pub next_expected_ranges: Vec, 1225 | /// Get the date and time in UTC that the upload session will expire. 1226 | /// 1227 | /// The complete file must be uploaded before this expiration time is reached. 1228 | pub expiration_date_time: TimestampString, 1229 | } 1230 | 1231 | impl UploadSession { 1232 | /// The upload size limit of a single [`upload_part`] call. 1233 | /// 1234 | /// The value is from 1235 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#upload-bytes-to-the-upload-session) 1236 | /// and may not be accurate or stable. 1237 | /// 1238 | /// [`upload_part`]: #method.upload_part 1239 | pub const MAX_PART_SIZE: usize = 60 << 20; // 60 MiB 1240 | 1241 | /// Construct back the upload session from upload URL. 1242 | pub fn from_upload_url(upload_url: impl Into) -> Self { 1243 | Self { 1244 | upload_url: upload_url.into(), 1245 | } 1246 | } 1247 | 1248 | /// Query the metadata of the upload to find out which byte ranges 1249 | /// have been received previously. 1250 | /// 1251 | /// # See also 1252 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#resuming-an-in-progress-upload) 1253 | pub async fn get_meta(&self, client: &Client) -> Result { 1254 | // No bearer auth. 1255 | client 1256 | .get(&self.upload_url) 1257 | .send() 1258 | .await? 1259 | .parse::() 1260 | .await 1261 | } 1262 | 1263 | /// The URL endpoint accepting PUT requests. 1264 | /// 1265 | /// It is exactly what you passed in [`UploadSession::from_upload_url`]. 1266 | /// 1267 | /// [`UploadSession::from_upload_url`]: #method.new 1268 | #[must_use] 1269 | pub fn upload_url(&self) -> &str { 1270 | &self.upload_url 1271 | } 1272 | 1273 | /// Cancel the upload session 1274 | /// 1275 | /// This cleans up the temporary file holding the data previously uploaded. 1276 | /// This should be used in scenarios where the upload is aborted, for example, 1277 | /// if the user cancels the transfer. 1278 | /// 1279 | /// Temporary files and their accompanying upload session are automatically 1280 | /// cleaned up after the `expirationDateTime` has passed. Temporary files may 1281 | /// not be deleted immediately after the expiration time has elapsed. 1282 | /// 1283 | /// # See also 1284 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#cancel-the-upload-session) 1285 | pub async fn delete(&self, client: &Client) -> Result<()> { 1286 | // No bearer auth. 1287 | client 1288 | .delete(&self.upload_url) 1289 | .send() 1290 | .await? 1291 | .parse_no_content() 1292 | .await 1293 | } 1294 | 1295 | /// Upload bytes to an upload session 1296 | /// 1297 | /// You can upload the entire file, or split the file into multiple byte ranges, 1298 | /// as long as the maximum bytes in any given request is less than 60 MiB. 1299 | /// The fragments of the file must be uploaded sequentially in order. Uploading 1300 | /// fragments out of order will result in an error. 1301 | /// 1302 | /// # Notes 1303 | /// If your app splits a file into multiple byte ranges, the size of each 1304 | /// byte range MUST be a multiple of 320 KiB (327,680 bytes). Using a fragment 1305 | /// size that does not divide evenly by 320 KiB will result in errors committing 1306 | /// some files. The 60 MiB limit and 320 KiB alignment are not checked locally since 1307 | /// they may change in the future. 1308 | /// 1309 | /// The `file_size` of all part upload requests should be identical. 1310 | /// 1311 | /// # Results 1312 | /// - If the part is uploaded successfully, but the file is not complete yet, 1313 | /// will return `None`. 1314 | /// - If this is the last part and it is uploaded successfully, 1315 | /// will return `Some()`. 1316 | /// 1317 | /// # Errors 1318 | /// When the file is completely uploaded, if an item with the same name is created 1319 | /// during uploading, the last `upload_to_session` call will return `Err` with 1320 | /// HTTP `409 CONFLICT`. 1321 | /// 1322 | /// # Panics 1323 | /// Panic if `remote_range` is invalid or not match the length of `data`. 1324 | /// 1325 | /// # See also 1326 | /// [Microsoft Docs](https://docs.microsoft.com/en-us/graph/api/driveitem-createuploadsession?view=graph-rest-1.0#upload-bytes-to-the-upload-session) 1327 | pub async fn upload_part( 1328 | &self, 1329 | data: impl Into, 1330 | remote_range: std::ops::Range, 1331 | file_size: u64, 1332 | client: &Client, 1333 | ) -> Result> { 1334 | use std::convert::TryFrom as _; 1335 | 1336 | let data = data.into(); 1337 | assert!(!data.is_empty(), "Empty data"); 1338 | assert!( 1339 | remote_range.start < remote_range.end && remote_range.end <= file_size 1340 | // `Range` has no method `len()`. 1341 | && remote_range.end - remote_range.start <= u64::try_from(data.len()).unwrap(), 1342 | "Invalid remote range", 1343 | ); 1344 | 1345 | // No bearer auth. 1346 | client 1347 | .put(&self.upload_url) 1348 | .header( 1349 | header::CONTENT_RANGE, 1350 | format!( 1351 | "bytes {}-{}/{}", 1352 | remote_range.start, 1353 | // Inclusive. 1354 | // We checked `remote_range.start < remote_range.end`, 1355 | // so this never overflows. 1356 | remote_range.end - 1, 1357 | file_size, 1358 | ), 1359 | ) 1360 | .body(data) 1361 | .send() 1362 | .await? 1363 | .parse_optional() 1364 | .await 1365 | } 1366 | } 1367 | 1368 | #[cfg(test)] 1369 | mod test { 1370 | use super::*; 1371 | use crate::ItemId; 1372 | 1373 | #[test] 1374 | fn test_api_url() { 1375 | let mock_item_id = ItemId("1234".to_owned()); 1376 | assert_eq!( 1377 | api_path!(&ItemLocation::from_id(&mock_item_id)), 1378 | "/drive/items/1234", 1379 | ); 1380 | 1381 | assert_eq!( 1382 | api_path!(&ItemLocation::from_path("/dir/file name").unwrap()), 1383 | "/drive/root:%2Fdir%2Ffile%20name:", 1384 | ); 1385 | } 1386 | 1387 | #[test] 1388 | fn test_path_name_check() { 1389 | let invalid_names = ["", ".*?", "a|b", "ab", ":run", "/", "\\"]; 1390 | let valid_names = [ 1391 | "QAQ", 1392 | "0", 1393 | ".", 1394 | "a-a:", // Unicode colon "\u{ff1a}" 1395 | "魔理沙", 1396 | ]; 1397 | 1398 | let check_name = |s: &str| FileName::new(s).is_some(); 1399 | let check_path = |s: &str| ItemLocation::from_path(s).is_some(); 1400 | 1401 | for s in &valid_names { 1402 | assert!(check_name(s), "{}", s); 1403 | let path = format!("/{s}"); 1404 | assert!(check_path(&path), "{}", path); 1405 | 1406 | for s2 in &valid_names { 1407 | let mut path = format!("/{s}/{s2}"); 1408 | assert!(check_path(&path), "{}", path); 1409 | path.push('/'); // Trailing 1410 | assert!(check_path(&path), "{}", path); 1411 | } 1412 | } 1413 | 1414 | for s in &invalid_names { 1415 | assert!(!check_name(s), "{}", s); 1416 | 1417 | // `/` and `/xx/` is valid and is tested below. 1418 | if s.is_empty() { 1419 | continue; 1420 | } 1421 | 1422 | let path = format!("/{s}"); 1423 | assert!(!check_path(&path), "{}", path); 1424 | 1425 | for s2 in &valid_names { 1426 | let path = format!("/{s2}/{s}"); 1427 | assert!(!check_path(&path), "{}", path); 1428 | } 1429 | } 1430 | 1431 | assert!(check_path("/")); 1432 | assert!(check_path("/a")); 1433 | assert!(check_path("/a/")); 1434 | assert!(check_path("/a/b")); 1435 | assert!(check_path("/a/b/")); 1436 | 1437 | assert!(!check_path("")); 1438 | assert!(!check_path("/a/b//")); 1439 | assert!(!check_path("a")); 1440 | assert!(!check_path("a/")); 1441 | assert!(!check_path("//")); 1442 | } 1443 | } 1444 | --------------------------------------------------------------------------------