今更だけどCloudflareR2使ってみた

## はじめに
※ S3タグ付いてるけど、ベースはCloudflareR2で進めていきます！
### 作るもの
今回はGolangでaws-sdk-go-v2を使ってCloudflareR2の操作をしていく！
(S3ではないけどS3でも同じだと思う)

### リポジトリのリンク
https://github.com/murasame29/cloudflare-go

### 対象
- aws-sdk-goが初めましての人
- Cloudflare R2使ってみたい人
- なんとなくGoが書きたい人
が対象です！

## 特徴軽く
CloudflareR2は手数料とか無しでS3互換のオブジェクトストレージを提供しているサービス
### 無料枠について
|  |無料枠 |課金|
|--|--|--|
|容量|10G/month| + $0.015 per GB/ month|
|クラスA操作| 1M ops/month | +$4.50 per 1M ops|
|クラスB操作| 10M ops/month | +$0.36 per 1M ops|

#### クラスA操作について
クラスA操作でできる操作
- ListBuckets
- PutBucket
- ListObjects
- PutObject
- CopyObject
- CompleteMultipartUpload
- CreateMultipartUpload
- istMultipartUploads
- UploadPart
- UploadPartCopy
- ListParts
- PutBucketEncryption
- PutBucketCors
- PutBucketLifecycleConfiguration

#### クラスB操作について
クラスB操作でできること
- HeadBucket
- HeadObject
- GetObject
- UsageSummary
- GetBucketEncryption
- GetBucketLocation
- GetBucketCors
- GetBucketLifecycleConfiguration

#### それ以外の無料でできること
- DeleteObject
- DeleteBucket
- AbortMultipartUpload

## セットアップ
アカウント作成は割愛！
### R2を有効化する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632123/d64cdf1b-f529-d422-f884-5a450c86f6e6.png)
この部分のR2ってところを押して支払情報 (無料枠を使うならpaypalおすすめ)を入力して
自分の情報を入れたら有効化できます！

### 新しいバケットを作る
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632123/2f456b72-83c0-7818-10fe-69da3cdeb097.png)
初めの画面がこれだと思うのでおもむろにバケットを作ってみましょう！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632123/2aea884c-2d2f-dc60-d22d-b0eaec02cfc0.png)
Bucket nameを適当に入力し、
特にロケーションに拘りがなければAutomaticを選んでCreate！
(ロケーションについて、ロケーションはデータが保存される場所(物理)です。Automaticを選択するとバケット作成リクエストをした最も近い利用可能なリージョンが選択されます)

### APIキーとかの準備
作成ができたらAPIキーとかいろいろ設定していこうと思います
R2にリクエストするのに必要な情報は
- アカウントID (AccountID)
- アクセスキーID (Access Key Id)
- アクセスキーシークレット (Access Key Secret)
なのでそれぞれ確認していきます。

#### アカウントID
アカウントIDはR2の初めの画面の右上にあります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632123/f36db5f2-e455-cb96-bfa5-c08d7623553e.png)
この値はエンドポイント(S3 API)を構築するのにつかわれます

#### アクセスキーID, アクセスキーシークレット
アカウントIDの下にある Manage R2 API Tokensを押して設定していきます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632123/661ee451-c059-dfee-81c1-c0efde8ecc06.png)
何も設定してないとなんもないので作ります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632123/a9ee715e-4577-ba7e-b865-d23808168619.png)
これが、アカウント設定の全貌です
それぞれ説明していきます。
##### Token name
この設定は、API Tokenの名前です。わかりやすい名前でいいでしょう。
##### Permissons 
この設定は、API Tokenがどのレベルで操作ができるかを設定できます。
構築するアプリケーションやサービス、ユーザによって可能な操作を変更することができます。
今回はいろいろいじっていく予定なのでAdmin Read & Writeにしときましょう

##### TTL
この設定はトークンの期限です。アプリケーション、サービスの仕様などに合わせて設定するべきでしょう。
今回は、１日限りなので 24hourにしておきます。
##### Client IP Address Filtering
この設定は、APIトークンを使用できるクライアントIPアドレスを制御できます。
Includeで許可してExcluideで却下する認識でいいはずです

### Create押してね(^^)/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632123/f032ec2d-e646-5031-adbe-2c5259173395.png)
するとこんな感じで出るのでそれぞれコピーしておきましょう

