Fix Dropbox OAuth configuration

- Fix 'access_token and refresh_token can not be set at the same time' error
- Update Dropbox to use only refresh_token (OpenDAL requirement)
- Add client_id/client_secret to OAuth credential storage
- Add validation for empty OAuth credentials
- Update CLI and volume manager for enhanced OAuth support
This commit is contained in:
Sinan Gencoglu
2026-01-03 23:13:58 +01:00
parent e4d5520e40
commit 68d6b36f4a
7 changed files with 4523 additions and 21 deletions

View File

@@ -238,8 +238,7 @@ async fn add_dropbox_interactive(ctx: &Context) -> Result<()> {
let client_id = text("App Key (Client ID)", false)?.unwrap();
let client_secret = password("App Secret (Client Secret)", false)?.unwrap();
println!("\nAfter authorizing, you'll receive tokens:");
let access_token = password("Access Token", false)?.unwrap();
println!("\nAfter completing OAuth flow, you'll receive a refresh token:");
let refresh_token = password("Refresh Token", false)?.unwrap();
println!("\nSummary:");
@@ -257,7 +256,6 @@ async fn add_dropbox_interactive(ctx: &Context) -> Result<()> {
display_name: name.clone(),
config: CloudStorageConfig::Dropbox {
root,
access_token,
refresh_token,
client_id,
client_secret,

View File

@@ -177,9 +177,6 @@ impl VolumeAddCloudArgs {
}
}
CloudServiceArg::Dropbox => {
let access_token = self
.access_token
.ok_or("--access-token is required for Dropbox")?;
let refresh_token = self
.refresh_token
.ok_or("--refresh-token is required for Dropbox")?;
@@ -192,7 +189,6 @@ impl VolumeAddCloudArgs {
CloudStorageConfig::Dropbox {
root: self.root,
access_token,
refresh_token,
client_id,
client_secret,

View File

File diff suppressed because it is too large Load Diff

View File

@@ -276,6 +276,8 @@ pub enum CredentialData {
OAuth {
access_token: String,
refresh_token: String,
client_id: String,
client_secret: String,
},
/// Simple API key
@@ -310,6 +312,8 @@ impl CloudCredential {
service: crate::volume::CloudServiceType,
access_token: String,
refresh_token: String,
client_id: String,
client_secret: String,
expires_at: Option<chrono::DateTime<chrono::Utc>>,
) -> Self {
Self {
@@ -317,6 +321,8 @@ impl CloudCredential {
data: CredentialData::OAuth {
access_token,
refresh_token,
client_id,
client_secret,
},
created_at: chrono::Utc::now(),
expires_at,
@@ -426,6 +432,8 @@ mod tests {
crate::volume::CloudServiceType::GoogleDrive,
"access_token".to_string(),
"refresh_token".to_string(),
"client_id".to_string(),
"client_secret".to_string(),
Some(future),
);
@@ -433,6 +441,8 @@ mod tests {
crate::volume::CloudServiceType::GoogleDrive,
"access_token".to_string(),
"refresh_token".to_string(),
"client_id".to_string(),
"client_secret".to_string(),
Some(past),
);

View File

@@ -32,6 +32,8 @@ pub enum CloudStorageConfig {
secret_access_key: String,
endpoint: Option<String>,
},
/// Google Drive with OAuth 2.0 credentials.
/// Requires both access_token and refresh_token for automatic token renewal.
GoogleDrive {
root: Option<String>,
access_token: String,
@@ -39,6 +41,8 @@ pub enum CloudStorageConfig {
client_id: String,
client_secret: String,
},
/// OneDrive with OAuth 2.0 credentials.
/// Requires both access_token and refresh_token for automatic token renewal.
OneDrive {
root: Option<String>,
access_token: String,
@@ -46,9 +50,11 @@ pub enum CloudStorageConfig {
client_id: String,
client_secret: String,
},
/// Dropbox with OAuth 2.0 refresh token for long-term access.
/// OpenDAL automatically obtains and refreshes access tokens as needed.
/// Only refresh_token is required (not access_token).
Dropbox {
root: Option<String>,
access_token: String,
refresh_token: String,
client_id: String,
client_secret: String,
@@ -148,6 +154,28 @@ impl LibraryAction for VolumeAddCloudAction {
client_id,
client_secret,
} => {
// Validate required OAuth credentials for Google Drive
if access_token.trim().is_empty() {
return Err(ActionError::InvalidInput(
"Google Drive requires a valid access_token".to_string(),
));
}
if refresh_token.trim().is_empty() {
return Err(ActionError::InvalidInput(
"Google Drive requires a valid refresh_token".to_string(),
));
}
if client_id.trim().is_empty() {
return Err(ActionError::InvalidInput(
"Google Drive requires a valid client_id".to_string(),
));
}
if client_secret.trim().is_empty() {
return Err(ActionError::InvalidInput(
"Google Drive requires a valid client_secret".to_string(),
));
}
let backend = CloudBackend::new_google_drive(
access_token,
refresh_token,
@@ -167,6 +195,8 @@ impl LibraryAction for VolumeAddCloudAction {
CloudServiceType::GoogleDrive,
access_token.clone(),
refresh_token.clone(),
client_id.clone(),
client_secret.clone(),
None, // Google Drive tokens typically don't have a fixed expiry in the refresh flow
);
@@ -190,6 +220,28 @@ impl LibraryAction for VolumeAddCloudAction {
client_id,
client_secret,
} => {
// Validate required OAuth credentials for OneDrive
if access_token.trim().is_empty() {
return Err(ActionError::InvalidInput(
"OneDrive requires a valid access_token".to_string(),
));
}
if refresh_token.trim().is_empty() {
return Err(ActionError::InvalidInput(
"OneDrive requires a valid refresh_token".to_string(),
));
}
if client_id.trim().is_empty() {
return Err(ActionError::InvalidInput(
"OneDrive requires a valid client_id".to_string(),
));
}
if client_secret.trim().is_empty() {
return Err(ActionError::InvalidInput(
"OneDrive requires a valid client_secret".to_string(),
));
}
let backend = CloudBackend::new_onedrive(
access_token,
refresh_token,
@@ -206,6 +258,8 @@ impl LibraryAction for VolumeAddCloudAction {
CloudServiceType::OneDrive,
access_token.clone(),
refresh_token.clone(),
client_id.clone(),
client_secret.clone(),
None,
);
@@ -224,13 +278,28 @@ impl LibraryAction for VolumeAddCloudAction {
}
CloudStorageConfig::Dropbox {
root,
access_token,
refresh_token,
client_id,
client_secret,
} => {
// Validate required OAuth credentials for Dropbox
if refresh_token.trim().is_empty() {
return Err(ActionError::InvalidInput(
"Dropbox requires a valid refresh_token".to_string(),
));
}
if client_id.trim().is_empty() {
return Err(ActionError::InvalidInput(
"Dropbox requires a valid client_id".to_string(),
));
}
if client_secret.trim().is_empty() {
return Err(ActionError::InvalidInput(
"Dropbox requires a valid client_secret".to_string(),
));
}
let backend = CloudBackend::new_dropbox(
access_token,
refresh_token,
client_id,
client_secret,
@@ -243,8 +312,10 @@ impl LibraryAction for VolumeAddCloudAction {
let credential = CloudCredential::new_oauth(
CloudServiceType::Dropbox,
access_token.clone(),
"".to_string(),
refresh_token.clone(),
client_id.clone(),
client_secret.clone(),
None,
);

View File

@@ -136,15 +136,17 @@ impl CloudBackend {
}
/// Create a new cloud backend for Dropbox
///
/// Uses OAuth 2.0 refresh token for long-term access. OpenDAL automatically
/// refreshes the access token when it expires, ensuring continuous operation
/// without manual intervention.
pub async fn new_dropbox(
access_token: impl AsRef<str>,
refresh_token: impl AsRef<str>,
client_id: impl AsRef<str>,
client_secret: impl AsRef<str>,
root: Option<String>,
) -> Result<Self, VolumeError> {
let mut builder = opendal::services::Dropbox::default()
.access_token(access_token.as_ref())
.refresh_token(refresh_token.as_ref())
.client_id(client_id.as_ref())
.client_secret(client_secret.as_ref());

View File

@@ -238,13 +238,15 @@ impl VolumeManager {
if let crate::crypto::cloud_credentials::CredentialData::OAuth {
access_token,
refresh_token,
client_id,
client_secret,
} = &credential.data
{
crate::volume::CloudBackend::new_google_drive(
access_token,
refresh_token,
"", // client_id not stored yet
"", // client_secret not stored yet
client_id,
client_secret,
Some(cloud_identifier.clone()),
).await
} else {
@@ -256,13 +258,15 @@ impl VolumeManager {
if let crate::crypto::cloud_credentials::CredentialData::OAuth {
access_token,
refresh_token,
client_id,
client_secret,
} = &credential.data
{
crate::volume::CloudBackend::new_onedrive(
access_token,
refresh_token,
"",
"",
client_id,
client_secret,
Some(cloud_identifier.clone()),
).await
} else {
@@ -272,15 +276,16 @@ impl VolumeManager {
}
crate::volume::CloudServiceType::Dropbox => {
if let crate::crypto::cloud_credentials::CredentialData::OAuth {
access_token,
refresh_token,
client_id,
client_secret,
..
} = &credential.data
{
crate::volume::CloudBackend::new_dropbox(
access_token,
refresh_token,
"",
"",
client_id,
client_secret,
Some(cloud_identifier.clone()),
).await
} else {