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