This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 61fbd696d feat(services/dropbox): Implement refresh token support 
(#2604)
61fbd696d is described below

commit 61fbd696d963b96130ff8ab4aabb3ab51111d0b7
Author: Xuanwo <[email protected]>
AuthorDate: Fri Jul 7 13:04:25 2023 +0800

    feat(services/dropbox): Implement refresh token support (#2604)
    
    * Refactor
    
    Signed-off-by: Xuanwo <[email protected]>
    
    * Refactor error handling
    
    Signed-off-by: Xuanwo <[email protected]>
    
    * Save work
    
    Signed-off-by: Xuanwo <[email protected]>
    
    * Fix typo
    
    Signed-off-by: Xuanwo <[email protected]>
    
    * Add early return
    
    Signed-off-by: Xuanwo <[email protected]>
    
    ---------
    
    Signed-off-by: Xuanwo <[email protected]>
---
 core/src/services/dropbox/backend.rs  |  56 +++++++++++-
 core/src/services/dropbox/builder.rs  | 103 +++++++++++++++++++---
 core/src/services/dropbox/core.rs     | 157 ++++++++++++++++++++++++++--------
 core/src/services/dropbox/error.rs    |  83 ++++++++----------
 core/src/services/dropbox/mod.rs      |   1 -
 core/src/services/dropbox/response.rs |  86 -------------------
 6 files changed, 305 insertions(+), 181 deletions(-)

diff --git a/core/src/services/dropbox/backend.rs 
b/core/src/services/dropbox/backend.rs
index f0ae8cde0..4af8f01b6 100644
--- a/core/src/services/dropbox/backend.rs
+++ b/core/src/services/dropbox/backend.rs
@@ -20,10 +20,10 @@ use std::sync::Arc;
 
 use async_trait::async_trait;
 use http::StatusCode;
+use serde::Deserialize;
 
 use super::core::DropboxCore;
 use super::error::parse_error;
-use super::response::DropboxMetadataResponse;
 use super::writer::DropboxWriter;
 use crate::raw::*;
 use crate::*;
@@ -163,3 +163,57 @@ impl Accessor for DropboxBackend {
         }
     }
 }
+
+#[derive(Default, Debug, Deserialize)]
+#[serde(default)]
+pub struct DropboxMetadataResponse {
+    #[serde(rename(deserialize = ".tag"))]
+    pub tag: String,
+    pub client_modified: String,
+    pub content_hash: Option<String>,
+    pub file_lock_info: Option<DropboxMetadataFileLockInfo>,
+    pub has_explicit_shared_members: Option<bool>,
+    pub id: String,
+    pub is_downloadable: Option<bool>,
+    pub name: String,
+    pub path_display: String,
+    pub path_lower: String,
+    pub property_groups: Option<Vec<DropboxMetadataPropertyGroup>>,
+    pub rev: Option<String>,
+    pub server_modified: Option<String>,
+    pub sharing_info: Option<DropboxMetadataSharingInfo>,
+    pub size: Option<u64>,
+}
+
+#[derive(Default, Debug, Deserialize)]
+#[serde(default)]
+pub struct DropboxMetadataFileLockInfo {
+    pub created: Option<String>,
+    pub is_lockholder: bool,
+    pub lockholder_name: Option<String>,
+}
+
+#[derive(Default, Debug, Deserialize)]
+#[serde(default)]
+pub struct DropboxMetadataPropertyGroup {
+    pub fields: Vec<DropboxMetadataPropertyGroupField>,
+    pub template_id: String,
+}
+
+#[derive(Default, Debug, Deserialize)]
+#[serde(default)]
+pub struct DropboxMetadataPropertyGroupField {
+    pub name: String,
+    pub value: String,
+}
+
+#[derive(Default, Debug, Deserialize)]
+#[serde(default)]
+pub struct DropboxMetadataSharingInfo {
+    pub modified_by: Option<String>,
+    pub parent_shared_folder_id: Option<String>,
+    pub read_only: Option<bool>,
+    pub shared_folder_id: Option<String>,
+    pub traverse_only: Option<bool>,
+    pub no_access: Option<bool>,
+}
diff --git a/core/src/services/dropbox/builder.rs 
b/core/src/services/dropbox/builder.rs
index 373c83a9c..2827d86a9 100644
--- a/core/src/services/dropbox/builder.rs
+++ b/core/src/services/dropbox/builder.rs
@@ -20,8 +20,13 @@ use std::fmt::Debug;
 use std::fmt::Formatter;
 use std::sync::Arc;
 