## Goで書いていくよ☺

### 接続クライアントを作る

```bash
go get -u github.com/aws/aws-sdk-go-v2
```

```go:client.go
package client

import (
	"context"
	"fmt"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/service/s3"
)

type awsConnect struct {
	accountID       string
	endpoint        string
	accessKeyID     string
	accessKeySecret string
}

type Connection interface {
	Connect(ctx context.Context) (*s3.Client, error)
}

func New(
	accountID,
	endpoint,
	accessKeyID,
	accessKeySecret string,
) AwsConnect {
	return &Connection{
		accountID:       accountID,
		endpoint:        endpoint,
		accessKeyID:     accessKeyID,
		accessKeySecret: accessKeySecret,
	}
}
// リゾルバを構築する
func (a *awsConnect) newResolver() aws.EndpointResolverWithOptions {
	return aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
		return aws.Endpoint{
            // awsやリージョンが変更されても扱えるように
			URL:               fmt.Sprintf("https://%s.%s", a.accountID, a.endpoint),
			HostnameImmutable: true,
			Source:            aws.EndpointSourceCustom,
		}, nil
	})
}
// 接続するメソッド。接続情報を返す
func (a *awsConnect) Connect(ctx context.Context) (*s3.Client, error) {
	cfg, err := config.LoadDefaultConfig(
		ctx,
		config.WithEndpointResolverWithOptions(a.newResolver()),
		config.WithCredentialsProvider(
			credentials.NewStaticCredentialsProvider(
				a.accessKeyID,
				a.accessKeySecret,
				"",
			)),
		config.WithRegion("auto"),
	)
	if err != nil {
		return nil, err
	}

	return s3.NewFromConfig(cfg), nil
}
```

### オブジェクトをアップロードする

```go:upload.go
// ファイルからcontent/typeを判定する
func checkContentType(file []byte) string {
	return http.DetectContentType(file)
}

func (r *r2crud) UploadObject(ctx context.Context, file []byte, key string) error {
	_, err := r.client.PutObject(ctx, &s3.PutObjectInput{
		Bucket:        aws.String(r.bucket),
		Key:           aws.String(key),
		Body:          bytes.NewReader(file),
		ContentLength: aws.Int64(int64(len(file))),
		ContentType:   aws.String(checkContentType(file)),
	})
	return err
}
```

```go:main.go
func filToByte(file *os.File) ([]byte, error) {
	fileInfo, err := file.Stat()
	if err != nil {
		return nil, fmt.Errorf("file stat error :%v\n", err)
	}
	fileBytes := make([]byte, fileInfo.Size())
	_, err = file.Read(fileBytes)
	if err != nil {
		return nil, fmt.Errorf("file read error :%v\n", err)
	}

	return fileBytes, nil
}

func main() {
	client, err := client.New(
		AccountID,
		EndPoint,
		AccessKeyID,
		accessKeySecret,
	).Connect(context.TODO())
	if err != nil {
		log.Fatalf("r2 client conneciton error :%v\n", err)
	}

	r2 := r2.NewR2CRUD(Bucket, client, 60)

	// 画像ファイルを開いておく
	file, err := os.OpenFile("sample.png", os.O_RDONLY, 0666)
	if err != nil {
		log.Fatalf("file open error :%v\n", err)
	}

	filedata, err := filToByte(file)
	if err != nil {
		log.Fatalf("file transcate error :%v\n", err)
	}

	// 画像ファイルをアップロード
	if err := r2.UploadObject(context.Background(), filedata, "sample.png"); err != nil {
		log.Fatalf("r2 upload error :%v\n", err)
	}

	log.Println("upload success")
}
```
#### 出力結果
```bash
2023/12/24 07:52:24 upload succes
```

