use std::path::Path;
use google_drive3::DriveHub;
use hyper_rustls::HttpsConnectorBuilder;
use hyper_util::client::legacy::connect::HttpConnector;
use yup_oauth2::{
ApplicationSecret, InstalledFlowAuthenticator, InstalledFlowReturnMethod,
ServiceAccountAuthenticator,
};
use crate::config::AppConfig;
use crate::error::{GDriveError, Result};
pub type HyperClient = google_drive3::common::Client<hyper_rustls::HttpsConnector<HttpConnector>>;
pub type Authenticator = yup_oauth2::authenticator::Authenticator<hyper_rustls::HttpsConnector<HttpConnector>>;
pub type DriveHubType = DriveHub<hyper_rustls::HttpsConnector<HttpConnector>>;
#[allow(dead_code)]
const SCOPES: &[&str] = &[
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/drive.metadata.readonly",
];
pub async fn build_drive_hub(config: &AppConfig, eager_token: bool) -> Result<DriveHubType> {
let auth = build_authenticator(config).await?;
if eager_token {
tracing::info!("Acquiring OAuth2 token (browser may open on first run)...");
let _token = auth
.token(SCOPES)
.await
.map_err(|e| GDriveError::OAuth2(format!("Failed to acquire token: {e}")))?;
tracing::info!("OAuth2 token acquired successfully.");
} else {
tracing::info!("Google OAuth2 token will be acquired on first API call.");
}
let connector = HttpsConnectorBuilder::new()
.with_native_roots()
.map_err(|e| GDriveError::Other(format!("Failed to build HTTPS connector: {e}")))?
.https_or_http()
.enable_http2()
.build();
let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
.build(connector);
Ok(DriveHub::new(client, auth))
}
async fn build_authenticator(config: &AppConfig) -> Result<Authenticator> {
let creds_path = &config.credentials_file;
let creds_content = tokio::fs::read_to_string(creds_path)
.await
.map_err(|e| GDriveError::OAuth2(format!("Cannot read credentials file '{creds_path}': {e}")))?;
if creds_content.contains("\"type\": \"service_account\"")
|| creds_content.contains("\"type\":\"service_account\"")
{
return build_service_account_auth(creds_path).await;
}
build_installed_flow_auth(&creds_content, config).await
}
async fn build_service_account_auth(creds_path: &str) -> Result<Authenticator> {
let sa_key = yup_oauth2::read_service_account_key(creds_path)
.await
.map_err(|e| GDriveError::OAuth2(format!("Invalid service account key: {e}")))?;
ServiceAccountAuthenticator::builder(sa_key)
.build()
.await
.map_err(|e| GDriveError::OAuth2(format!("Failed to build service account authenticator: {e}")))
}
async fn build_installed_flow_auth(creds_content: &str, config: &AppConfig) -> Result<Authenticator> {
let secret: ApplicationSecret = parse_application_secret(creds_content)?;
let token_cache = config.resolved_token_cache_path();
let mut builder = InstalledFlowAuthenticator::builder(secret, InstalledFlowReturnMethod::HTTPRedirect);
if Path::new(&token_cache).exists() || !token_cache.is_empty() {
builder = builder.persist_tokens_to_disk(&token_cache);
}
builder
.build()
.await
.map_err(|e| GDriveError::OAuth2(format!("Failed to build installed flow authenticator: {e}")))
}
pub fn build_shared_hyper_client() -> Result<HyperClient> {
let connector = HttpsConnectorBuilder::new()
.with_native_roots()
.map_err(|e| GDriveError::Other(format!("Failed to build HTTPS connector: {e}")))?
.https_or_http()
.enable_http2()
.build();
Ok(
hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
.build(connector),
)
}
fn parse_application_secret(content: &str) -> Result<ApplicationSecret> {
let json: serde_json::Value =
serde_json::from_str(content).map_err(|e| GDriveError::OAuth2(format!("Invalid JSON: {e}")))?;
let inner = json
.get("installed")
.or_else(|| json.get("web"))
.ok_or_else(|| {
GDriveError::OAuth2(
"Credentials file must contain an 'installed' or 'web' key".to_string(),
)
})?;
serde_json::from_value(inner.clone())
.map_err(|e| GDriveError::OAuth2(format!("Failed to parse application secret: {e}")))
}