+use chrono::DateTime;
+use chrono::Utc;
+use tokio::sync::Mutex;
+
 use super::backend::DropboxBackend;
 use super::core::DropboxCore;
+use super::core::DropboxSigner;
 use crate::raw::*;
 use crate::*;
 
@@ -72,8 +77,12 @@ use crate::*;
 
 #[derive(Default)]
 pub struct DropboxBuilder {
-    access_token: Option<String>,
     root: Option<String>,
+    access_token: Option<String>,
+    refresh_token: Option<String>,
+    client_id: Option<String>,
+    client_secret: Option<String>,
+
     http_client: Option<HttpClient>,
 }
 
@@ -84,15 +93,47 @@ impl Debug for DropboxBuilder {
 }
 
 impl DropboxBuilder {
-    /// default: no access token, which leads to failure
+    /// Set the root directory for dropbox.
+    ///
+    /// Default to `/` if not set.
+    pub fn root(&mut self, root: &str) -> &mut Self {
+        self.root = Some(root.to_string());
+        self
+    }
+
+    /// Access token is used for temporary access to the Dropbox API.
+    ///
+    /// You can get the access token from [Dropbox App 
Console](https://www.dropbox.com/developers/apps)
+    ///
+    /// NOTE: this token will be expired in 4 hours. If you are trying to use 
dropbox services in a long time, please set a refresh_token instead.
     pub fn access_token(&mut self, access_token: &str) -> &mut Self {
         self.access_token = Some(access_token.to_string());
         self
     }
 
-    /// default: no root path, which leads to failure
-    pub fn root(&mut self, root: &str) -> &mut Self {
-        self.root = Some(root.to_string());
+    /// Refersh token is used for long term access to the Dropbox API.
+    ///
+    /// You can get the refresh token via OAuth2.0 Flow of dropbox.
+    ///
+    /// OpenDAL will use this refresh token to get a new access token when the 
old one is expired.
+    pub fn refresh_token(&mut self, refresh_token: &str) -> &mut Self {
+        self.refresh_token = Some(refresh_token.to_string());
+        self
+    }
+
+    /// Set the client id for dropbox.
+    ///
+    /// This is required for OAuth2.0 Flow with refresh token.
+    pub fn client_id(&mut self, client_id: &str) -> &mut Self {
+        self.client_id = Some(client_id.to_string());
+        self
+    }
+
+    /// Set the client secret for dropbox.
+    ///
+    /// This is required for OAuth2.0 Flow with refresh token.
+    pub fn client_secret(&mut self, client_secret: &str) -> &mut Self {
+        self.client_secret = Some(client_secret.to_string());
         self
     }
 
@@ -116,6 +157,9 @@ impl Builder for DropboxBuilder {
         let mut builder = Self::default();
         map.get("root").map(|v| builder.root(v));
         map.get("access_token").map(|v| builder.access_token(v));
+        map.get("refresh_token").map(|v| builder.refresh_token(v));
+        map.get("client_id").map(|v| builder.client_id(v));
+        map.get("client_secret").map(|v| builder.client_secret(v));
         builder
     }
 
@@ -129,20 +173,57 @@ impl Builder for DropboxBuilder {
                     .with_context("service", Scheme::Dropbox)
             })?
         };
-        let token = match self.access_token.clone() {
-            Some(access_token) => access_token,
-            None => {
+
+        let signer = match (self.access_token.take(), 
self.refresh_token.take()) {
+            (Some(access_token), None) => DropboxSigner {
+                access_token,
+                // We will never expire user specified token.
+                expires_in: DateTime::<Utc>::MAX_UTC,
+                ..Default::default()
+            },
+            (None, Some(refresh_token)) => {
+                let client_id = self.client_id.take().ok_or_else(|| {
+                    Error::new(
+                        ErrorKind::ConfigInvalid,
+                        "client_id must be set when refresh_token is set",
+                    )
+                    .with_context("service", Scheme::Dropbox)
+                })?;
+                let client_secret = self.client_secret.take().ok_or_else(|| {
+                    Error::new(
+                        ErrorKind::ConfigInvalid,
+                        "client_secret must be set when refresh_token is set",
+                    )
+                    .with_context("service", Scheme::Dropbox)
+                })?;
+
+                DropboxSigner {
+                    refresh_token,
+                    client_id,
+                    client_secret,
+                    ..Default::default()
+                }
+            }
+            (Some(_), Some(_)) => {
+                return Err(Error::new(
+                    ErrorKind::ConfigInvalid,
+                    "access_token and refresh_token can not be set at the same 
time",
+                )
+                .with_context("service", Scheme::Dropbox))
+            }
+            (None, None) => {
                 return Err(Error::new(
                     ErrorKind::ConfigInvalid,
-                    "access_token is required",
-                ))
+                    "access_token or refresh_token must be set",
+                )
+                .with_context("service", Scheme::Dropbox))
             }
         };
 
         Ok(DropboxBackend {
             core: Arc::new(DropboxCore {
                 root,
-                token,
+                signer: Arc::new(Mutex::new(signer)),
                 client,
             }),
         })
