pub(crate) mod account;
pub(crate) mod auth;
pub(crate) mod dedupe;
pub(crate) mod format;
pub(crate) mod helpers;
pub(crate) mod move_file;
pub(crate) mod read;
pub(crate) mod rename;
pub(crate) mod search;
use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::drive::account::DRIVE_ACCOUNT_ENV;
use crate::drive::client::DriveClient;
#[derive(Parser)]
pub struct DriveCommand {
#[arg(long, global = true, value_name = "NAME")]
pub account: Option<String>,
#[command(subcommand)]
pub command: DriveSubcommands,
}
#[derive(Subcommand)]
pub enum DriveSubcommands {
Auth(auth::AuthCommand),
Account(account::AccountCommand),
Search(search::SearchCommand),
Read(read::ReadCommand),
Dedupe(dedupe::DedupeCommand),
Rename(rename::RenameCommand),
Move(move_file::MoveCommand),
}
impl DriveCommand {
pub async fn execute(self) -> Result<()> {
let _account_guard = self
.account
.as_ref()
.map(|account| crate::utils::env::ScopedEnvVar::set(DRIVE_ACCOUNT_ENV, account));
match self.command {
DriveSubcommands::Auth(cmd) => cmd.execute().await,
DriveSubcommands::Account(cmd) => cmd.execute(),
command => {
let client = helpers::create_client()?;
command.dispatch(&client).await
}
}
}
}
impl DriveSubcommands {
async fn dispatch(self, client: &DriveClient) -> Result<()> {
match self {
Self::Auth(_) => unreachable!("Auth is dispatched before client resolution"),
Self::Account(_) => unreachable!("Account is dispatched before client resolution"),
Self::Search(cmd) => cmd.execute(client).await,
Self::Read(cmd) => cmd.execute(client).await,
Self::Dedupe(cmd) => cmd.execute(client).await,
Self::Rename(cmd) => cmd.execute(client).await,
Self::Move(cmd) => cmd.execute(client).await,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::cli::drive::format::OutputFormat;
use crate::drive::auth::{DriveCredentials, DriveScope};
use crate::utils::secret::Secret;
fn dead_credentials() -> DriveCredentials {
DriveCredentials {
client_id: "client".to_string(),
client_secret: Secret::new("secret"),
refresh_token: Secret::new("refresh"),
scope: DriveScope::ReadOnly,
}
}
fn dead_client() -> DriveClient {
DriveClient::new("http://127.0.0.1:1", &dead_credentials()).unwrap()
}
#[tokio::test]
async fn execute_routes_auth_subcommand_and_surfaces_missing_credentials() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveCommand {
account: None,
command: DriveSubcommands::Auth(auth::AuthCommand {
command: auth::AuthSubcommands::Status(auth::StatusCommand { all: false }),
}),
};
let err = cmd.execute().await.unwrap_err();
assert!(err.to_string().contains("not configured"));
}
#[tokio::test]
async fn execute_non_auth_subcommand_errors_when_credentials_missing() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveCommand {
account: None,
command: DriveSubcommands::Search(search::SearchCommand {
query: "name contains 'report'".to_string(),
limit: 10,
output: OutputFormat::Table,
}),
};
let err = cmd.execute().await.unwrap_err();
assert!(err.to_string().contains("not configured"));
}
#[tokio::test]
async fn execute_restores_account_env_var_after_return() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveCommand {
account: Some("work".to_string()),
command: DriveSubcommands::Account(account::AccountCommand {
command: account::AccountSubcommands::List(account::list::ListCommand {
output: OutputFormat::Table,
}),
}),
};
cmd.execute().await.unwrap();
assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
}
#[tokio::test]
async fn execute_restores_previous_account_env_var_after_return() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
std::env::set_var(DRIVE_ACCOUNT_ENV, "personal");
let cmd = DriveCommand {
account: Some("work".to_string()),
command: DriveSubcommands::Account(account::AccountCommand {
command: account::AccountSubcommands::List(account::list::ListCommand {
output: OutputFormat::Table,
}),
}),
};
cmd.execute().await.unwrap();
assert_eq!(
std::env::var(DRIVE_ACCOUNT_ENV).ok().as_deref(),
Some("personal")
);
}
#[tokio::test]
async fn execute_does_not_leak_account_across_sequential_calls() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let account_list_cmd = || DriveCommand {
account: None,
command: DriveSubcommands::Account(account::AccountCommand {
command: account::AccountSubcommands::List(account::list::ListCommand {
output: OutputFormat::Table,
}),
}),
};
let first = DriveCommand {
account: Some("alpha".to_string()),
..account_list_cmd()
};
first.execute().await.unwrap();
assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
account_list_cmd().execute().await.unwrap();
assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
}
#[tokio::test]
async fn execute_absent_account_leaves_ambient_env_var_untouched() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
std::env::set_var(DRIVE_ACCOUNT_ENV, "personal");
let cmd = DriveCommand {
account: None,
command: DriveSubcommands::Account(account::AccountCommand {
command: account::AccountSubcommands::List(account::list::ListCommand {
output: OutputFormat::Table,
}),
}),
};
cmd.execute().await.unwrap();
assert_eq!(
std::env::var(DRIVE_ACCOUNT_ENV).ok().as_deref(),
Some("personal")
);
}
#[tokio::test]
async fn execute_routes_account_list_without_client_resolution() {
let guard = crate::drive::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let cmd = DriveCommand {
account: None,
command: DriveSubcommands::Account(account::AccountCommand {
command: account::AccountSubcommands::List(account::list::ListCommand {
output: OutputFormat::Table,
}),
}),
};
cmd.execute().await.unwrap();
}
#[tokio::test]
async fn dispatch_routes_search() {
let cmd = DriveSubcommands::Search(search::SearchCommand {
query: "name contains 'x'".to_string(),
limit: 10,
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_read() {
let cmd = DriveSubcommands::Read(read::ReadCommand {
file_id: "f1".to_string(),
content: false,
export_mime_type: None,
out_file: None,
verify: false,
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_dedupe() {
let cmd = DriveSubcommands::Dedupe(dedupe::DedupeCommand {
query: "name contains 'x'".to_string(),
limit: 10,
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_rename() {
let cmd = DriveSubcommands::Rename(rename::RenameCommand {
file_id: "f1".to_string(),
new_name: "New Name".to_string(),
dry_run: false,
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
#[tokio::test]
async fn dispatch_routes_move() {
let cmd = DriveSubcommands::Move(move_file::MoveCommand {
file_ids: vec!["f1".to_string()],
to: "dest1".to_string(),
allow_visibility_increase: false,
allow_visibility_decrease: false,
allow_drive_boundary_crossing: false,
dry_run: false,
output: OutputFormat::Table,
});
assert!(cmd.dispatch(&dead_client()).await.is_err());
}
}