#### R2に確認しに行ってみる... 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632123/00a3f5a5-f626-9e9c-280f-908479313b2d.png)
あるやん！
次々！
### オブジェクトを取得する
```go:read.go
// prefixで指定したフォルダ(空白だとBucket直下)からすべてのファイルのアクセスリンクを取得
func (r *r2crud) ListObjectsURL(ctx context.Context, prefix string) ([]string, error) {
	object, err := r.ListObjects(ctx, prefix)
	if err != nil {
		return nil, err
	}

	var files []string
	for _, obj := range object.Contents {
		accessURL, err := r.PublishPresignedObjectURL(ctx, *obj.Key)
		if err != nil {
			return nil, err
		}
		files = append(files, accessURL)
	}

	return files, nil
}
// keyで指定したオブジェクトの情報を返す
func (r *r2crud) GetObject(ctx context.Context, key string) (*s3.GetObjectOutput, error) {
	return r.client.GetObject(context.Background(), &s3.GetObjectInput{
		Bucket: aws.String(r.bucket),
		Key:    aws.String(key),
	})
}
// prefixで指定した、オブジェクトの情報を返す
func (r *r2crud) ListObjects(ctx context.Context, prefix string) (*s3.ListObjectsV2Output, error) {
	return r.client.ListObjectsV2(context.Background(), &s3.ListObjectsV2Input{
		Bucket: aws.String(r.bucket),
		Prefix: aws.String(prefix),
	})
}
// 指定したキーの署名付きリンクを生成して返す
func (r *r2crud) PublishPresignedObjectURL(ctx context.Context, key string) (string, error) {
	object, err := r.PresignClient.PresignGetObject(ctx, &s3.GetObjectInput{
		Bucket:          aws.String(r.bucket),
		Key:             aws.String(key),
		ResponseExpires: aws.Time(time.Now().Add(r.Config.PresignLinkExpired * time.Hour)),
	})
	if err != nil {
		return "", err
	}

	return object.URL, nil
}
```

```go:main.go
	objects, err := r2.ListObjects(context.Background(), "")
	if err != nil {
		log.Fatalf("r2 list objects error :%v\n", err)
	}

	for i, obj := range objects.Contents {
		log.Printf("%d: %s", i, *obj.Key) // オブジェクトのキー表示
	}

```
####実行結果！
```bash
2023/12/24 08:00:30 0: sample.png
2023/12/24 08:00:30 1: simple.png
```

### オブジェクトの署名付きリンクを発行する

```go main.go

	// オブジェクトのURLを取得
	url, err := r2.PublishPresignedObjectURL(context.Background(), "simple.png")
	if err != nil {
		log.Fatalf("r2 publish presigned object url error :%v\n", err)
	}

	log.Println("sample.png Access URL :", url)

```
#### 実行結果
```bash
2023/12/24 08:00:30 sample.png Access URL : https://f3a8ca6e1c9f1b944048aeb0c52ae135.r2.cloudflarestorage.com/r2-sample/simple.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-(省略
```

#### リンクにアクセスしてみた！
![2023-12-24 08-06-28.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632123/3b399144-5765-e50c-7137-2527ea4dcbd2.gif)
しっかりとGopher君のアクセスリンクが生成されアクセスすることができました！

### オブジェクトを削除する
```go:delete.go
指定したキーのオブジェクトを削除する
func (r *r2crud) DeleteObject(ctx context.Context, key string) error {
	_, err := r.client.DeleteObject(ctx, &s3.DeleteObjectInput{
		Bucket: aws.String(r.bucket),
		Key:    aws.String(key),
	})
	return err
}
```

```go main.go
	if err := r2.DeleteObject(context.Background(), "simple.png"); err != nil {
		log.Fatalf("r2 delete object error :%v\n", err)
	}

	log.Println("delete success")
```

#### 実行してみる
まずは現状のR2の様子
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632123/b012159e-62a9-52a0-91f4-59287109f13a.png)
実行する
```bash
2023/12/24 08:10:52 delete success
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632123/24cce388-42e4-2fbe-29b9-2e9119d79b01.png)
ちゃんと削除されてます！
これにはGohper君もにっこりです！

## つまずきポイントカモ？
もし、実行したときに "**not found, ResolveEndpointV2**"
みたいなエラーが出たときは aws-sdk-go-v2のバージョンを上げましょう。
古いバージョンではエラーが起こるようです
https://github.com/aws/aws-sdk-go-v2/issues/2370
参考までに
## まとめ
どうでしたか？aws-sdk-go-v2を使用して書けるため、Cloudflareでミニマルスタート => S3に変更しちゃった！も簡単に対応できるのでハッカソンからの継続開発に向いてそうですね(^^)/

無料で扱えるので皆さんぜひ使ってみてね！！！！