Cloudflare R2のObjectを操作するcrate「cf-r2-sdk」を作成した話

# crate作成の経緯

今まで利用していた「cloudflare-r2-rs」という Cloudflare R2 の Object を操作する crate が、おそらく[この aws SDKの更新](https://www.cloudflarestatus.com/incidents/t5nrjmpxc1cj)によって正常に動作しなくなってしまいました。なので、この crate に似た crate を自作したというお話です (私の作成した crateの方が機能は少ないです)。(pull request にしなかった理由は、私の Rust に対する理解度が不足しており、cloudflare-r2-rs のコードの一部がわからなかったためです...)

# 作成したcrate

作成したcrate「cf-r2-sdk」は、以下のリンクから確認できます！

https://crates.io/crates/cf-r2-sdk

https://docs.rs/cf-r2-sdk/latest/cf_r2_sdk/index.html

GitHub Repository

https://github.com/Myxogastria0808/cf-r2-sdk

# 問題の原因と思われるものと解決

AWSが最近SDKを更新したことによって、CRC32チェックサムがデフォルトで有効となったのが問題の原因だと思われます (推測です) 。Cloudflareのドキュメントには、解決策の一つに`S3Client config` に以下の設定を追加すると解決すると書かれていたので、指示通り追加することで解決しました。

```
requestChecksumCalculation: "WHEN_REQUIRED",
responseChecksumValidation: "WHEN_REQUIRED",
```

引用元： https://developers.cloudflare.com/r2/examples/aws/aws-sdk-js-v3/

具体的には、以下のようなプログラムを書きました。

```rust
let config = aws_sdk_s3::config::Builder::new()
    .credentials_provider(credentials)
    .region(Region::new(self.region.clone()))
    .endpoint_url(&self.endpoint)
    .set_request_checksum_calculation(Some(RequestChecksumCalculation::WhenRequired))
    .set_response_checksum_validation(Some(ResponseChecksumValidation::WhenRequired))
    .clone()
    .build();
```

コード参考元: https://github.com/Songmu/r2sync (Owner: [Songmu](https://github.com/Songmu))

# 作成したcrate「cf-r2-sdk」の使い方

使い方は [docs.rs](https://docs.rs/cf-r2-sdk/latest/cf_r2_sdk/index.html#how-to-use) にもある程度書いていますが、記事にも使い方を書いておこうと思います。

Sample Repositoryは、以下のリンクから見ることができます。

https://github.com/Myxogastria0808/cf-r2-sdk-sample

1. 必要なcrateを追加

```sh
cargo add cf-r2-sdk
cargo add dotenvy
cargo add tokio -F full
```

2. `BUCKET_NAME`、`ENDPOINT_URL`、`ACCESS_KEY_ID`、`SECRET_ACCESS_KEY`、(`REGION`) を .env ファイルに記述しておきます。 (.env ファイルは、必ず .gitignore ファイルに追記して push しないように注意してください)

```.env
BUCKET_NAME=
ACCESS_KEY_ID=
SECRET_ACCESS_KEY=
ENDPOINT_URL=
REGION=auto
```

<details><summary>ENDPOINT_URL、ACCESS_KEY_ID、SECRET_ACCESS_KEY の取得方法はこちら</summary>

1. R2 Object Strage > Overview に移動
2. API > Manage API tokens をクリックします。
![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3052981/83e7f5e5-710e-42d4-3794-c11c057fa96d.png)
3. Create API Token をクリックします。
![image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3052981/ff629a72-cc5e-ef42-2e25-5075dba85158.png)
4. Permissionsを Object Read & Write にして、Create API tokenをクリックします。
![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3052981/04aee789-b4ca-4ea6-9f2e-22b67b189584.png)
5. ACCESS Key ID、Secret Access Key、endpoints を取得したら、Finish で完了です。
![image4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3052981/69c15aa9-5545-d0d7-cdcf-845117839ead.png)




</details>

3. `cf-r2-sdk::builder::Builer` で、`aws_sdk_s3::Client` を作成します。

```rust
use cf_r2_sdk::builder::Builder;
use dotenvy::dotenv;
use std::env;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    // load .env file
    dotenv().expect(".env file not found.");
    // insert a environment variable
    let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
    let endpoint_url: String =
        env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
    let access_key_id: String =
        env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
    let secret_access_key: String =
        env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
    let region: String = env::var("REGION").expect("REGION not found in .env file.");

    // 1.`cf-r2-sdk::builder::Builer` で、`aws_sdk_s3::Client` を作成する
    let object: cf_r2_sdk::operator::Operator = Builder::new()
        .set_bucket_name(bucket_name)
        .set_access_key_id(access_key_id)
        .set_secret_access_key(secret_access_key)
        .set_endpoint(endpoint_url)
        .set_region(region)
        .create_client();
}
```

4. R2 の Object Strage を操作します。

   - バイナリデータのアップロード

例

```rust
use cf_r2_sdk::builder::Builder;
use dotenvy::dotenv;
use std::env;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), cf_r2_sdk::error::OperationError> {
   // load .env file
   dotenv().expect(".env file not found.");
   // insert a environment variable
   let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
   let endpoint_url: String =
       env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
   let access_key_id: String =
       env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
   let secret_access_key: String =
      env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
   let region: String = env::var("REGION").expect("REGION not found in .env file.");

   let object: cf_r2_sdk::operator::Operator = Builder::new()
       .set_bucket_name(bucket_name)
       .set_access_key_id(access_key_id)
       .set_secret_access_key(secret_access_key)
       .set_endpoint(endpoint_url)
       .set_region(region)
       .create_client();

   // upload binary data
   object
       .upload_binary("sample.txt", "test/plain", b"Hello, World!", None)
       .await?;
   Ok(())
}
```

   - ファイルのアップロード

例

```rust
use cf_r2_sdk::builder::Builder;
use dotenvy::dotenv;
use std::env;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), cf_r2_sdk::error::OperationError> {
   // load .env file
   dotenv().expect(".env file not found.");
   // insert a environment variable
   let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
   let endpoint_url: String =
       env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
   let access_key_id: String =
       env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
   let secret_access_key: String =
      env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
   let region: String = env::var("REGION").expect("REGION not found in .env file.");

   let object: cf_r2_sdk::operator::Operator = Builder::new()
       .set_bucket_name(bucket_name)
       .set_access_key_id(access_key_id)
       .set_secret_access_key(secret_access_key)
       .set_endpoint(endpoint_url)
       .set_region(region)
       .create_client();

   // upload file
   object
       .upload_file("sample.jpg", "image/jpeg", "./data/sample.jpg", None)
       .await?;
   Ok(())
}
```

   - バイナリデータのダウンロード

例

```rust
use cf_r2_sdk::builder::Builder;
use dotenvy::dotenv;
use std::env;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), cf_r2_sdk::error::OperationError> {
   // load .env file
   dotenv().expect(".env file not found.");
   // insert a environment variable
   let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
   let endpoint_url: String =
       env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
   let access_key_id: String =
       env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
   let secret_access_key: String =
      env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
   let region: String = env::var("REGION").expect("REGION not found in .env file.");

   let object: cf_r2_sdk::operator::Operator = Builder::new()
       .set_bucket_name(bucket_name)
       .set_access_key_id(access_key_id)
       .set_secret_access_key(secret_access_key)
       .set_endpoint(endpoint_url)
       .set_region(region)
       .create_client();

   object
       .upload_binary("sample.txt", "test/plain", b"Hello, World!", None)
       .await?;

   // download binary data
   let bin: Vec<u8> = object.download("sample.txt").await?;

   println!("{:?}", bin);
   Ok(())
}
```

   - ファイルの削除

例

```rust
use cf_r2_sdk::builder::Builder;
use dotenvy::dotenv;
use std::env;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), cf_r2_sdk::error::OperationError> {
   // load .env file
   dotenv().expect(".env file not found.");
   // insert a environment variable
   let bucket_name = env::var("BUCKET_NAME").expect("BUCKET_NAME not found in .env file.");
   let endpoint_url: String =
       env::var("ENDPOINT_URL").expect("ENDPOINT_URL not found in .env file.");
   let access_key_id: String =
       env::var("ACCESS_KEY_ID").expect("ACCESS_KEY_ID not found in .env file.");
   let secret_access_key: String =
      env::var("SECRET_ACCESS_KEY").expect("SECRET_ACCESS_KEY not found in .env file.");
   let region: String = env::var("REGION").expect("REGION not found in .env file.");

   let object: cf_r2_sdk::operator::Operator = Builder::new()
       .set_bucket_name(bucket_name)
       .set_access_key_id(access_key_id)
       .set_secret_access_key(secret_access_key)
       .set_endpoint(endpoint_url)
       .set_region(region)
       .create_client();

   let _ = object
       .upload_binary("sample.txt", "test/plain", b"Hello, World!", None)
       .await;

   // delete file
   object
       .delete("sample.txt")
       .await?;
   Ok(())
}
```

# 終わりに

本当は cloudflare-r2-rs に pull request するべきなのでしょうが、私の Rust に対する理解度が不足しており、cloudflare-r2-rs のコードリーディングに失敗したという裏話があります。最終的に、Rust の crate の作成方法を学習できて、とてもいい機会になりました！