はじめに
※ 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を有効化する
この部分のR2ってところを押して支払情報 (無料枠を使うならpaypalおすすめ)を入力して
自分の情報を入れたら有効化できます!
新しいバケットを作る
初めの画面がこれだと思うのでおもむろにバケットを作ってみましょう!
Bucket nameを適当に入力し、
特にロケーションに拘りがなければAutomaticを選んでCreate!
(ロケーションについて、ロケーションはデータが保存される場所(物理)です。Automaticを選択するとバケット作成リクエストをした最も近い利用可能なリージョンが選択されます)
APIキーとかの準備
作成ができたらAPIキーとかいろいろ設定していこうと思います
R2にリクエストするのに必要な情報は
- アカウントID (AccountID)
- アクセスキーID (Access Key Id)
- アクセスキーシークレット (Access Key Secret)
なのでそれぞれ確認していきます。
アカウントID
アカウントIDはR2の初めの画面の右上にあります
この値はエンドポイント(S3 API)を構築するのにつかわれます
アクセスキーID, アクセスキーシークレット
アカウントIDの下にある Manage R2 API Tokensを押して設定していきます
何も設定してないとなんもないので作ります
これが、アカウント設定の全貌です
それぞれ説明していきます。
Token name
この設定は、API Tokenの名前です。わかりやすい名前でいいでしょう。
Permissons
この設定は、API Tokenがどのレベルで操作ができるかを設定できます。
構築するアプリケーションやサービス、ユーザによって可能な操作を変更することができます。
今回はいろいろいじっていく予定なのでAdmin Read & Writeにしときましょう
TTL
この設定はトークンの期限です。アプリケーション、サービスの仕様などに合わせて設定するべきでしょう。
今回は、1日限りなので 24hourにしておきます。
Client IP Address Filtering
この設定は、APIトークンを使用できるクライアントIPアドレスを制御できます。
Includeで許可してExcluideで却下する認識でいいはずです
Create押してね(^^)/
Goで書いていくよ☺
接続クライアントを作る
go get -u github.com/aws/aws-sdk-go-v2
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
}
オブジェクトをアップロードする
// ファイルから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
}
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に確認しに行ってみる...
オブジェクトを取得する
// 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
}
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
オブジェクトの署名付きリンクを発行する
// オブジェクトの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-(省略
リンクにアクセスしてみた!
しっかりとGopher君のアクセスリンクが生成されアクセスすることができました!
オブジェクトを削除する
指定したキーのオブジェクトを削除する
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
}
if err := r2.DeleteObject(context.Background(), "simple.png"); err != nil {
log.Fatalf("r2 delete object error :%v\n", err)
}
log.Println("delete success")
実行してみる
2023/12/24 08:10:52 delete success
ちゃんと削除されてます!
これにはGohper君もにっこりです!
つまずきポイントカモ?
もし、実行したときに "not found, ResolveEndpointV2"
みたいなエラーが出たときは aws-sdk-go-v2のバージョンを上げましょう。
古いバージョンではエラーが起こるようです
https://github.com/aws/aws-sdk-go-v2/issues/2370
参考までに
まとめ
どうでしたか?aws-sdk-go-v2を使用して書けるため、Cloudflareでミニマルスタート => S3に変更しちゃった!も簡単に対応できるのでハッカソンからの継続開発に向いてそうですね(^^)/
無料で扱えるので皆さんぜひ使ってみてね!!!!