1pub(crate) mod account;
4pub(crate) mod auth;
5pub(crate) mod create;
6pub(crate) mod dedupe;
7pub(crate) mod edit;
8pub(crate) mod format;
9pub(crate) mod helpers;
10pub(crate) mod move_file;
13pub(crate) mod permissions;
14pub(crate) mod read;
15pub(crate) mod rename;
16pub(crate) mod search;
17pub(crate) mod upload;
18
19use anyhow::Result;
20use clap::{Parser, Subcommand};
21
22use crate::drive::account::DRIVE_ACCOUNT_ENV;
23use crate::drive::client::DriveClient;
24
25#[derive(Parser)]
27pub struct DriveCommand {
28 #[arg(long, global = true, value_name = "NAME")]
40 pub account: Option<String>,
41 #[command(subcommand)]
43 pub command: DriveSubcommands,
44}
45
46#[derive(Subcommand)]
48pub enum DriveSubcommands {
49 Auth(auth::AuthCommand),
51 Account(account::AccountCommand),
53 Search(search::SearchCommand),
55 Read(read::ReadCommand),
57 Dedupe(dedupe::DedupeCommand),
59 Create(create::CreateCommand),
63 Upload(upload::UploadCommand),
67 Edit(edit::EditCommand),
73 Rename(rename::RenameCommand),
76 Move(move_file::MoveCommand),
79 Permissions(permissions::PermissionsCommand),
82}
83
84impl DriveCommand {
85 pub async fn execute(self) -> Result<()> {
90 let _account_guard = self
99 .account
100 .as_ref()
101 .map(|account| crate::utils::env::ScopedEnvVar::set(DRIVE_ACCOUNT_ENV, account));
102
103 match self.command {
104 DriveSubcommands::Auth(cmd) => cmd.execute().await,
105 DriveSubcommands::Account(cmd) => cmd.execute(),
106 DriveSubcommands::Permissions(cmd) => cmd.execute().await,
111 command => {
112 let client = helpers::create_client()?;
113 command.dispatch(&client).await
114 }
115 }
116 }
117}
118
119impl DriveSubcommands {
120 async fn dispatch(self, client: &DriveClient) -> Result<()> {
124 match self {
125 Self::Auth(_) => unreachable!("Auth is dispatched before client resolution"),
126 Self::Account(_) => unreachable!("Account is dispatched before client resolution"),
127 Self::Permissions(_) => {
128 unreachable!("Permissions is dispatched before client resolution")
129 }
130 Self::Search(cmd) => cmd.execute(client).await,
131 Self::Read(cmd) => cmd.execute(client).await,
132 Self::Dedupe(cmd) => cmd.execute(client).await,
133 Self::Create(cmd) => cmd.execute(client).await,
134 Self::Upload(cmd) => cmd.execute(client).await,
135 Self::Edit(cmd) => cmd.execute(client).await,
136 Self::Rename(cmd) => cmd.execute(client).await,
137 Self::Move(cmd) => cmd.execute(client).await,
138 }
139 }
140}
141
142#[cfg(test)]
143#[allow(clippy::unwrap_used)]
144mod tests {
145 use super::*;
146 use crate::cli::drive::format::OutputFormat;
147 use crate::drive::auth::{DriveCredentials, DriveGrantedScopes};
148 use crate::utils::secret::Secret;
149
150 fn dead_credentials() -> DriveCredentials {
151 DriveCredentials {
152 client_id: "client".to_string(),
153 client_secret: Secret::new("secret"),
154 refresh_token: Secret::new("refresh"),
155 scope: DriveGrantedScopes::READONLY,
156 }
157 }
158
159 fn dead_client() -> DriveClient {
160 DriveClient::new("http://127.0.0.1:1", &dead_credentials()).unwrap()
161 }
162
163 #[tokio::test]
164 async fn execute_routes_auth_subcommand_and_surfaces_missing_credentials() {
165 let guard = crate::drive::test_support::EnvGuard::take();
166 let _dir = guard.clear_credentials();
167
168 let cmd = DriveCommand {
169 account: None,
170 command: DriveSubcommands::Auth(auth::AuthCommand {
171 command: auth::AuthSubcommands::Status(auth::StatusCommand { all: false }),
172 }),
173 };
174 let err = cmd.execute().await.unwrap_err();
175 assert!(err.to_string().contains("not configured"));
176 }
177
178 #[tokio::test]
179 async fn execute_non_auth_subcommand_errors_when_credentials_missing() {
180 let guard = crate::drive::test_support::EnvGuard::take();
181 let _dir = guard.clear_credentials();
182
183 let cmd = DriveCommand {
184 account: None,
185 command: DriveSubcommands::Search(search::SearchCommand {
186 query: "name contains 'report'".to_string(),
187 limit: 10,
188 output: OutputFormat::Table,
189 }),
190 };
191 let err = cmd.execute().await.unwrap_err();
192 assert!(err.to_string().contains("not configured"));
193 }
194
195 #[tokio::test]
196 async fn execute_restores_account_env_var_after_return() {
197 let guard = crate::drive::test_support::EnvGuard::take();
198 let _dir = guard.clear_credentials();
199
200 let cmd = DriveCommand {
201 account: Some("work".to_string()),
202 command: DriveSubcommands::Account(account::AccountCommand {
203 command: account::AccountSubcommands::List(account::list::ListCommand {
204 output: OutputFormat::Table,
205 }),
206 }),
207 };
208 cmd.execute().await.unwrap();
209 assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
210 }
211
212 #[tokio::test]
213 async fn execute_restores_previous_account_env_var_after_return() {
214 let guard = crate::drive::test_support::EnvGuard::take();
215 let _dir = guard.clear_credentials();
216 std::env::set_var(DRIVE_ACCOUNT_ENV, "personal");
217
218 let cmd = DriveCommand {
219 account: Some("work".to_string()),
220 command: DriveSubcommands::Account(account::AccountCommand {
221 command: account::AccountSubcommands::List(account::list::ListCommand {
222 output: OutputFormat::Table,
223 }),
224 }),
225 };
226 cmd.execute().await.unwrap();
227 assert_eq!(
228 std::env::var(DRIVE_ACCOUNT_ENV).ok().as_deref(),
229 Some("personal")
230 );
231 }
232
233 #[tokio::test]
234 async fn execute_does_not_leak_account_across_sequential_calls() {
235 let guard = crate::drive::test_support::EnvGuard::take();
236 let _dir = guard.clear_credentials();
237
238 let account_list_cmd = || DriveCommand {
239 account: None,
240 command: DriveSubcommands::Account(account::AccountCommand {
241 command: account::AccountSubcommands::List(account::list::ListCommand {
242 output: OutputFormat::Table,
243 }),
244 }),
245 };
246
247 let first = DriveCommand {
248 account: Some("alpha".to_string()),
249 ..account_list_cmd()
250 };
251 first.execute().await.unwrap();
252 assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
253
254 account_list_cmd().execute().await.unwrap();
257 assert_eq!(std::env::var(DRIVE_ACCOUNT_ENV).ok(), None);
258 }
259
260 #[tokio::test]
261 async fn execute_absent_account_leaves_ambient_env_var_untouched() {
262 let guard = crate::drive::test_support::EnvGuard::take();
263 let _dir = guard.clear_credentials();
264 std::env::set_var(DRIVE_ACCOUNT_ENV, "personal");
265
266 let cmd = DriveCommand {
267 account: None,
268 command: DriveSubcommands::Account(account::AccountCommand {
269 command: account::AccountSubcommands::List(account::list::ListCommand {
270 output: OutputFormat::Table,
271 }),
272 }),
273 };
274 cmd.execute().await.unwrap();
275 assert_eq!(
276 std::env::var(DRIVE_ACCOUNT_ENV).ok().as_deref(),
277 Some("personal")
278 );
279 }
280
281 #[tokio::test]
282 async fn execute_routes_account_list_without_client_resolution() {
283 let guard = crate::drive::test_support::EnvGuard::take();
284 let _dir = guard.clear_credentials();
285
286 let cmd = DriveCommand {
287 account: None,
288 command: DriveSubcommands::Account(account::AccountCommand {
289 command: account::AccountSubcommands::List(account::list::ListCommand {
290 output: OutputFormat::Table,
291 }),
292 }),
293 };
294 cmd.execute().await.unwrap();
295 }
296
297 #[tokio::test]
298 async fn execute_routes_permissions_show_without_client_resolution() {
299 let guard = crate::drive::test_support::EnvGuard::take();
300 let _dir = guard.clear_credentials();
301
302 let cmd = DriveCommand {
303 account: None,
304 command: DriveSubcommands::Permissions(permissions::PermissionsCommand {
305 command: permissions::PermissionsSubcommands::Show(
306 permissions::show::ShowCommand {
307 output: OutputFormat::Table,
308 },
309 ),
310 }),
311 };
312 cmd.execute().await.unwrap();
313 }
314
315 #[tokio::test]
316 async fn execute_routes_permissions_check_and_surfaces_missing_credentials() {
317 let guard = crate::drive::test_support::EnvGuard::take();
318 let _dir = guard.clear_credentials();
319
320 let cmd = DriveCommand {
321 account: None,
322 command: DriveSubcommands::Permissions(permissions::PermissionsCommand {
323 command: permissions::PermissionsSubcommands::Check(
324 permissions::check::CheckCommand {
325 id: "f1".to_string(),
326 operation: permissions::check::OperationArg::Read,
327 output: OutputFormat::Table,
328 },
329 ),
330 }),
331 };
332 let err = cmd.execute().await.unwrap_err();
333 assert!(err.to_string().contains("not configured"));
334 }
335
336 #[tokio::test]
337 async fn dispatch_routes_search() {
338 let cmd = DriveSubcommands::Search(search::SearchCommand {
339 query: "name contains 'x'".to_string(),
340 limit: 10,
341 output: OutputFormat::Table,
342 });
343 assert!(cmd.dispatch(&dead_client()).await.is_err());
344 }
345
346 #[tokio::test]
347 async fn dispatch_routes_read() {
348 let cmd = DriveSubcommands::Read(read::ReadCommand {
349 file_id: "f1".to_string(),
350 content: false,
351 export_mime_type: None,
352 out_file: None,
353 verify: false,
354 output: OutputFormat::Table,
355 });
356 assert!(cmd.dispatch(&dead_client()).await.is_err());
357 }
358
359 #[tokio::test]
360 async fn dispatch_routes_dedupe() {
361 let cmd = DriveSubcommands::Dedupe(dedupe::DedupeCommand {
362 query: "name contains 'x'".to_string(),
363 limit: 10,
364 output: OutputFormat::Table,
365 });
366 assert!(cmd.dispatch(&dead_client()).await.is_err());
367 }
368
369 #[tokio::test]
370 async fn dispatch_routes_create() {
371 let guard = crate::drive::test_support::EnvGuard::take();
382 let _dir = guard.clear_credentials();
383
384 let cmd = DriveSubcommands::Create(create::CreateCommand {
385 name: "New File".to_string(),
386 parent: "parent-1".to_string(),
387 folder: false,
388 mime_type: None,
389 dry_run: false,
390 output: OutputFormat::Table,
391 });
392 assert!(cmd.dispatch(&dead_client()).await.is_ok());
393 }
394
395 #[tokio::test]
396 async fn dispatch_routes_upload() {
397 let guard = crate::drive::test_support::EnvGuard::take();
400 let _dir = guard.clear_credentials();
401 let content_dir = tempfile::tempdir().unwrap();
402 let local_path = content_dir.path().join("upload-me.txt");
403 std::fs::write(&local_path, b"content").unwrap();
404
405 let cmd = DriveSubcommands::Upload(upload::UploadCommand {
406 local_path,
407 parent: "parent-1".to_string(),
408 name: None,
409 mime_type: None,
410 dry_run: false,
411 output: OutputFormat::Table,
412 });
413 assert!(cmd.dispatch(&dead_client()).await.is_ok());
414 }
415
416 #[tokio::test]
417 async fn dispatch_routes_edit() {
418 let guard = crate::drive::test_support::EnvGuard::take();
421 let _dir = guard.clear_credentials();
422 let content_dir = tempfile::tempdir().unwrap();
423 let content_path = content_dir.path().join("new-content.txt");
424 std::fs::write(&content_path, b"content").unwrap();
425
426 let cmd = DriveSubcommands::Edit(edit::EditCommand {
427 file_id: "f1".to_string(),
428 content: content_path.to_str().unwrap().to_string(),
429 mime_type: None,
430 dry_run: false,
431 output: OutputFormat::Table,
432 });
433 assert!(cmd.dispatch(&dead_client()).await.is_ok());
434 }
435
436 #[tokio::test]
437 async fn dispatch_routes_rename() {
438 let cmd = DriveSubcommands::Rename(rename::RenameCommand {
439 file_id: "f1".to_string(),
440 new_name: "New Name".to_string(),
441 dry_run: false,
442 output: OutputFormat::Table,
443 });
444 assert!(cmd.dispatch(&dead_client()).await.is_err());
445 }
446
447 #[tokio::test]
448 async fn dispatch_routes_move() {
449 let cmd = DriveSubcommands::Move(move_file::MoveCommand {
450 file_ids: vec!["f1".to_string()],
451 to: "dest1".to_string(),
452 allow_visibility_increase: false,
453 allow_visibility_decrease: false,
454 allow_drive_boundary_crossing: false,
455 dry_run: false,
456 output: OutputFormat::Table,
457 });
458 assert!(cmd.dispatch(&dead_client()).await.is_err());
459 }
460}