1use std::io::{IsTerminal, stderr, stdout};
8
9use clap::{Parser, Subcommand, ValueEnum};
10use rc_core::config::Defaults;
11use rc_core::{ConfigManager, RequestHeader, set_global_request_headers};
12
13use crate::exit_code::ExitCode;
14use crate::output::{Formatter, OutputConfig};
15
16mod admin;
17mod alias;
18mod anonymous;
19mod bucket;
20mod cat;
21mod completions;
22mod cors;
23pub mod cp;
24pub mod diff;
25mod encryption;
26mod event;
27mod find;
28mod head;
29mod ilm;
30mod ls;
31mod mb;
32mod mirror;
33mod mv;
34mod object;
35mod pipe;
36mod quota;
37mod rb;
38mod replicate;
39mod rm;
40mod share;
41mod sql;
42mod stat;
43mod tag;
44mod tree;
45mod version;
46
47#[derive(Parser, Debug)]
52#[command(name = "rc")]
53#[command(author, version, about, long_about = None)]
54#[command(propagate_version = true)]
55pub struct Cli {
56 #[arg(long, global = true, value_enum)]
58 pub format: Option<OutputFormat>,
59
60 #[arg(long, global = true, default_value = "false")]
62 pub json: bool,
63
64 #[arg(long, global = true, default_value = "false")]
66 pub no_color: bool,
67
68 #[arg(long, global = true, default_value = "false")]
70 pub no_progress: bool,
71
72 #[arg(short, long, global = true, default_value = "false")]
74 pub quiet: bool,
75
76 #[arg(long, global = true, default_value = "false")]
78 pub debug: bool,
79
80 #[arg(short = 'H', long = "header", global = true, value_parser = parse_request_header)]
82 pub request_headers: Vec<RequestHeader>,
83
84 #[command(subcommand)]
85 pub command: Commands,
86}
87
88fn parse_request_header(value: &str) -> Result<RequestHeader, String> {
89 RequestHeader::parse(value).map_err(|error| error.to_string())
90}
91
92#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
93pub enum OutputFormat {
94 Auto,
95 Human,
96 Json,
97}
98
99#[derive(Copy, Clone, Debug, Eq, PartialEq)]
100enum OutputBehavior {
101 HumanDefault,
102 StructuredDefault,
103}
104
105#[derive(Copy, Clone, Debug)]
106struct GlobalOutputOptions {
107 format: Option<OutputFormat>,
108 json: bool,
109 no_color: bool,
110 force_color: bool,
111 no_progress: bool,
112 quiet: bool,
113}
114
115impl GlobalOutputOptions {
116 fn from_cli(cli: &Cli) -> Self {
117 Self {
118 format: cli.format,
119 json: cli.json,
120 no_color: cli.no_color,
121 force_color: false,
122 no_progress: cli.no_progress,
123 quiet: cli.quiet,
124 }
125 }
126
127 fn apply_defaults(mut self, defaults: &Defaults) -> Result<Self, String> {
128 if self.format.is_none() && !self.json {
129 self.format = Some(match defaults.output.as_str() {
130 "human" => OutputFormat::Human,
131 "json" => OutputFormat::Json,
132 value => return Err(format!("Invalid default output format '{value}'")),
133 });
134 }
135
136 if !self.no_color {
137 match defaults.color.as_str() {
138 "auto" => {}
139 "always" => self.force_color = true,
140 "never" => self.no_color = true,
141 value => return Err(format!("Invalid default color mode '{value}'")),
142 }
143 }
144 if !defaults.progress {
145 self.no_progress = true;
146 }
147
148 Ok(self)
149 }
150
151 fn resolve(self, behavior: OutputBehavior) -> OutputConfig {
152 let stdout_is_tty = stdout().is_terminal();
153 let stderr_is_tty = stderr().is_terminal();
154
155 let selected_format = if self.json {
156 OutputFormat::Json
157 } else {
158 self.format.unwrap_or(match behavior {
159 OutputBehavior::HumanDefault => OutputFormat::Human,
160 OutputBehavior::StructuredDefault => OutputFormat::Auto,
161 })
162 };
163
164 let json = match selected_format {
165 OutputFormat::Json => true,
166 OutputFormat::Human => false,
167 OutputFormat::Auto => !stdout_is_tty,
168 };
169
170 OutputConfig {
171 json,
172 no_color: self.no_color || (!self.force_color && !stdout_is_tty) || json,
173 no_progress: self.no_progress || !stderr_is_tty || json,
174 quiet: self.quiet,
175 }
176 }
177}
178
179#[derive(Subcommand, Debug)]
180pub enum Commands {
181 #[command(subcommand)]
183 Alias(alias::AliasCommands),
184
185 #[command(subcommand)]
187 Admin(admin::AdminCommands),
188
189 Bucket(bucket::BucketArgs),
191
192 Object(object::ObjectArgs),
194
195 Ls(ls::LsArgs),
198
199 Mb(mb::MbArgs),
201
202 Rb(rb::RbArgs),
204
205 Cat(cat::CatArgs),
207
208 Head(head::HeadArgs),
210
211 Stat(stat::StatArgs),
213
214 Cp(cp::CpArgs),
217
218 Mv(mv::MvArgs),
220
221 Rm(rm::RmArgs),
223
224 Pipe(pipe::PipeArgs),
226
227 Find(find::FindArgs),
230
231 Event(event::EventArgs),
233
234 #[command(subcommand)]
236 Cors(cors::CorsCommands),
237
238 Diff(diff::DiffArgs),
240
241 Mirror(mirror::MirrorArgs),
243
244 Tree(tree::TreeArgs),
246
247 Share(share::ShareArgs),
249
250 Sql(sql::SqlArgs),
252
253 #[command(subcommand)]
256 Version(version::VersionCommands),
257
258 #[command(subcommand)]
260 Tag(tag::TagCommands),
261
262 #[command(subcommand)]
264 Anonymous(anonymous::AnonymousCommands),
265
266 #[command(subcommand)]
268 Quota(quota::QuotaCommands),
269
270 Ilm(ilm::IlmArgs),
272
273 Replicate(replicate::ReplicateArgs),
275
276 Completions(completions::CompletionsArgs),
279 }
284
285pub async fn execute(cli: Cli) -> ExitCode {
287 set_global_request_headers(cli.request_headers.clone());
288 let cli_output_options = GlobalOutputOptions::from_cli(&cli);
289 let defaults = match ConfigManager::new() {
290 Ok(manager) if manager.config_path().exists() => match manager.load() {
291 Ok(config) => Some(config.defaults),
292 Err(error) => {
293 return Formatter::new(cli_output_options.resolve(OutputBehavior::HumanDefault))
294 .fail(
295 ExitCode::GeneralError,
296 &format!("Failed to load configuration: {error}"),
297 );
298 }
299 },
300 Ok(_) => None,
301 Err(error) => {
302 return Formatter::new(cli_output_options.resolve(OutputBehavior::HumanDefault)).fail(
303 ExitCode::GeneralError,
304 &format!("Failed to load configuration: {error}"),
305 );
306 }
307 };
308 let output_options = cli_output_options;
309 let output_options = if let Some(defaults) = &defaults {
310 match output_options.apply_defaults(defaults) {
311 Ok(options) => options,
312 Err(error) => {
313 return Formatter::new(cli_output_options.resolve(OutputBehavior::HumanDefault))
314 .fail(
315 ExitCode::GeneralError,
316 &format!("Failed to load configuration: {error}"),
317 );
318 }
319 }
320 } else {
321 output_options
322 };
323
324 match cli.command {
325 Commands::Alias(cmd) => {
326 alias::execute(cmd, output_options.resolve(OutputBehavior::HumanDefault)).await
327 }
328 Commands::Admin(cmd) => {
329 admin::execute(cmd, output_options.resolve(OutputBehavior::HumanDefault)).await
330 }
331 Commands::Bucket(args) => {
332 bucket::execute(
333 args,
334 output_options.resolve(OutputBehavior::StructuredDefault),
335 )
336 .await
337 }
338 Commands::Object(args) => {
339 let behavior = match &args.command {
340 object::ObjectCommands::Show(_) | object::ObjectCommands::Head(_) => {
341 OutputBehavior::HumanDefault
342 }
343 _ => OutputBehavior::StructuredDefault,
344 };
345 object::execute(args, output_options.resolve(behavior)).await
346 }
347 Commands::Ls(args) => {
348 ls::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
349 }
350 Commands::Mb(args) => {
351 mb::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
352 }
353 Commands::Rb(args) => {
354 rb::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
355 }
356 Commands::Cat(args) => {
357 cat::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
358 }
359 Commands::Head(args) => {
360 head::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
361 }
362 Commands::Stat(args) => {
363 stat::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
364 }
365 Commands::Cp(args) => {
366 cp::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
367 }
368 Commands::Mv(args) => {
369 mv::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
370 }
371 Commands::Rm(args) => {
372 rm::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
373 }
374 Commands::Pipe(args) => {
375 pipe::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
376 }
377 Commands::Find(args) => {
378 find::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
379 }
380 Commands::Event(args) => {
381 event::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
382 }
383 Commands::Cors(cmd) => {
384 cors::execute(
385 cors::CorsArgs { command: cmd },
386 output_options.resolve(OutputBehavior::HumanDefault),
387 )
388 .await
389 }
390 Commands::Diff(args) => {
391 diff::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
392 }
393 Commands::Mirror(args) => {
394 mirror::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
395 }
396 Commands::Tree(args) => {
397 tree::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
398 }
399 Commands::Share(args) => {
400 share::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
401 }
402 Commands::Sql(args) => {
403 sql::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
404 }
405 Commands::Version(cmd) => {
406 version::execute(
407 version::VersionArgs { command: cmd },
408 output_options.resolve(OutputBehavior::HumanDefault),
409 )
410 .await
411 }
412 Commands::Tag(cmd) => {
413 tag::execute(
414 tag::TagArgs { command: cmd },
415 output_options.resolve(OutputBehavior::HumanDefault),
416 )
417 .await
418 }
419 Commands::Anonymous(cmd) => {
420 anonymous::execute(
421 anonymous::AnonymousArgs { command: cmd },
422 output_options.resolve(OutputBehavior::HumanDefault),
423 )
424 .await
425 }
426 Commands::Quota(cmd) => {
427 quota::execute(
428 quota::QuotaArgs { command: cmd },
429 output_options.resolve(OutputBehavior::HumanDefault),
430 )
431 .await
432 }
433 Commands::Ilm(args) => {
434 ilm::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
435 }
436 Commands::Replicate(args) => {
437 replicate::execute(args, output_options.resolve(OutputBehavior::HumanDefault)).await
438 }
439 Commands::Completions(args) => completions::execute(args),
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use clap::Parser;
447
448 #[test]
449 fn structured_default_uses_auto_format_when_not_explicit() {
450 let options = GlobalOutputOptions {
451 format: None,
452 json: false,
453 no_color: false,
454 force_color: false,
455 no_progress: false,
456 quiet: false,
457 };
458
459 let resolved = options.resolve(OutputBehavior::StructuredDefault);
460 assert_eq!(resolved.json, !std::io::stdout().is_terminal());
461 }
462
463 #[test]
464 fn human_default_keeps_human_format_when_not_explicit() {
465 let options = GlobalOutputOptions {
466 format: None,
467 json: false,
468 no_color: false,
469 force_color: false,
470 no_progress: false,
471 quiet: false,
472 };
473
474 let resolved = options.resolve(OutputBehavior::HumanDefault);
475 assert!(!resolved.json);
476 }
477
478 #[test]
479 fn explicit_json_overrides_behavior_defaults() {
480 let options = GlobalOutputOptions {
481 format: Some(OutputFormat::Human),
482 json: true,
483 no_color: false,
484 force_color: false,
485 no_progress: false,
486 quiet: false,
487 };
488
489 let resolved = options.resolve(OutputBehavior::HumanDefault);
490 assert!(resolved.json);
491 }
492
493 #[test]
494 fn explicit_human_overrides_structured_default() {
495 let options = GlobalOutputOptions {
496 format: Some(OutputFormat::Human),
497 json: false,
498 no_color: false,
499 force_color: false,
500 no_progress: false,
501 quiet: false,
502 };
503
504 let resolved = options.resolve(OutputBehavior::StructuredDefault);
505 assert!(!resolved.json);
506 }
507
508 #[test]
509 fn explicit_auto_overrides_human_default() {
510 let options = GlobalOutputOptions {
511 format: Some(OutputFormat::Auto),
512 json: false,
513 no_color: false,
514 force_color: false,
515 no_progress: false,
516 quiet: false,
517 };
518
519 let resolved = options.resolve(OutputBehavior::HumanDefault);
520 assert_eq!(resolved.json, !std::io::stdout().is_terminal());
521 }
522
523 #[test]
524 fn configured_defaults_control_output_color_and_progress() {
525 let options = GlobalOutputOptions {
526 format: None,
527 json: false,
528 no_color: false,
529 force_color: false,
530 no_progress: false,
531 quiet: false,
532 };
533 let defaults = Defaults {
534 output: "json".to_string(),
535 color: "never".to_string(),
536 progress: false,
537 };
538
539 let resolved = options
540 .apply_defaults(&defaults)
541 .expect("valid defaults")
542 .resolve(OutputBehavior::HumanDefault);
543
544 assert!(resolved.json);
545 assert!(resolved.no_color);
546 assert!(resolved.no_progress);
547 }
548
549 #[test]
550 fn invalid_configured_defaults_are_rejected() {
551 let options = GlobalOutputOptions {
552 format: None,
553 json: false,
554 no_color: false,
555 force_color: false,
556 no_progress: false,
557 quiet: false,
558 };
559 let defaults = Defaults {
560 output: "yaml".to_string(),
561 ..Defaults::default()
562 };
563
564 assert!(options.apply_defaults(&defaults).is_err());
565 }
566
567 #[test]
568 fn cli_accepts_global_custom_amz_header() {
569 let cli = Cli::try_parse_from([
570 "rc",
571 "-H",
572 "x-amz-bucket-encrypt-enabled:1",
573 "bucket",
574 "list",
575 "local/",
576 ])
577 .expect("parse custom header");
578
579 assert_eq!(cli.request_headers.len(), 1);
580 assert_eq!(cli.request_headers[0].name, "x-amz-bucket-encrypt-enabled");
581 assert_eq!(cli.request_headers[0].value, "1");
582 }
583
584 #[test]
585 fn cli_rejects_non_amz_custom_header() {
586 let error = Cli::try_parse_from(["rc", "-H", "authorization:secret", "ls", "local/"])
587 .expect_err("non amz header should fail");
588
589 assert!(
590 error
591 .to_string()
592 .contains("Only x-amz-* custom request headers are supported")
593 );
594 }
595
596 #[test]
597 fn cli_accepts_bucket_cors_subcommand() {
598 let cli = Cli::try_parse_from(["rc", "bucket", "cors", "list", "local/my-bucket"])
599 .expect("parse bucket cors");
600
601 match cli.command {
602 Commands::Bucket(args) => match args.command {
603 bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
604 assert_eq!(arg.path, "local/my-bucket");
605 }
606 other => panic!("expected bucket cors list command, got {:?}", other),
607 },
608 other => panic!("expected bucket command, got {:?}", other),
609 }
610 }
611
612 #[test]
613 fn cli_accepts_bucket_list_alias() {
614 let cli =
615 Cli::try_parse_from(["rc", "bucket", "ls", "local/"]).expect("parse bucket ls alias");
616
617 match cli.command {
618 Commands::Bucket(args) => match args.command {
619 bucket::BucketCommands::List(arg) => {
620 assert_eq!(arg.path, "local/");
621 }
622 other => panic!("expected bucket list alias, got {:?}", other),
623 },
624 other => panic!("expected bucket command, got {:?}", other),
625 }
626 }
627
628 #[test]
629 fn cli_accepts_top_level_cors_subcommand() {
630 let cli = Cli::try_parse_from(["rc", "cors", "remove", "local/my-bucket"])
631 .expect("parse top-level cors");
632
633 match cli.command {
634 Commands::Cors(cors::CorsCommands::Remove(arg)) => {
635 assert_eq!(arg.path, "local/my-bucket");
636 }
637 other => panic!("expected top-level cors remove command, got {:?}", other),
638 }
639 }
640
641 #[test]
642 fn cli_accepts_top_level_cors_get_alias() {
643 let cli =
644 Cli::try_parse_from(["rc", "cors", "get", "local/my-bucket"]).expect("parse cors get");
645
646 match cli.command {
647 Commands::Cors(cors::CorsCommands::List(arg)) => {
648 assert_eq!(arg.path, "local/my-bucket");
649 }
650 other => panic!("expected top-level cors get alias, got {:?}", other),
651 }
652 }
653
654 #[test]
655 fn cli_accepts_top_level_event_subcommand() {
656 let cli = Cli::try_parse_from(["rc", "event", "list", "local/my-bucket"])
657 .expect("parse top-level event");
658
659 match cli.command {
660 Commands::Event(event::EventArgs {
661 command: event::EventCommands::List(arg),
662 }) => {
663 assert_eq!(arg.path, "local/my-bucket");
664 }
665 other => panic!("expected top-level event list command, got {:?}", other),
666 }
667 }
668
669 #[test]
670 fn cli_accepts_top_level_event_add_subcommand() {
671 let cli = Cli::try_parse_from([
672 "rc",
673 "event",
674 "add",
675 "local/my-bucket",
676 "arn:aws:sqs:us-east-1:123456789012:jobs",
677 "--event",
678 "put,delete",
679 "--force",
680 ])
681 .expect("parse top-level event add");
682
683 match cli.command {
684 Commands::Event(event::EventArgs {
685 command: event::EventCommands::Add(arg),
686 }) => {
687 assert_eq!(arg.path, "local/my-bucket");
688 assert_eq!(arg.arn, "arn:aws:sqs:us-east-1:123456789012:jobs");
689 assert_eq!(arg.events, vec!["put,delete".to_string()]);
690 assert!(arg.force);
691 }
692 other => panic!("expected top-level event add command, got {:?}", other),
693 }
694 }
695
696 #[test]
697 fn cli_accepts_sql_select_options() {
698 let cli = Cli::try_parse_from([
699 "rc",
700 "sql",
701 "local/reports/data.jsonl",
702 "--query",
703 "SELECT * FROM S3Object",
704 "--input-format",
705 "json",
706 "--output-format",
707 "json",
708 "--compression",
709 "gzip",
710 ])
711 .expect("parse sql command");
712
713 match cli.command {
714 Commands::Sql(arg) => {
715 assert_eq!(arg.path, "local/reports/data.jsonl");
716 assert_eq!(arg.query, "SELECT * FROM S3Object");
717 assert!(matches!(arg.input_format, sql::InputFormatArg::Json));
718 assert!(matches!(arg.output_format, sql::OutputFormatArg::Json));
719 assert!(matches!(arg.compression, sql::CompressionArg::Gzip));
720 }
721 other => panic!("expected sql command, got {:?}", other),
722 }
723 }
724
725 #[test]
726 fn cli_accepts_sql_defaults() {
727 let cli = Cli::try_parse_from([
728 "rc",
729 "sql",
730 "local/reports/data.csv",
731 "--query",
732 "SELECT s._1 FROM S3Object s",
733 ])
734 .expect("parse sql command defaults");
735
736 match cli.command {
737 Commands::Sql(arg) => {
738 assert_eq!(arg.path, "local/reports/data.csv");
739 assert_eq!(arg.query, "SELECT s._1 FROM S3Object s");
740 assert!(matches!(arg.input_format, sql::InputFormatArg::Csv));
741 assert!(matches!(arg.output_format, sql::OutputFormatArg::Csv));
742 assert!(matches!(arg.compression, sql::CompressionArg::None));
743 }
744 other => panic!("expected sql command, got {:?}", other),
745 }
746 }
747
748 #[test]
749 fn cli_accepts_object_list_alias() {
750 let cli = Cli::try_parse_from(["rc", "object", "ls", "local/my-bucket/logs/"])
751 .expect("parse object ls alias");
752
753 match cli.command {
754 Commands::Object(args) => match args.command {
755 object::ObjectCommands::List(arg) => {
756 assert_eq!(arg.path, "local/my-bucket/logs/");
757 }
758 other => panic!("expected object list alias, got {:?}", other),
759 },
760 other => panic!("expected object command, got {:?}", other),
761 }
762 }
763
764 #[test]
765 fn cli_accepts_top_level_event_remove_subcommand() {
766 let cli = Cli::try_parse_from([
767 "rc",
768 "event",
769 "remove",
770 "local/my-bucket",
771 "arn:aws:sns:us-east-1:123456789012:alerts",
772 "--force",
773 ])
774 .expect("parse top-level event remove");
775
776 match cli.command {
777 Commands::Event(event::EventArgs {
778 command: event::EventCommands::Remove(arg),
779 }) => {
780 assert_eq!(arg.path, "local/my-bucket");
781 assert_eq!(arg.arn, "arn:aws:sns:us-east-1:123456789012:alerts");
782 assert!(arg.force);
783 }
784 other => panic!("expected top-level event remove command, got {:?}", other),
785 }
786 }
787
788 #[test]
789 fn cli_accepts_bucket_cors_get_alias() {
790 let cli = Cli::try_parse_from(["rc", "bucket", "cors", "get", "local/my-bucket"])
791 .expect("parse bucket cors get");
792
793 match cli.command {
794 Commands::Bucket(args) => match args.command {
795 bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
796 assert_eq!(arg.path, "local/my-bucket");
797 }
798 other => panic!("expected bucket cors get alias, got {:?}", other),
799 },
800 other => panic!("expected bucket command, got {:?}", other),
801 }
802 }
803
804 #[test]
805 fn cli_accepts_bucket_cors_set_with_positional_source() {
806 let cli =
807 Cli::try_parse_from(["rc", "bucket", "cors", "set", "local/my-bucket", "cors.xml"])
808 .expect("parse bucket cors set with positional source");
809
810 match cli.command {
811 Commands::Bucket(args) => match args.command {
812 bucket::BucketCommands::Cors(cors::CorsCommands::Set(arg)) => {
813 assert_eq!(arg.path, "local/my-bucket");
814 assert_eq!(arg.source.as_deref(), Some("cors.xml"));
815 }
816 other => panic!("expected bucket cors set command, got {:?}", other),
817 },
818 other => panic!("expected bucket command, got {:?}", other),
819 }
820 }
821
822 #[test]
823 fn cli_accepts_top_level_cors_set_with_positional_source() {
824 let cli = Cli::try_parse_from(["rc", "cors", "set", "local/my-bucket", "cors.xml"])
825 .expect("parse top-level cors set with positional source");
826
827 match cli.command {
828 Commands::Cors(cors::CorsCommands::Set(arg)) => {
829 assert_eq!(arg.path, "local/my-bucket");
830 assert_eq!(arg.source.as_deref(), Some("cors.xml"));
831 assert_eq!(arg.file, None);
832 assert!(!arg.force);
833 }
834 other => panic!("expected top-level cors set command, got {:?}", other),
835 }
836 }
837
838 #[test]
839 fn cli_accepts_top_level_cors_set_with_legacy_file_flag() {
840 let cli = Cli::try_parse_from([
841 "rc",
842 "cors",
843 "set",
844 "local/my-bucket",
845 "--file",
846 "cors.json",
847 "--force",
848 ])
849 .expect("parse top-level cors set with --file");
850
851 match cli.command {
852 Commands::Cors(cors::CorsCommands::Set(arg)) => {
853 assert_eq!(arg.path, "local/my-bucket");
854 assert_eq!(arg.source, None);
855 assert_eq!(arg.file.as_deref(), Some("cors.json"));
856 assert!(arg.force);
857 }
858 other => panic!("expected top-level cors set command, got {:?}", other),
859 }
860 }
861
862 #[test]
863 fn cli_accepts_bucket_cors_list_force_flag() {
864 let cli =
865 Cli::try_parse_from(["rc", "bucket", "cors", "list", "local/my-bucket", "--force"])
866 .expect("parse bucket cors list with force");
867
868 match cli.command {
869 Commands::Bucket(args) => match args.command {
870 bucket::BucketCommands::Cors(cors::CorsCommands::List(arg)) => {
871 assert_eq!(arg.path, "local/my-bucket");
872 assert!(arg.force);
873 }
874 other => panic!("expected bucket cors list command, got {:?}", other),
875 },
876 other => panic!("expected bucket command, got {:?}", other),
877 }
878 }
879
880 #[test]
881 fn cli_accepts_bucket_lifecycle_subcommand() {
882 let cli = Cli::try_parse_from([
883 "rc",
884 "bucket",
885 "lifecycle",
886 "rule",
887 "list",
888 "local/my-bucket",
889 ])
890 .expect("parse bucket lifecycle rule list");
891
892 match cli.command {
893 Commands::Bucket(args) => match args.command {
894 bucket::BucketCommands::Lifecycle(ilm::IlmArgs {
895 command: ilm::IlmCommands::Rule(ilm::rule::RuleCommands::List(arg)),
896 }) => {
897 assert_eq!(arg.path, "local/my-bucket");
898 assert!(!arg.force);
899 }
900 other => panic!(
901 "expected bucket lifecycle rule list command, got {:?}",
902 other
903 ),
904 },
905 other => panic!("expected bucket command, got {:?}", other),
906 }
907 }
908
909 #[test]
910 fn cli_accepts_bucket_replication_subcommand() {
911 let cli = Cli::try_parse_from(["rc", "bucket", "replication", "status", "local/my-bucket"])
912 .expect("parse bucket replication status");
913
914 match cli.command {
915 Commands::Bucket(args) => match args.command {
916 bucket::BucketCommands::Replication(replicate::ReplicateArgs {
917 command: replicate::ReplicateCommands::Status(arg),
918 }) => {
919 assert_eq!(arg.path, "local/my-bucket");
920 assert!(!arg.force);
921 }
922 other => panic!(
923 "expected bucket replication status command, got {:?}",
924 other
925 ),
926 },
927 other => panic!("expected bucket command, got {:?}", other),
928 }
929 }
930
931 #[test]
932 fn cli_accepts_bucket_replication_add_tls_flags() {
933 let cli = Cli::try_parse_from([
934 "rc",
935 "bucket",
936 "replication",
937 "add",
938 "local/my-bucket",
939 "--remote-bucket",
940 "backup/archive",
941 "--insecure",
942 ])
943 .expect("parse bucket replication add with insecure");
944
945 match cli.command {
946 Commands::Bucket(args) => match args.command {
947 bucket::BucketCommands::Replication(replicate::ReplicateArgs {
948 command: replicate::ReplicateCommands::Add(arg),
949 }) => {
950 assert_eq!(arg.path, "local/my-bucket");
951 assert_eq!(arg.remote_bucket, "backup/archive");
952 assert!(arg.insecure);
953 }
954 other => panic!("expected bucket replication add command, got {:?}", other),
955 },
956 other => panic!("expected bucket command, got {:?}", other),
957 }
958 }
959
960 #[test]
961 fn cli_accepts_bucket_remove_subcommand() {
962 let cli = Cli::try_parse_from(["rc", "bucket", "remove", "local/my-bucket"])
963 .expect("parse bucket remove");
964
965 match cli.command {
966 Commands::Bucket(args) => match args.command {
967 bucket::BucketCommands::Remove(arg) => {
968 assert_eq!(arg.target, "local/my-bucket");
969 }
970 other => panic!("expected bucket remove command, got {:?}", other),
971 },
972 other => panic!("expected bucket command, got {:?}", other),
973 }
974 }
975
976 #[test]
977 fn cli_accepts_object_remove_subcommand() {
978 let cli = Cli::try_parse_from([
979 "rc",
980 "object",
981 "remove",
982 "local/my-bucket/report.csv",
983 "--dry-run",
984 ])
985 .expect("parse object remove");
986
987 match cli.command {
988 Commands::Object(args) => match args.command {
989 object::ObjectCommands::Remove(arg) => {
990 assert_eq!(arg.paths, vec!["local/my-bucket/report.csv".to_string()]);
991 assert!(arg.dry_run);
992 }
993 other => panic!("expected object remove command, got {:?}", other),
994 },
995 other => panic!("expected object command, got {:?}", other),
996 }
997 }
998
999 #[test]
1000 fn cli_accepts_bucket_event_remove_subcommand() {
1001 let cli = Cli::try_parse_from([
1002 "rc",
1003 "bucket",
1004 "event",
1005 "remove",
1006 "local/my-bucket",
1007 "arn:aws:sns:us-east-1:123456789012:alerts",
1008 ])
1009 .expect("parse bucket event remove");
1010
1011 match cli.command {
1012 Commands::Bucket(args) => match args.command {
1013 bucket::BucketCommands::Event(event::EventCommands::Remove(arg)) => {
1014 assert_eq!(arg.path, "local/my-bucket");
1015 assert_eq!(arg.arn, "arn:aws:sns:us-east-1:123456789012:alerts");
1016 }
1017 other => panic!("expected bucket event remove command, got {:?}", other),
1018 },
1019 other => panic!("expected bucket command, got {:?}", other),
1020 }
1021 }
1022
1023 #[test]
1024 fn cli_accepts_rm_purge_flag() {
1025 let cli = Cli::try_parse_from(["rc", "rm", "local/my-bucket/object.txt", "--purge"])
1026 .expect("parse rm purge");
1027
1028 match cli.command {
1029 Commands::Rm(arg) => {
1030 assert_eq!(arg.paths, vec!["local/my-bucket/object.txt".to_string()]);
1031 assert!(arg.purge);
1032 }
1033 other => panic!("expected rm command, got {:?}", other),
1034 }
1035 }
1036
1037 #[test]
1038 fn cli_accepts_object_remove_purge_flag() {
1039 let cli = Cli::try_parse_from([
1040 "rc",
1041 "object",
1042 "remove",
1043 "local/my-bucket/object.txt",
1044 "--purge",
1045 ])
1046 .expect("parse object remove purge");
1047
1048 match cli.command {
1049 Commands::Object(args) => match args.command {
1050 object::ObjectCommands::Remove(arg) => {
1051 assert_eq!(arg.paths, vec!["local/my-bucket/object.txt".to_string()]);
1052 assert!(arg.purge);
1053 }
1054 other => panic!("expected object remove command, got {:?}", other),
1055 },
1056 other => panic!("expected object command, got {:?}", other),
1057 }
1058 }
1059
1060 #[test]
1061 fn cli_accepts_object_stat_subcommand() {
1062 let cli = Cli::try_parse_from(["rc", "object", "stat", "local/my-bucket/report.json"])
1063 .expect("parse object stat");
1064
1065 match cli.command {
1066 Commands::Object(args) => match args.command {
1067 object::ObjectCommands::Stat(arg) => {
1068 assert_eq!(arg.path, "local/my-bucket/report.json");
1069 }
1070 other => panic!("expected object stat command, got {:?}", other),
1071 },
1072 other => panic!("expected object command, got {:?}", other),
1073 }
1074 }
1075
1076 #[test]
1077 fn cli_accepts_object_copy_with_transfer_options() {
1078 let cli = Cli::try_parse_from([
1079 "rc",
1080 "object",
1081 "copy",
1082 "./report.json",
1083 "local/my-bucket/reports/",
1084 "--content-type",
1085 "application/json",
1086 "--storage-class",
1087 "STANDARD_IA",
1088 "--dry-run",
1089 ])
1090 .expect("parse object copy with transfer options");
1091
1092 match cli.command {
1093 Commands::Object(args) => match args.command {
1094 object::ObjectCommands::Copy(arg) => {
1095 assert_eq!(arg.source, "./report.json");
1096 assert_eq!(arg.target, "local/my-bucket/reports/");
1097 assert_eq!(arg.content_type.as_deref(), Some("application/json"));
1098 assert_eq!(arg.storage_class.as_deref(), Some("STANDARD_IA"));
1099 assert!(arg.dry_run);
1100 }
1101 other => panic!("expected object copy command, got {:?}", other),
1102 },
1103 other => panic!("expected object command, got {:?}", other),
1104 }
1105 }
1106
1107 #[test]
1108 fn cli_accepts_object_move_with_recursive_dry_run() {
1109 let cli = Cli::try_parse_from([
1110 "rc",
1111 "object",
1112 "move",
1113 "local/source-bucket/logs/",
1114 "local/archive-bucket/logs/",
1115 "--recursive",
1116 "--dry-run",
1117 "--continue-on-error",
1118 ])
1119 .expect("parse object move with recursive dry-run");
1120
1121 match cli.command {
1122 Commands::Object(args) => match args.command {
1123 object::ObjectCommands::Move(arg) => {
1124 assert_eq!(arg.source, "local/source-bucket/logs/");
1125 assert_eq!(arg.target, "local/archive-bucket/logs/");
1126 assert!(arg.recursive);
1127 assert!(arg.dry_run);
1128 assert!(arg.continue_on_error);
1129 }
1130 other => panic!("expected object move command, got {:?}", other),
1131 },
1132 other => panic!("expected object command, got {:?}", other),
1133 }
1134 }
1135
1136 #[test]
1137 fn cli_accepts_object_show_and_head_options() {
1138 let show_cli = Cli::try_parse_from([
1139 "rc",
1140 "object",
1141 "show",
1142 "local/my-bucket/report.json",
1143 "--version-id",
1144 "v1",
1145 "--rewind",
1146 "1h",
1147 ])
1148 .expect("parse object show options");
1149
1150 match show_cli.command {
1151 Commands::Object(args) => match args.command {
1152 object::ObjectCommands::Show(arg) => {
1153 assert_eq!(arg.path, "local/my-bucket/report.json");
1154 assert_eq!(arg.version_id.as_deref(), Some("v1"));
1155 assert_eq!(arg.rewind.as_deref(), Some("1h"));
1156 }
1157 other => panic!("expected object show command, got {:?}", other),
1158 },
1159 other => panic!("expected object command, got {:?}", other),
1160 }
1161
1162 let head_cli = Cli::try_parse_from([
1163 "rc",
1164 "object",
1165 "head",
1166 "local/my-bucket/report.json",
1167 "--bytes",
1168 "128",
1169 "--version-id",
1170 "v2",
1171 ])
1172 .expect("parse object head options");
1173
1174 match head_cli.command {
1175 Commands::Object(args) => match args.command {
1176 object::ObjectCommands::Head(arg) => {
1177 assert_eq!(arg.path, "local/my-bucket/report.json");
1178 assert_eq!(arg.bytes, Some(128));
1179 assert_eq!(arg.version_id.as_deref(), Some("v2"));
1180 }
1181 other => panic!("expected object head command, got {:?}", other),
1182 },
1183 other => panic!("expected object command, got {:?}", other),
1184 }
1185 }
1186
1187 #[test]
1188 fn cli_accepts_object_find_and_tree_options() {
1189 let find_cli = Cli::try_parse_from([
1190 "rc",
1191 "object",
1192 "find",
1193 "local/my-bucket/logs/",
1194 "--name",
1195 "*.json",
1196 "--maxdepth",
1197 "2",
1198 "--count",
1199 "--print",
1200 ])
1201 .expect("parse object find options");
1202
1203 match find_cli.command {
1204 Commands::Object(args) => match args.command {
1205 object::ObjectCommands::Find(arg) => {
1206 assert_eq!(arg.path, "local/my-bucket/logs/");
1207 assert_eq!(arg.name.as_deref(), Some("*.json"));
1208 assert_eq!(arg.maxdepth, 2);
1209 assert!(arg.count);
1210 assert!(arg.print);
1211 }
1212 other => panic!("expected object find command, got {:?}", other),
1213 },
1214 other => panic!("expected object command, got {:?}", other),
1215 }
1216
1217 let tree_cli = Cli::try_parse_from([
1218 "rc",
1219 "object",
1220 "tree",
1221 "local/my-bucket/logs/",
1222 "--level",
1223 "4",
1224 "--size",
1225 "--pattern",
1226 "*.json",
1227 "--full-path",
1228 ])
1229 .expect("parse object tree options");
1230
1231 match tree_cli.command {
1232 Commands::Object(args) => match args.command {
1233 object::ObjectCommands::Tree(arg) => {
1234 assert_eq!(arg.path, "local/my-bucket/logs/");
1235 assert_eq!(arg.level, 4);
1236 assert!(arg.size);
1237 assert_eq!(arg.pattern.as_deref(), Some("*.json"));
1238 assert!(arg.full_path);
1239 }
1240 other => panic!("expected object tree command, got {:?}", other),
1241 },
1242 other => panic!("expected object command, got {:?}", other),
1243 }
1244 }
1245}