1pub(crate) mod account;
4pub(crate) mod auth;
5pub(crate) mod dedupe;
6pub(crate) mod format;
7pub(crate) mod helpers;
8pub(crate) mod move_file;
11pub(crate) mod read;
12pub(crate) mod rename;
13pub(crate) mod search;
14
15use anyhow::Result;
16use clap::{Parser, Subcommand};
17
18use crate::drive::account::DRIVE_ACCOUNT_ENV;
19use crate::drive::client::DriveClient;
20
21#[derive(Parser)]
23pub struct DriveCommand {
24 #[arg(long, global = true, value_name = "NAME")]
36 pub account: Option<String>,
37 #[command(subcommand)]
39 pub command: DriveSubcommands,
40}
41
42#[derive(Subcommand)]
44pub enum DriveSubcommands {
45 Auth(auth::AuthCommand),
47 Account(account::AccountCommand),
49 Search(search::SearchCommand),
51 Read(read::ReadCommand),
53 Dedupe(dedupe::DedupeCommand),
55 Rename(rename::RenameCommand),
58 Move(move_file::MoveCommand),
61}
62
63impl DriveCommand {
64 pub async fn execute(self) -> Result<()> {
69 let _account_guard = self
78 .account
79 .as_ref()
80 .map(|account| crate::utils::env::ScopedEnvVar::set(DRIVE_ACCOUNT_ENV, account));
81
82 match self.command {
83 DriveSubcommands::Auth(cmd) => cmd.execute().await,
84 DriveSubcommands::Account(cmd) => cmd.execute(),
85 command => {
86 let client = helpers::create_client()?;
87 command.dispatch(&client).await
88 }
89 }
90 }
91}
92
93impl DriveSubcommands {
94 async fn dispatch(self, client: &DriveClient) -> Result<()> {
98 match self {
99 Self::Auth(_) => unreachable!("Auth is dispatched before client resolution"),
100 Self::Account(_) => unreachable!("Account is dispatched before client resolution"),
101 Self::Search(cmd) => cmd.execute(client).await,
102 Self::Read(cmd) => cmd.execute(client).await,
103 Self::Dedupe(cmd) => cmd.execute(client).await,
104 Self::Rename(cmd) => cmd.execute(client).await,
105 Self::Move(cmd) => cmd.execute(client).await,
106 }
107 }
108}
109
110#[cfg(test)]
111#[allow(clippy::unwrap_used)]
112mod tests {
113 use super::*;
114 use crate::cli::drive::format::OutputFormat;
115 use crate::drive::auth::{DriveCredentials, DriveScope};
116 use crate::utils::secret::Secret;
117
118 fn dead_credentials() -> DriveCredentials {
119 DriveCredentials {
120 client_id: "client".to_string(),
121 client_secret: Secret::new("secret"),
122 refresh_token: Secret::new("refresh"),
123 scope: DriveScope::ReadOnly,
124 }
125 }
126
127 fn dead_client() -> DriveClient {
128 DriveClient::new("http://127.0.0.1:1", &dead_credentials()).unwrap()
129 }
130
131 #[tokio::test]
132 async fn execute_routes_auth_subcommand_and_surfaces_missing_credentials() {
133 let guard = crate::drive::test_support::EnvGuard::take();
134 let _dir = guard.clear_credentials();
135
136 let cmd = DriveCommand {
137 account: None,
138 command: DriveSubcommands::Auth(auth::AuthCommand {
139 command: auth::AuthSubcommands::Status(auth::StatusCommand { all: false }),
140 }),
141 };
142 let err = cmd.execute().await.unwrap_err();
143 assert!(err.to_string().contains("not configured"));
144 }
145
146 #[tokio::test]
147 async fn execute_non_auth_subcommand_errors_when_credentials_missing() {
148 let guard = crate::drive::test_support::EnvGuard::take();
149 let _dir = guard.clear_credentials();
150
151 let cmd = DriveCommand {
152 account: None,
153 command: DriveSubcommands::Search(search::SearchCommand {
154 query: "name contains 'report'".to_string(),
155 limit: 10,
156 output: OutputFormat::Table,
157 }),
158 };
159 let err = cmd.execute().await.unwrap_err();
160 assert!(err.to_string().contains("not configured"));
161 }
162
163 #[tokio::test]
164 async fn execute_restores_account_env_var_after_return() {
165 let guard = crate::drive::test_support::EnvGuard::take();
166 let _dir = guard.clear_credentials();
167
168 let cmd = DriveCommand {
169 account: Some("work".to_string()),
170 command: DriveSubcommands::Account(account::AccountCommand {
171 command: account::AccountSubcommands::List(account::list::ListCommand {
172 output: OutputFormat::Table,
173 }),
174 }),
175 };
176 cmd.execute().await.unwrap();
177 assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
178 }
179
180 #[tokio::test]
181 async fn execute_restores_previous_account_env_var_after_return() {
182 let guard = crate::drive::test_support::EnvGuard::take();
183 let _dir = guard.clear_credentials();
184 std::env::set_var(DRIVE_ACCOUNT_ENV, "personal");
185
186 let cmd = DriveCommand {
187 account: Some("work".to_string()),
188 command: DriveSubcommands::Account(account::AccountCommand {
189 command: account::AccountSubcommands::List(account::list::ListCommand {
190 output: OutputFormat::Table,
191 }),
192 }),
193 };
194 cmd.execute().await.unwrap();
195 assert_eq!(
196 std::env::var(DRIVE_ACCOUNT_ENV).ok().as_deref(),
197 Some("personal")
198 );
199 }
200
201 #[tokio::test]
202 async fn execute_does_not_leak_account_across_sequential_calls() {
203 let guard = crate::drive::test_support::EnvGuard::take();
204 let _dir = guard.clear_credentials();
205
206 let account_list_cmd = || DriveCommand {
207 account: None,
208 command: DriveSubcommands::Account(account::AccountCommand {
209 command: account::AccountSubcommands::List(account::list::ListCommand {
210 output: OutputFormat::Table,
211 }),
212 }),
213 };
214
215 let first = DriveCommand {
216 account: Some("alpha".to_string()),
217 ..account_list_cmd()
218 };
219 first.execute().await.unwrap();
220 assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
221
222 account_list_cmd().execute().await.unwrap();
225 assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
226 }
227
228 #[tokio::test]
229 async fn execute_absent_account_leaves_ambient_env_var_untouched() {
230 let guard = crate::drive::test_support::EnvGuard::take();
231 let _dir = guard.clear_credentials();
232 std::env::set_var(DRIVE_ACCOUNT_ENV, "personal");
233
234 let cmd = DriveCommand {
235 account: None,
236 command: DriveSubcommands::Account(account::AccountCommand {
237 command: account::AccountSubcommands::List(account::list::ListCommand {
238 output: OutputFormat::Table,
239 }),
240 }),
241 };
242 cmd.execute().await.unwrap();
243 assert_eq!(
244 std::env::var(DRIVE_ACCOUNT_ENV).ok().as_deref(),
245 Some("personal")
246 );
247 }
248
249 #[tokio::test]
250 async fn execute_routes_account_list_without_client_resolution() {
251 let guard = crate::drive::test_support::EnvGuard::take();
252 let _dir = guard.clear_credentials();
253
254 let cmd = DriveCommand {
255 account: None,
256 command: DriveSubcommands::Account(account::AccountCommand {
257 command: account::AccountSubcommands::List(account::list::ListCommand {
258 output: OutputFormat::Table,
259 }),
260 }),
261 };
262 cmd.execute().await.unwrap();
263 }
264
265 #[tokio::test]
266 async fn dispatch_routes_search() {
267 let cmd = DriveSubcommands::Search(search::SearchCommand {
268 query: "name contains 'x'".to_string(),
269 limit: 10,
270 output: OutputFormat::Table,
271 });
272 assert!(cmd.dispatch(&dead_client()).await.is_err());
273 }
274
275 #[tokio::test]
276 async fn dispatch_routes_read() {
277 let cmd = DriveSubcommands::Read(read::ReadCommand {
278 file_id: "f1".to_string(),
279 content: false,
280 export_mime_type: None,
281 out_file: None,
282 verify: false,
283 output: OutputFormat::Table,
284 });
285 assert!(cmd.dispatch(&dead_client()).await.is_err());
286 }
287
288 #[tokio::test]
289 async fn dispatch_routes_dedupe() {
290 let cmd = DriveSubcommands::Dedupe(dedupe::DedupeCommand {
291 query: "name contains 'x'".to_string(),
292 limit: 10,
293 output: OutputFormat::Table,
294 });
295 assert!(cmd.dispatch(&dead_client()).await.is_err());
296 }
297
298 #[tokio::test]
299 async fn dispatch_routes_rename() {
300 let cmd = DriveSubcommands::Rename(rename::RenameCommand {
301 file_id: "f1".to_string(),
302 new_name: "New Name".to_string(),
303 dry_run: false,
304 output: OutputFormat::Table,
305 });
306 assert!(cmd.dispatch(&dead_client()).await.is_err());
307 }
308
309 #[tokio::test]
310 async fn dispatch_routes_move() {
311 let cmd = DriveSubcommands::Move(move_file::MoveCommand {
312 file_ids: vec!["f1".to_string()],
313 to: "dest1".to_string(),
314 allow_visibility_increase: false,
315 allow_visibility_decrease: false,
316 allow_drive_boundary_crossing: false,
317 dry_run: false,
318 output: OutputFormat::Table,
319 });
320 assert!(cmd.dispatch(&dead_client()).await.is_err());
321 }
322}