use crate::traits::{StorageBackend, StorageError, StorageItem};
use crate::providers::OAuthCredentials;
use crate::providers::utils::translate_http_error;
use async_trait::async_trait;
use std::path::{Path, PathBuf};
use tracing::info;
pub struct OneDriveProvider {
client: reqwest::Client,
credentials: OAuthCredentials,
api_url: String,
token_manager: std::sync::Arc<super::utils::OAuthTokenManager>,
upload_limiter: Option<crate::rate_limit::TokenBucket>,
download_limiter: Option<crate::rate_limit::TokenBucket>,
}
crate::impl_provider_builder!(OneDriveProvider, OneDriveProviderBuilder, OAuthCredentials, relative);
crate::impl_oauth_token_helper!(OneDriveProvider);
impl OneDriveProvider {
pub fn with_client_options(
credentials: OAuthCredentials,
timeout: Option<std::time::Duration>,
custom_headers: Option<reqwest::header::HeaderMap>,
) -> Self {
let client = super::utils::build_http_client(timeout, custom_headers);
let auth_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token".to_string();
let token_manager = std::sync::Arc::new(super::utils::OAuthTokenManager::new(
client.clone(),
&auth_url,
&credentials.client_id,
&credentials.client_secret,
&credentials.refresh_token,
"OneDrive",
));
Self {
client,
credentials,
api_url: "https://graph.microsoft.com/v1.0".to_string(),
token_manager,
upload_limiter: None,
download_limiter: None,
}
}
pub fn with_limiters(
mut self,
upload_limiter: Option<crate::rate_limit::TokenBucket>,
download_limiter: Option<crate::rate_limit::TokenBucket>,
) -> Self {
if self.upload_limiter.is_none() {
self.upload_limiter = upload_limiter;
}
if self.download_limiter.is_none() {
self.download_limiter = download_limiter;
}
self
}
#[cfg(test)]
pub fn with_endpoints(mut self, auth_url: String, api_url: String) -> Self {
self.api_url = api_url;
self.token_manager = std::sync::Arc::new(super::utils::OAuthTokenManager::new(
self.client.clone(),
&auth_url,
&self.credentials.client_id,
&self.credentials.client_secret,
&self.credentials.refresh_token,
"OneDrive",
));
self
}
}
#[async_trait]
impl StorageBackend for OneDriveProvider {
fn name(&self) -> &str {
"OneDrive"
}
async fn upload(&self, local_path: &Path, remote_path: &str) -> Result<(), StorageError> {
super::utils::execute_with_retry(self.name(), "upload", || async {
let token = self.get_access_token().await?;
let clean_path = self.format_path(remote_path);
info!("[{}] Real upload starting for '{}'", self.name(), clean_path);
let (body, size) = super::utils::get_upload_body(local_path, self.upload_limiter.clone()).await?;
let upload_url = format!("{}/me/drive/root:/{}:/content", self.api_url, clean_path);
let res = super::utils::apply_bearer_auth(self.client.put(&upload_url), &token)
.header("Content-Type", "application/octet-stream")
.header("Content-Length", size.to_string())
.body(body)
.send()
.await?;
if !res.status().is_success() {
return Err(translate_http_error(res, self.name(), "upload").await);
}
Ok(())
}).await
}
async fn download(&self, remote_path: &str, local_path: &Path) -> Result<(), StorageError> {
super::utils::execute_with_retry(self.name(), "download", || async {
let token = self.get_access_token().await?;
let clean_path = self.format_path(remote_path);
let download_url = format!("{}/me/drive/root:/{}:/content", self.api_url, clean_path);
let res = super::utils::apply_bearer_auth(self.client.get(&download_url), &token)
.send()
.await?;
if !res.status().is_success() {
return Err(translate_http_error(res, self.name(), "download").await);
}
super::utils::download_rate_limited(res, local_path, self.download_limiter.clone()).await?;
Ok(())
}).await
}
async fn delete(&self, remote_path: &str) -> Result<(), StorageError> {
super::utils::execute_with_retry(self.name(), "delete", || async {
let token = self.get_access_token().await?;
let clean_path = self.format_path(remote_path);
let delete_url = format!("{}/me/drive/root:/{}", self.api_url, clean_path);
let res = super::utils::apply_bearer_auth(self.client.delete(&delete_url), &token)
.send()
.await?;
if !res.status().is_success() {
return Err(translate_http_error(res, self.name(), "delete").await);
}
Ok(())
}).await
}
async fn create_folder(&self, remote_path: &str) -> Result<(), StorageError> {
super::utils::execute_with_retry(self.name(), "create_folder", || async {
let token = self.get_access_token().await?;
let clean_path = self.format_path(remote_path);
if clean_path.is_empty() {
return Ok(());
}
let (parent_path, folder_name) = match clean_path.rfind('/') {
Some(idx) => (&clean_path[..idx], &clean_path[idx+1..]),
None => ("", clean_path.as_ref()),
};
let create_url = if parent_path.is_empty() {
format!("{}/me/drive/root/children", self.api_url)
} else {
format!("{}/me/drive/root:/{}:/children", self.api_url, parent_path)
};
let body = serde_json::json!({
"name": folder_name,
"folder": {},
"@microsoft.graph.conflictBehavior": "fail"
});
let res = super::utils::apply_bearer_auth(self.client.post(&create_url), &token)
.json(&body)
.send()
.await?;
let status = res.status();
if !status.is_success() {
let err_text = res.text().await.unwrap_or_default();
if err_text.contains("nameAlreadyExists") || err_text.contains("itemAlreadyExists") {
return Ok(());
}
return Err(StorageError::Provider {
message: format!("Failed to create folder: {}", err_text),
status: Some(status.as_u16()),
});
}
Ok(())
}).await
}
async fn list(&self, remote_path: &str) -> Result<Vec<StorageItem>, StorageError> {
super::utils::execute_with_retry(self.name(), "list", || async {
let token = self.get_access_token().await?;
let clean_path = self.format_path(remote_path);
let mut list_url = if clean_path.is_empty() {
format!("{}/me/drive/root/children", self.api_url)
} else {
format!("{}/me/drive/root:/{}:/children", self.api_url, clean_path)
};
let mut items = Vec::new();
loop {
let res = super::utils::apply_bearer_auth(self.client.get(&list_url), &token)
.send()
.await?
.json::<serde_json::Value>()
.await?;
if let Some(values) = res["value"].as_array() {
for item in values {
let name = item["name"].as_str().unwrap_or("").to_string();
let size = item["size"].as_u64().unwrap_or(0);
let is_dir = item.get("folder").is_some();
let rel_path = if remote_path.is_empty() {
name
} else {
format!("{}/{}", remote_path, name)
};
items.push(StorageItem {
path: PathBuf::from(rel_path),
size,
modified: std::time::SystemTime::now(),
is_dir,
checksum: None,
permissions: None,
});
}
}
if let Some(next_link) = res["@odata.nextLink"].as_str() {
list_url = next_link.to_string();
} else {
break;
}
}
Ok(items)
}).await
}
}
pub struct OneDriveProviderBuilder {
pub credentials: OAuthCredentials,
pub timeout: Option<std::time::Duration>,
pub custom_headers: Option<reqwest::header::HeaderMap>,
}
impl OneDriveProviderBuilder {
pub fn new(credentials: OAuthCredentials) -> Self {
Self {
credentials,
timeout: None,
custom_headers: None,
}
}
pub fn build(self) -> OneDriveProvider {
OneDriveProvider::with_client_options(self.credentials, self.timeout, self.custom_headers)
}
}