diff --git a/core/src/services/dropbox/core.rs 
b/core/src/services/dropbox/core.rs
index 790d69fcd..bbb2e5208 100644
--- a/core/src/services/dropbox/core.rs
+++ b/core/src/services/dropbox/core.rs
@@ -18,16 +18,22 @@
 use std::default::Default;
 use std::fmt::Debug;
 use std::fmt::Formatter;
+use std::sync::Arc;
 
 use bytes::Bytes;
+use chrono::DateTime;
+use chrono::Utc;
 use http::header;
-use http::request::Builder;
+use http::header::CONTENT_LENGTH;
+use http::header::CONTENT_TYPE;
 use http::Request;
 use http::Response;
 use serde::Deserialize;
 use serde::Serialize;
+use tokio::sync::Mutex;
 
 use crate::raw::build_rooted_abs_path;
+use crate::raw::new_json_deserialize_error;
 use crate::raw::new_json_serialize_error;
 use crate::raw::new_request_build_error;
 use crate::raw::AsyncBody;
@@ -36,7 +42,7 @@ use crate::raw::IncomingAsyncBody;
 use crate::types::Result;
 
 pub struct DropboxCore {
-    pub token: String,
+    pub signer: Arc<Mutex<DropboxSigner>>,
     pub client: HttpClient,
     pub root: String,
 }
@@ -63,12 +69,13 @@ impl DropboxCore {
         };
         let request_payload =
             
serde_json::to_string(&download_args).map_err(new_json_serialize_error)?;
-        let request = self
-            .build_auth_header(Request::post(&url))
+        let mut request = Request::post(&url)
             .header("Dropbox-API-Arg", request_payload)
-            .header(header::CONTENT_LENGTH, 0)
+            .header(CONTENT_LENGTH, 0)
             .body(AsyncBody::Empty)
             .map_err(new_request_build_error)?;
+
+        self.sign(&mut request).await?;
         self.client.send(request).await
     }
 
@@ -86,15 +93,14 @@ impl DropboxCore {
         };
         let mut request_builder = Request::post(&url);
         if let Some(size) = size {
-            request_builder = request_builder.header(header::CONTENT_LENGTH, 
size);
+            request_builder = request_builder.header(CONTENT_LENGTH, size);
         }
         request_builder = request_builder.header(
-            header::CONTENT_TYPE,
+            CONTENT_TYPE,
             content_type.unwrap_or("application/octet-stream"),
         );
 
