5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Progate Path コミュニティAdvent Calendar 2023

Day 24

今更だけどCloudflareR2使ってみた

Last updated at Posted at 2023-12-23

はじめに

※ S3タグ付いてるけど、ベースはCloudflareR2で進めていきます!

作るもの

今回はGolangでaws-sdk-go-v2を使ってCloudflareR2の操作をしていく!
(S3ではないけどS3でも同じだと思う)

リポジトリのリンク

対象

  • 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
この部分のR2ってところを押して支払情報 (無料枠を使うならpaypalおすすめ)を入力して
自分の情報を入れたら有効化できます!

新しいバケットを作る

image.png
初めの画面がこれだと思うのでおもむろにバケットを作ってみましょう!
image.png
Bucket nameを適当に入力し、
特にロケーションに拘りがなければAutomaticを選んでCreate!
(ロケーションについて、ロケーションはデータが保存される場所(物理)です。Automaticを選択するとバケット作成リクエストをした最も近い利用可能なリージョンが選択されます)

APIキーとかの準備

作成ができたらAPIキーとかいろいろ設定していこうと思います
R2にリクエストするのに必要な情報は

  • アカウントID (AccountID)
  • アクセスキーID (Access Key Id)
  • アクセスキーシークレット (Access Key Secret)
    なのでそれぞれ確認していきます。

アカウントID

アカウントIDはR2の初めの画面の右上にあります
image.png
この値はエンドポイント(S3 API)を構築するのにつかわれます

アクセスキーID, アクセスキーシークレット

アカウントIDの下にある Manage R2 API Tokensを押して設定していきます
image.png
何も設定してないとなんもないので作ります
image.png
これが、アカウント設定の全貌です
それぞれ説明していきます。

Token name

この設定は、API Tokenの名前です。わかりやすい名前でいいでしょう。

Permissons

この設定は、API Tokenがどのレベルで操作ができるかを設定できます。
構築するアプリケーションやサービス、ユーザによって可能な操作を変更することができます。
今回はいろいろいじっていく予定なのでAdmin Read & Writeにしときましょう

TTL

この設定はトークンの期限です。アプリケーション、サービスの仕様などに合わせて設定するべきでしょう。
今回は、1日限りなので 24hourにしておきます。

Client IP Address Filtering

この設定は、APIトークンを使用できるクライアントIPアドレスを制御できます。
Includeで許可してExcluideで却下する認識でいいはずです

Create押してね(^^)/

image.png
するとこんな感じで出るのでそれぞれコピーしておきましょう

Goで書いていくよ☺

接続クライアントを作る

go get -u github.com/aws/aws-sdk-go-v2
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
}

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

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
}
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")
}

出力結果

2023/12/24 07:52:24 upload succes

R2に確認しに行ってみる...

image.png
あるやん!
次々!

オブジェクトを取得する

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
}
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) // オブジェクトのキー表示
	}

####実行結果!

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)

実行結果

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
しっかりとGopher君のアクセスリンクが生成されアクセスすることができました!

オブジェクトを削除する

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
実行する

2023/12/24 08:10:52 delete success

image.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に変更しちゃった!も簡単に対応できるのでハッカソンからの継続開発に向いてそうですね(^^)/

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

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?