Rust で AWS S3 の曖昧検索を簡単にする CLI を作った

## はじめに

こんにちは、@uchidash456 です！！

今回 Rust の勉強を兼ねて、S3 オブジェクトのパスを検索する CLI ツール: [s3_search](https://github.com/shoot16625/s3_search) を作成しました。下画像のように、1 階層ずつインクリメンタルサーチし、オブジェクトの URI を取得することができます。
![t-rec.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343913/2efd01d3-5316-4ae5-5c45-8e88f893c11e.gif)

## 背景

業務上 S3 に定期的にデータを格納することが多く、データ更新の確認やスナップショットの取得などを AWS マネジメントコンソール（マネコン）を使っていつもポチポチしていました。
ただマネコンの S3 操作ってイケてないんですよね。バケットの検索でしか逐次検索を採用しておらず、オブジェクトの検索時はプレフィックス入力後にエンターを押して初めて結果が返ってきます。
ですので、階層が深いときは、キーワードでフィルター -> クリック -> フィルター -> クリック ... を X 回繰り返すことになります。わりとよくやっており、今ではキーワードパターンとクリック位置を覚えていて猛スピードでファイルまで到達できるようになりました w。
**もっとスムーズに目的のファイルに到達したいな**と思っていた次第です。

また、 [AWS CLI](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-services-s3-commands.html) の `aws s3 ls` を使うことで同様に検索が可能ですが、マネコン以上に入力の手間が多く、**タブ補完でいい感じにやってくれたらいいな**と思っていました。

## s3_search

### インストール方法

Homebrew でインストールできます 💫

```sh
brew tap shoot16625/tap
brew install s3_search
```

Rust の開発環境が整っている場合は、下記のコマンドでインストールできます。

```sh
cargo install s3_search
```

https://crates.io/crates/s3_search

### 使い方

インストール後、`s3s` を実行するとバケット一覧の表示・選択ができます。（事前に `~/.aws/credentials` の発行が必要です。）
選択をしていくと、1 階層下のオブジェクト一覧が表示され、また選択を繰り返し、目的のファイルに到達すると、そのファイルの URI が表示されます。ファイル周辺の状態を確認するのはマネコンの方が便利なので、リンクをクリックして Web に飛ぶスタイルを採用しています。（純正 Terminal だとリンククリックできないと思うので機能追加するといいかも）

```sh:コマンド例
# search s3 object path (default region: AWS_DEFAULT_REGION)
s3s

# search s3 object path (specify region)
s3s --region us-west-2

# help
s3s --help
```

※画像では録画の都合上 `./` をつけていますが、実際は不要です mm
![t-rec.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343913/2efd01d3-5316-4ae5-5c45-8e88f893c11e.gif)

### コマンド全体像

1. コマンドライン引数をパースする。
   - [clap](https://docs.rs/clap/latest/clap/) を利用しています。とてもシンプルに引数を扱えました。（ref. [Rust でコマンドライン引数を扱う (2) clap クレート](https://maku77.github.io/p/bdp2doy/)）
2. AWS クライアントを準備し、バケット一覧を取得する。
   - [aws_sdk_s3](https://docs.rs/aws-sdk-s3/latest/aws_sdk_s3/) を利用しています。
3. バケット一覧を描画し、あいまい検索できるようにする。
   - [dialoguer](https://docs.rs/dialoguer/latest/dialoguer/) を利用しています。[FuzzySelect](https://docs.rs/dialoguer/latest/dialoguer/struct.FuzzySelect.html) にリスト一覧を渡すことで、あいまい検索が可能です。他にもインタラクティブな入力を実現するためのメソッドが色々あるようです。（ref. [dialoguer でインタラクティブな入力](https://qiita.com/deepgreenAN/items/4f231d7c6d8596bfc39d))
4. 選択結果を元に、1 階層下のオブジェクト一覧を取得する。
   - 普通にオブジェクト一覧を取得しようとすると全オブジェクトが返ってきて、処理速度の低下や金銭的コストが無駄にかかってしまうので、`delimiter` を指定して、1 階層下のオブジェクトのみを取得するようにしています。
5. オブジェクト一覧を描画し、あいまい検索できるようにする。
   - (3)と同様の処理で実現できます。
6. ファイルに到達するまで(4)と(5)を繰り返し、マネコンの URI を生成して出力する。
   - ファイルが存在する場合はオブジェクト詳細画面の URI を生成し、存在しない場合はオブジェクト一覧画面の URI を生成しています。

### リリースの自動化

[Rust で EC2 検索を簡単にする CLI を作った ~ ec2-search ~](https://yuutookun.hatenablog.com/entry/rust_ec2_search) を参考に、GitHub Actions で自動リリースを行っています。
PR を main ブランチにマージすると、Git Tag の付与が走ります。それに成功すると、プロジェクトのビルドとリリースノートの更新、crates.io へのパブリッシュが走るようになっています。

この tag を打ち終わったら、リリースを開始する部分に手間取ったので、経緯を残しておきます。
当初は、下記のようにすれば tag の push をトリガーにリリースが開始できると思っていましたが、これでは動きません。

```yaml
on:
  push:
    tags:
      - "v*"
```

Github Actions では他のワークフローの処理をトリガーにする場合、専用の　`workflow_run` を利用する必要がありました。下記のようにすることで、tag 付与のワークフローの完了をトリガーにできるようになります。

```yaml
on:
  workflow_run:
    workflows: ["Create Git Tag"]
    types: ["completed"]
```

ただし、ワークフローの成功・失敗は考慮されていないので、別途 `if: ${{ github.event.workflow_run.conclusion == 'success' }}` を差し込んだりしています。

## おわりに

今回、初めて CLI を作成・公開することができ、ハードルの低さを体感しました。また出来としては、初期に思い描いていた UI にかなり近いものができ満足しています。Rust の入門としてはちょうどよいレベル感で、進められたかなと思います。また、何か思いついたら追加していきたいと思います。