-        let request = self
-            .build_auth_header(request_builder)
+        let mut request = request_builder
             .header(
                 "Dropbox-API-Arg",
                 
serde_json::to_string(&args).map_err(new_json_serialize_error)?,
@@ -102,6 +108,7 @@ impl DropboxCore {
             .body(body)
             .map_err(new_request_build_error)?;
 
+        self.sign(&mut request).await?;
         self.client.send(request).await
     }
 
@@ -113,12 +120,13 @@ impl DropboxCore {
 
         let bs = 
Bytes::from(serde_json::to_string(&args).map_err(new_json_serialize_error)?);
 
-        let request = self
-            .build_auth_header(Request::post(&url))
-            .header(header::CONTENT_TYPE, "application/json")
-            .header(header::CONTENT_LENGTH, bs.len())
+        let mut request = Request::post(&url)
+            .header(CONTENT_TYPE, "application/json")
+            .header(CONTENT_LENGTH, bs.len())
             .body(AsyncBody::Bytes(bs))
             .map_err(new_request_build_error)?;
+
+        self.sign(&mut request).await?;
         self.client.send(request).await
     }
 
@@ -130,12 +138,13 @@ impl DropboxCore {
 
         let bs = 
Bytes::from(serde_json::to_string(&args).map_err(new_json_serialize_error)?);
 
-        let request = self
-            .build_auth_header(Request::post(&url))
-            .header(header::CONTENT_TYPE, "application/json")
-            .header(header::CONTENT_LENGTH, bs.len())
+        let mut request = Request::post(&url)
+            .header(CONTENT_TYPE, "application/json")
+            .header(CONTENT_LENGTH, bs.len())
             .body(AsyncBody::Bytes(bs))
             .map_err(new_request_build_error)?;
+
+        self.sign(&mut request).await?;
         self.client.send(request).await
     }
 
@@ -148,19 +157,87 @@ impl DropboxCore {
 
         let bs = 
Bytes::from(serde_json::to_string(&args).map_err(new_json_serialize_error)?);
 
-        let request = self
-            .build_auth_header(Request::post(&url))
-            .header(header::CONTENT_TYPE, "application/json")
-            .header(header::CONTENT_LENGTH, bs.len())
+        let mut request = Request::post(&url)
+            .header(CONTENT_TYPE, "application/json")
+            .header(CONTENT_LENGTH, bs.len())
             .body(AsyncBody::Bytes(bs))
             .map_err(new_request_build_error)?;
+
+        self.sign(&mut request).await?;
+
         self.client.send(request).await
     }
 
-    fn build_auth_header(&self, mut req: Builder) -> Builder {
-        let auth_header_content = format!("Bearer {}", self.token);
-        req = req.header(header::AUTHORIZATION, auth_header_content);
-        req
+    pub async fn sign<T>(&self, req: &mut Request<T>) -> Result<()> {
+        let mut signer = self.signer.lock().await;
+
+        // Access token is valid, use it directly.
+        if !signer.access_token.is_empty() && signer.expires_in > Utc::now() {
+            let value = format!("Bearer {}", signer.access_token)
+                .parse()
+                .expect("token must be valid header");
+            req.headers_mut().insert(header::AUTHORIZATION, value);
+            return Ok(());
+        }
+
+        // Refresh invalid token.
+        let url = "https://api.dropboxapi.com/oauth2/token".to_string();
+
+        let content = format!(
+            
"grant_type=refresh_token&refresh_token={}&client_id={}&client_secret={}",
+            signer.refresh_token, signer.client_id, signer.client_secret
+        );
+        let bs = Bytes::from(content);
+
+        let request = Request::post(&url)
+            .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
+            .header(CONTENT_LENGTH, bs.len())
+            .body(AsyncBody::Bytes(bs))
+            .map_err(new_request_build_error)?;
+
+        let resp = self.client.send(request).await?;
+        let body = resp.into_body().bytes().await?;
+
+        let token: DropboxTokenResponse =
+            serde_json::from_slice(&body).map_err(new_json_deserialize_error)?;
+
+        // Update signer after token refreshed.
+
+        signer.access_token = token.access_token.clone();
+
+        // Refresh it 2 minutes earlier.
+        signer.expires_in = Utc::now() + 
chrono::Duration::seconds(token.expires_in as i64)
+            - chrono::Duration::seconds(120);
+
+        let value = format!("Bearer {}", token.access_token)
+            .parse()
+            .expect("token must be valid header");
+        req.headers_mut().insert(header::AUTHORIZATION, value);
+
+        Ok(())
+    }
+}
+
+#[derive(Clone)]
+pub struct DropboxSigner {
+    pub client_id: String,
+    pub client_secret: String,
+    pub refresh_token: String,
+
+    pub access_token: String,
+    pub expires_in: DateTime<Utc>,
+}
+
+impl Default for DropboxSigner {
+    fn default() -> Self {
+        DropboxSigner {
+            refresh_token: "".to_string(),
+            client_id: String::new(),
+            client_secret: String::new(),
+
+            access_token: "".to_string(),
+            expires_in: DateTime::<Utc>::MIN_UTC,
+        }
     }
 }
 
@@ -178,6 +255,18 @@ struct DropboxUploadArgs {
     strict_conflict: bool,
 }
 
+impl Default for DropboxUploadArgs {
+    fn default() -> Self {
+        DropboxUploadArgs {
+            mode: "overwrite".to_string(),
+            path: "".to_string(),
+            mute: true,
+            autorename: false,
+            strict_conflict: false,
+        }
+    }
+}
+
 #[derive(Clone, Debug, Deserialize, Serialize)]
 struct DropboxDeleteArgs {
     path: String,
@@ -196,18 +285,6 @@ struct DropboxMetadataArgs {
     path: String,
 }
 
-impl Default for DropboxUploadArgs {
-    fn default() -> Self {
-        DropboxUploadArgs {
-            mode: "overwrite".to_string(),
-            path: "".to_string(),
-            mute: true,
-            autorename: false,
-            strict_conflict: false,
-        }
-    }
-}
-
 impl Default for DropboxMetadataArgs {
     fn default() -> Self {
         DropboxMetadataArgs {
@@ -218,3 +295,9 @@ impl Default for DropboxMetadataArgs {
         }
     }
 }
+
+#[derive(Clone, Deserialize)]
+struct DropboxTokenResponse {
+    access_token: String,
+    expires_in: usize,
+}
diff --git a/core/src/services/dropbox/error.rs 
b/core/src/services/dropbox/error.rs
index 5e5831b15..57e3c177b 100644
--- a/core/src/services/dropbox/error.rs
+++ b/core/src/services/dropbox/error.rs
@@ -17,19 +17,26 @@
 
 use http::Response;
 use http::StatusCode;
+use http::Uri;
+use serde::Deserialize;
 
-use super::response::DropboxErrorResponse;
 use crate::raw::*;
 use crate::Error;
 use crate::ErrorKind;
 use crate::Result;
 
+#[derive(Default, Debug, Deserialize)]
+#[serde(default)]
+pub struct DropboxErrorResponse {
+    error_summary: String,
+}
+
 /// Parse error response into Error.
 pub async fn parse_error(resp: Response<IncomingAsyncBody>) -> Result<Error> {
     let (parts, body) = resp.into_parts();
     let bs = body.bytes().await?;
 
-    let (kind, retryable) = match parts.status {
+    let (mut kind, mut retryable) = match parts.status {
         StatusCode::NOT_FOUND => (ErrorKind::NotFound, false),
         StatusCode::FORBIDDEN => (ErrorKind::PermissionDenied, false),
         StatusCode::INTERNAL_SERVER_ERROR
@@ -39,53 +46,39 @@ pub async fn parse_error(resp: Response<IncomingAsyncBody>) 
-> Result<Error> {
         _ => (ErrorKind::Unexpected, false),
     };
 
-    let dropbox_error =
-        
serde_json::from_slice::<DropboxErrorResponse>(&bs).map_err(new_json_deserialize_error);
-    match dropbox_error {
-        Ok(dropbox_error) => {
-            // We cannot get the error type from the response header when the 
status code is 409.
-            // Because Dropbox API v2 will put error summary in the response 
body,
-            // we need to parse it to get the correct error type and then 
error kind.
-            // See 
https://www.dropbox.com/developers/documentation/http/documentation#error-handling
-            let error_summary = dropbox_error.error_summary.as_str();
+    let (message, dropbox_err) = 
serde_json::from_slice::<DropboxErrorResponse>(&bs)
+        .map(|dropbox_err| (format!("{dropbox_err:?}"), Some(dropbox_err)))
+        .unwrap_or_else(|_| (String::from_utf8_lossy(&bs).into_owned(), None));
 
-            let mut err = Error::new(
-                match parts.status {
-                    // 409 Conflict means that Endpoint-specific error.
-                    // Look to the JSON response body for the specifics of the 
error.
-                    StatusCode::CONFLICT => {
-                        if error_summary.contains("path/not_found")
-                            || error_summary.contains("path_lookup/not_found")
-                        {
-                            ErrorKind::NotFound
-                        } else if error_summary.contains("path/conflict") {
-                            ErrorKind::AlreadyExists
-                        } else {
-                            ErrorKind::Unexpected
-                        }
-                    }
-                    // Otherwise, we can get the error type from the response 
status code.
-                    _ => kind,
-                },
-                error_summary,
-            )
-            .with_context("response", format!("{parts:?}"));
+    if let Some(dropbox_err) = dropbox_err {
+        (kind, retryable) =
+            
parse_dropbox_error_summary(&dropbox_err.error_summary).unwrap_or((kind, 
retryable));
+    }
 
-            if retryable {
-                err = err.set_temporary();
-            }
+    let mut err = Error::new(kind, &message).with_context("response", 
format!("{parts:?}"));
 
-            Ok(err)
-        }
-        Err(_err) => {
-            let mut err = Error::new(kind, &String::from_utf8_lossy(&bs))
-                .with_context("response", format!("{parts:?}"));
+    if retryable {
+        err = err.set_temporary();
+    }
 
-            if retryable {
-                err = err.set_temporary();
-            }
+    if let Some(uri) = parts.extensions.get::<Uri>() {
+        err = err.with_context("uri", uri.to_string());
+    }
+
+    Ok(err)
+}
 
-            Ok(err)
-        }
+/// We cannot get the error type from the response header when the status code 
is 409.
+/// Because Dropbox API v2 will put error summary in the response body,
+/// we need to parse it to get the correct error type and then error kind.
+///
+/// See 
<https://www.dropbox.com/developers/documentation/http/documentation#error-handling>
+pub fn parse_dropbox_error_summary(summary: &str) -> Option<(ErrorKind, bool)> 
{
+    if summary.starts_with("path/not_found") || 
summary.starts_with("path_lookup/not_found") {
+        Some((ErrorKind::NotFound, false))
+    } else if summary.starts_with("path/conflict") {
+        Some((ErrorKind::AlreadyExists, false))
+    } else {
+        None
     }
 }
diff --git a/core/src/services/dropbox/mod.rs b/core/src/services/dropbox/mod.rs
index 048b7d74d..239974a75 100644
--- a/core/src/services/dropbox/mod.rs
+++ b/core/src/services/dropbox/mod.rs
@@ -19,7 +19,6 @@ mod backend;
 mod builder;
 mod core;
 mod error;
-mod response;
 mod writer;
 
 pub use builder::DropboxBuilder as Dropbox;
diff --git a/core/src/services/dropbox/response.rs 
b/core/src/services/dropbox/response.rs
deleted file mode 100644
index 2e5c15a81..000000000
--- a/core/src/services/dropbox/response.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use serde::Deserialize;
-
-#[derive(Default, Debug, Deserialize)]
-#[serde(default)]
-pub struct DropboxErrorResponse {
-    pub error_summary: String,
-    pub error: DropboxErrorDetail,
-}
-
-#[derive(Default, Debug, Deserialize)]
-#[serde(default)]
-pub struct DropboxErrorDetail {
-    #[serde(rename(deserialize = ".tag"))]
-    pub tag: String,
-}
-
-#[derive(Default, Debug, Deserialize)]
-#[serde(default)]
-pub struct DropboxMetadataResponse {
-    #[serde(rename(deserialize = ".tag"))]
-    pub tag: String,
-    pub client_modified: String,
-    pub content_hash: Option<String>,
-    pub file_lock_info: Option<DropboxMetadataFileLockInfo>,
-    pub has_explicit_shared_members: Option<bool>,
-    pub id: String,
-    pub is_downloadable: Option<bool>,
-    pub name: String,
-    pub path_display: String,
-    pub path_lower: String,
-    pub property_groups: Option<Vec<DropboxMetadataPropertyGroup>>,
-    pub rev: Option<String>,
-    pub server_modified: Option<String>,
-    pub sharing_info: Option<DropboxMetadataSharingInfo>,
-    pub size: Option<u64>,
-}
-
-#[derive(Default, Debug, Deserialize)]
-#[serde(default)]
-pub struct DropboxMetadataFileLockInfo {
-    pub created: Option<String>,
-    pub is_lockholder: bool,
-    pub lockholder_name: Option<String>,
-}
-
-#[derive(Default, Debug, Deserialize)]
-#[serde(default)]
-pub struct DropboxMetadataPropertyGroup {
-    pub fields: Vec<DropboxMetadataPropertyGroupField>,
-    pub template_id: String,
-}
-
-#[derive(Default, Debug, Deserialize)]
-#[serde(default)]
-pub struct DropboxMetadataPropertyGroupField {
-    pub name: String,
-    pub value: String,
-}
-
-#[derive(Default, Debug, Deserialize)]
-#[serde(default)]
-pub struct DropboxMetadataSharingInfo {
-    pub modified_by: Option<String>,
-    pub parent_shared_folder_id: Option<String>,
-    pub read_only: Option<bool>,
-    pub shared_folder_id: Option<String>,
-    pub traverse_only: Option<bool>,
-    pub no_access: Option<bool>,
-}

Reply via email to