🤒

GoでS3にPostPolicyで画像をアップロードしてみる

2024/10/22に公開

S3へファイルをアップロードする方法はいくつかありますが、PostPolicyによるアップロードってご存知でしたか。
お恥ずかしながら私は知りませんでした。

今回はGoでPostPolicyを試してみたので、その記録を残したいと思います。

そもそもなんでPost Policyを使うのか

アップロードするファイルに制限をかけたいから

以上です。

そこに至るまでのかんたんな経緯

  1. 元々はサーバから画像をアップロードする仕組みを考えていた
  2. サーバからS3へファイルアップロードをするとその間で帯域を食ってお金がかかりそうだった
  3. 十分に短いExpiresを設定したPresignedURLを発行していフロントから画像アップロードをしたらいいかと考えた
  4. それによりサーバでファイルを受け取ったときにかけていた制限を書ける場所がなくなってしまった
  5. フロントからアップするけど制限はかけたい
  6. PresignedURLにそれっぽい機能が見つけられない
  7. Bucket Policyというのを調べるがアクセスコントロールは出来そうだが、アップロードの制限は見つけられない
  8. 調べてたらPostPolicyを見つける (参考: カミナシさんのブログ)

こんな感じです。
「見つけられない」と書いてある部分は文字通り僕が見つけられなかったので、「できない」わけじゃないかもしれません。
もしやり方を知ってる方がいたら教えてほしいです。

最終的な実装

具体的な実装を以下に記載していきます。
前述したカミナシさんのブログを参考にしている部分があるので、一部コードが同じ部分があります。ご了承ください。

またコードが長いので、要所要所については後で補足しようとおもいます。
上から読んでも理解しづらい可能性が高いので、補足部分から見ていただいたほうが情報を取りやすいかもしれません。

バックエンド

// core/domain/repository
package repository

type SignedFileRepository interface {
	GetSignedFileRequest(context.Context, model.StorageFile) (*model.SignedFileRequest, error)
}
package repositoryimpl

var _ repository.SignedFileRepository = (*SignedFileRepository)(nil)
var service = "s3"

type policy struct {
	Expiration string `json:"expiration"`
	Conditions []any  `json:"conditions"`
}

type encodedPolicy struct {
	Base64Policy string
	SignedPolicy string
}

type SignedFileRepository struct {
	handler *myS3.Handler
}

func NewSignedFileRepository(
	handler *myS3.Handler,
) *SignedFileRepository {
	return &SignedFileRepository{
		handler: handler,
	}
}

func (r *SignedFileRepository) GetSignedFileRequest(ctx context.Context, file model.StorageFile) (*model.SignedFileRequest, error) {
	credentials, err := r.handler.Config().Credentials.Retrieve(ctx)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	now := localtime.Now()

	// 順番を担保して処理しないとハッシュ値が毎回変わってテスト不可になるため順番を担保させている
	params := []map[string]string{
		{"bucket": file.Bucket()},
		{"acl": "private"},
		{"X-Amz-Algorithm": "AWS4-HMAC-SHA256"},
		{"X-Amz-Credential": strings.Join([]string{credentials.AccessKeyID, now.Format("20060102"), r.handler.Config().Region, service, "aws4_request"}, "/")},
		{"X-Amz-Date": now.Format(localtime.ISO8601)},
	}

	policy := policy{
		Expiration: now.Add(1 * time.Hour).Format("2006-01-02T15:04:05Z"),
		Conditions: mergeCondition(
			[]any{
				[]string{"eq", "$key", file.Key()},
				[]string{"starts-with", "$Content-Type", "image/"},
				[]any{"content-length-range", 0, 10 * 1024 * 1024}, // 0-10MB
			},
			params,
		),
	}

	signingKey := createSigningKey(r.handler.Config().Region, credentials, now)
	encodedPolicy, err := sign(policy, signingKey)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	awsOptions := r.handler.Client().Options()
	endpoint, err := awsOptions.EndpointResolverV2.ResolveEndpoint(ctx, bindEndpointParams(awsOptions, file.Bucket()))
	if err != nil {
		return nil, errors.WithStack(err)
	}

	// Policy自体がrequest時のパラメータに必要なため追加
	params = append(params, map[string]string{"Policy": encodedPolicy.Base64Policy})

	return convert.ToP(model.NewSignedFileRequest(
		endpoint.URI.String(),
		encodedPolicy.SignedPolicy,
		paramsToMap(params),
	)), nil
}

func paramsToMap(params []map[string]string) map[string]string {
	tmp := make(map[string]string)
	for _, param := range params {
		for k, v := range param {
			tmp[k] = v
		}
	}
	return tmp
}

func mergeCondition(arr []any, params []map[string]string) []any {
	for _, v := range params {
		arr = append(arr, v)
	}

	return arr
}

func sign(policy policy, signingKey []byte) (*encodedPolicy, error) {
	policyJSON, err := json.Marshal(policy)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	base := base64.StdEncoding.EncodeToString(policyJSON)

	signature := hash(signingKey, base)
	return &encodedPolicy{
		Base64Policy: base,
		SignedPolicy: fmt.Sprintf("%x", signature),
	}, nil
}

func createSigningKey(region string, credentials aws.Credentials, date time.Time) []byte {
	// DateKey = HMAC-SHA256("AWS4" + "<SecretAccessKey>", "<yyyymmdd>")
	dateKey := hash([]byte("AWS4"+credentials.SecretAccessKey), date.Format("20060102"))
	// DateRegionKey = HMAC-SHA256(DateKey, "<aws-region>")
	dateRegionKey := hash(dateKey, region)
	// DateRegionServiceKey = HMAC-SHA256(DateRegionKey, "<aws-service>")
	dateRegionServiceKey := hash(dateRegionKey, service)
	// SigningKey = HMAC-SHA256(DateRegionServiceKey, "aws4_request")
	signingKey := hash(dateRegionServiceKey, "aws4_request")
	return signingKey
}

func hash(secretKey []byte, message string) []byte {
	key := []byte(secretKey)
	h := hmac.New(sha256.New, key)
	h.Write([]byte(message))
	hashed := h.Sum(nil)
	return hashed
}

// INFO: 以下のコードはaws sdk go v2から必要な部分だけ引用
func bindEndpointParams(options s3.Options, bucket string) s3.EndpointParameters {
	params := s3.EndpointParameters{}

	params.Region = bindRegion(options.Region)
	params.UseFIPS = aws.Bool(options.EndpointOptions.UseFIPSEndpoint == aws.FIPSEndpointStateEnabled)
	params.UseDualStack = aws.Bool(options.EndpointOptions.UseDualStackEndpoint == aws.DualStackEndpointStateEnabled)
	params.Endpoint = options.BaseEndpoint
	params.ForcePathStyle = aws.Bool(options.UsePathStyle)
	params.Accelerate = aws.Bool(options.UseAccelerate)
	params.DisableMultiRegionAccessPoints = aws.Bool(options.DisableMultiRegionAccessPoints)
	params.UseArnRegion = aws.Bool(options.UseARNRegion)

	params.DisableS3ExpressSessionAuth = options.DisableS3ExpressSessionAuth
	params.Bucket = &bucket

	return params
}

func bindRegion(region string) *string {
	if region == "" {
		return nil
	}
	return aws.String(mapFIPSRegion(region))
}

func mapFIPSRegion(region string) string {
	const fipsInfix = "-fips-"
	const fipsPrefix = "fips-"
	const fipsSuffix = "-fips"

	if strings.Contains(region, fipsInfix) ||
		strings.Contains(region, fipsPrefix) ||
		strings.Contains(region, fipsSuffix) {
		region = strings.ReplaceAll(region, fipsInfix, "-")
		region = strings.ReplaceAll(region, fipsPrefix, "")
		region = strings.ReplaceAll(region, fipsSuffix, "")
	}

	return region
}
package model

type SignedFileRequest struct {
	url       string
	signature string
	params    map[string]string
}

func NewSignedFileRequest(
	url string,
	signature string,
	params map[string]string,
) SignedFileRequest {
	return SignedFileRequest{
		url:       url,
		signature: signature,
		params:    params,
	}
}

func (m SignedFileRequest) URL() string               { return m.url }
func (m SignedFileRequest) Signature() string         { return m.signature }
func (m SignedFileRequest) Params() map[string]string { return m.params }
package s3

var client *s3.Client
var c aws.Config
var once sync.Once

type Handler struct {
	client *s3.Client
	cfg    aws.Config
}

func NewS3Client(ctx context.Context, cfg config.AWS) (*Handler, error) {
	once.Do(func() {
		var err error
		var opts []func(*awsConfig.LoadOptions) error
		if cfg.S3Endpoint != "" {
			opts = append(opts, awsConfig.WithSharedConfigProfile("localstack"))
		}

		c, err = awsConfig.LoadDefaultConfig(ctx, opts...)
		if err != nil {
			panic(err)
		}
		client = s3.NewFromConfig(c, func(o *s3.Options) {
			o.Region = cfg.Region
			o.EndpointResolverV2 = &resolverV2{
				cfg: cfg,
			}
		})
	})

	return &Handler{
		client: client,
		cfg:    c,
	}, nil
}

func (h *Handler) Client() *s3.Client {
	return h.client
}

func (h *Handler) Config() aws.Config {
	return h.cfg
}

type resolverV2 struct {
	cfg config.AWS
}

func (r *resolverV2) ResolveEndpoint(
	ctx context.Context,
	params s3.EndpointParameters,
) (smithyendpoints.Endpoint, error) {
	if r.cfg.S3Endpoint != "" {
		params.Endpoint = aws.String(r.cfg.S3Endpoint)
		params.ForcePathStyle = aws.Bool(true)
	}

	// delegate back to the default v2 resolver otherwise
	return s3.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, params)
}

フロントエンド

export const ImageUploadUrlFetcher = {
  getImageUploadUrl: async (): Promise<PageImageUploadUrlResponse> => {
    const res = await fetch(APIRoute.PAGE_IMAGE_UPLOAD_URL.URL, {
      method: APIRoute.PAGE_IMAGE_UPLOAD_URL.METHOD,
      credentials: "include",
      cache: "no-cache",
    });

    return v.parse(PageImageUploadUrlResponseSchema, await res.json());
  },
  put: async (data: PageImageUploadUrlResponse, file: File): Promise<void> => {
    const body = new FormData();
    body.append("key", data.key);
    body.append("Content-Type", file.type);
    body.append("X-Amz-Signature", data.signature);
    data.params.forEach((v, k) => {
      body.append(k, v);
    });
    // file以後はfieldが無視されるので末尾
    body.append("file", file);

    await fetch(data.url, {
      method: "POST",
      body: body,
      mode: "no-cors",
    });
  },
};
import * as v from "valibot";
import { ObjectToMapSchema } from "../definition/image";

export const PageImageUploadUrlResponseSchema = v.object({
  url: v.string(),
  key: v.string(),
  signature: v.string(),
  params: ObjectToMapSchema,
});

export const ObjectToMapSchema = v.pipe(
  v.unknown(),
  v.transform((v) => {
    return new Map<string, string>(Object.entries(v as Object));
  }),
);

コードの補足

以下にやろうとしていたことを書きつつ、コードを補足していこうと思います。

PostPolicy生成部分のInterface定義

type SignedFileRepository interface {
	GetSignedFileRequest(context.Context, model.StorageFile) (*model.SignedFileRequest, error)
}

type SignedFileRequest struct {
	url       string
	signature string
	params    map[string]string
}

type StorageFile struct {
	bucket string
	key    string
}

こちらの定義は以下の考えから出てきたものです。

  • AWSというサービスをドメイン層に持ち込みたくない
  • せめてGCPに差し替え可能な定義にしたい

AWSでのPostPolicyを利用した画像アップロードと同等の機能がGCP(GoogleCloud)にも存在するか調べてみたところ、SignedURLというものを見つけました。
名前的にはPresignedURLに近いのですが、機能としてはPostPolicyに近そうでした。

inputにあたるStorageFileは、各サービスのどちらの機能もbucketkey(filename)で保存するファイルを構成するため、それを指定しています。

outputにあたるSignedFileRequestは各サービスで以下が必要になるため、それぞれ定義しています。

  • ファイルの配置先のurl
  • 署名したデータのsignature
  • Signatureの元になった情報群になるparams

paramsの部分だけ少しズルをして具体的なstructではなくmapにして抽象度を上げているのですが、これは仕方ないですね。

ともあれ、これで一応AWSでもGoogleCloudでも対応できる型になったような気がします。
GoogleCloudでは実装試してませんが...

PostPolicy生成の具体的な実装

PostPolicyのパラメータの仕様などは公式のこちらとか、前述のカミナシさんの記事を見たほうがわかりやすいので説明しません。

Policyの作成

では先程のrepositoryの具体的な実装を見ていきます。
まずはPostPolicyで利用するPolicy Documentを生成していきます。

var service = "s3"
func (r *SignedFileRepository) GetSignedFileRequest(ctx context.Context, file model.StorageFile) (*model.SignedFileRequest, error) {
	credentials, err := r.handler.Config().Credentials.Retrieve(ctx)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	now := localtime.Now()

	// 順番を担保して処理しないとハッシュ値が毎回変わってテスト不可になるため順番を担保させている
	params := []map[string]string{
		{"bucket": file.Bucket()},
		{"acl": "private"},
		{"X-Amz-Algorithm": "AWS4-HMAC-SHA256"},
		{"X-Amz-Credential": strings.Join([]string{credentials.AccessKeyID, now.Format("20060102"), r.handler.Config().Region, service, "aws4_request"}, "/")},
		{"X-Amz-Date": now.Format(localtime.ISO8601)},
	}

	policy := policy{
		Expiration: now.Add(1 * time.Hour).Format("2006-01-02T15:04:05Z"),
		Conditions: mergeCondition(
			[]any{
				[]string{"eq", "$key", file.Key()},
				[]string{"starts-with", "$Content-Type", "image/"},
				[]any{"content-length-range", 0, 10 * 1024 * 1024}, // 0-10MB
			},
			params,
		),
	}
	....
}

paramsとConditions周りが少し変なことをしています。特にparams
Conditionが[]anyのため、それをマッチする形でmapを渡そうとすると自然に[]mapの形になります。

また本題とズレてコメントにもあるようにテストコードの話になってしまうのですが、Goのmapは順番を保持せずランダムに値を返します。
Conditionを作成するときにmapからランダムにconditionへ値が渡るようにすると、実行ごとに生成される署名の値が変わってしまいます。
それだとテスト時に都合が悪いんですね。
そのため、ちょっと変な感じがするのですがsliceを経由することで順番を担保するようにしています。

他は愚直にデータを集めてPolicyを作成しているだけです。

ほかに一点あるとすると以下の部分ですかね。

credentials, err := r.handler.Config().Credentials.Retrieve(ctx)

repositoryのコンストラクタ引数にconfigを追加して、それを利用してもいいのですがHandlerのaws-sdk-v2のclientを一度生成しているのに再びCredentialを取得する処理を別に用意するのは冗長なので、以下のようにしてconfigを取り出してそこからretrieveするようにしました。

func NewS3Client(ctx context.Context, cfg config.AWS) (*Handler, error) {
	once.Do(func() {
		...
		c, err = awsConfig.LoadDefaultConfig(ctx, opts...)
		if err != nil {
			panic(err)
		}
		...
	})

	return &Handler{
		client: client,
		cfg:    c,
	}, nil
}
func (h *Handler) Config() aws.Config {
	return h.cfg
}

SigningKeyを作成する

これはカミナシさんの記事にあるのをそのまま拝借しています。

func (r *SignedFileRepository) GetSignedFileRequest(ctx context.Context, file model.StorageFile) (*model.SignedFileRequest, error) {
	...
	signingKey := createSigningKey(r.handler.Config().Region, credentials, now)
	...
}

func createSigningKey(region string, credentials aws.Credentials, date time.Time) []byte {
	// DateKey = HMAC-SHA256("AWS4" + "<SecretAccessKey>", "<yyyymmdd>")
	dateKey := hash([]byte("AWS4"+credentials.SecretAccessKey), date.Format("20060102"))
	// DateRegionKey = HMAC-SHA256(DateKey, "<aws-region>")
	dateRegionKey := hash(dateKey, region)
	// DateRegionServiceKey = HMAC-SHA256(DateRegionKey, "<aws-service>")
	dateRegionServiceKey := hash(dateRegionKey, service)
	// SigningKey = HMAC-SHA256(DateRegionServiceKey, "aws4_request")
	signingKey := hash(dateRegionServiceKey, "aws4_request")
	return signingKey
}

func hash(secretKey []byte, message string) []byte {
	key := []byte(secretKey)
	h := hmac.New(sha256.New, key)
	h.Write([]byte(message))
	hashed := h.Sum(nil)
	return hashed
}

AWS公式だとこのあたりとかのコードになります。
AWSのGithub上のサンプルコードだとここでしょうか。
(でもこれsdk v1かな...?)

PolicyにSigningKeyを利用してSignする

作成した鍵で署名していきます。

func (r *SignedFileRepository) GetSignedFileRequest(ctx context.Context, file model.StorageFile) (*model.SignedFileRequest, error) {
	...
	encodedPolicy, err := sign(policy, signingKey)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	...
}

func sign(policy policy, signingKey []byte) (*encodedPolicy, error) {
	policyJSON, err := json.Marshal(policy)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	base := base64.StdEncoding.EncodeToString(policyJSON)

	signature := hash(signingKey, base)
	return &encodedPolicy{
		Base64Policy: base,
		SignedPolicy: fmt.Sprintf("%x", signature),
	}, nil
}

type encodedPolicy struct {
	Base64Policy string
	SignedPolicy string
}

署名したSignatureの他にBase64のPolicy stringも返却しています。
これはフロントから画像をアップロードする際にこのBase64StringをPolicyとしてS3に渡す必要があるからです。

画像アップロード先のURLを作成する

実はここが一番(無駄に)苦労しました。
ここで必要なのが(たぶん)リージョン、バケットが指定されたURLの生成です。
それだけを愚直に生成するのであればそんなに大変ではないんですが、AWS SDKを利用しているのだから必要なURL解決はSDKに任せたかったんですね。

理由は大きく2つで以下です。

  • EndpointResolverのオプションがいっぱいあるので本当にリージョン、バケットだけ含めたURL作ればいいだけなのかもわからなかった
  • Resolverを利用して環境ごとに接続先を変えたかった

で、問題のコードは以下です。

func (r *SignedFileRepository) GetSignedFileRequest(ctx context.Context, file model.StorageFile) (*model.SignedFileRequest, error) {
	...
	awsOptions := r.handler.Client().Options()
	endpoint, err := awsOptions.EndpointResolverV2.ResolveEndpoint(ctx, bindEndpointParams(awsOptions, file.Bucket()))
	if err != nil {
		return nil, errors.WithStack(err)
	}
	...
}

// INFO: 以下のコードはaws sdk go v2から必要な部分だけ引用
func bindEndpointParams(options s3.Options, bucket string) s3.EndpointParameters {
	params := s3.EndpointParameters{}

	params.Region = bindRegion(options.Region)
	params.UseFIPS = aws.Bool(options.EndpointOptions.UseFIPSEndpoint == aws.FIPSEndpointStateEnabled)
	params.UseDualStack = aws.Bool(options.EndpointOptions.UseDualStackEndpoint == aws.DualStackEndpointStateEnabled)
	params.Endpoint = options.BaseEndpoint
	params.ForcePathStyle = aws.Bool(options.UsePathStyle)
	params.Accelerate = aws.Bool(options.UseAccelerate)
	params.DisableMultiRegionAccessPoints = aws.Bool(options.DisableMultiRegionAccessPoints)
	params.UseArnRegion = aws.Bool(options.UseARNRegion)

	params.DisableS3ExpressSessionAuth = options.DisableS3ExpressSessionAuth
	params.Bucket = &bucket

	return params
}

func bindRegion(region string) *string {
	if region == "" {
		return nil
	}
	return aws.String(mapFIPSRegion(region))
}

func mapFIPSRegion(region string) string {
	const fipsInfix = "-fips-"
	const fipsPrefix = "fips-"
	const fipsSuffix = "-fips"

	if strings.Contains(region, fipsInfix) ||
		strings.Contains(region, fipsPrefix) ||
		strings.Contains(region, fipsSuffix) {
		region = strings.ReplaceAll(region, fipsInfix, "-")
		region = strings.ReplaceAll(region, fipsPrefix, "")
		region = strings.ReplaceAll(region, fipsSuffix, "")
	}

	return region
}

SDKに任せたいと言いつつ、SDKのコードを自分のコード側に落とし込んでいるだけです。
下3つの関数はSDK
これには以下のデメリットがあるので、ご注意ください。

  • SDKのバージョンアップに追従できない

SDKから何かしらの方法で自身がアクセスすべきAWSのURLのベースを取得できないかと探してみたんですけど、見つからなかったんですよね。
これもご存知の方がいたら教えてほしいです。

AWS SDK Go V2のコードは Apache License Version 2.0なので拝借するのは大丈夫なはず...
License

ただこれでrepository側の実装で環境ごとの接続先切り替えを意識しなくてよくなりました。

処理した結果を返す

最後に値を返却する型につめて返します。

func (r *SignedFileRepository) GetSignedFileRequest(ctx context.Context, file model.StorageFile) (*model.SignedFileRequest, error) {
	...
	// Policy自体がrequest時のパラメータに必要なため追加
	params = append(params, map[string]string{"Policy": encodedPolicy.Base64Policy})

	return convert.ToP(model.NewSignedFileRequest(
		endpoint.URI.String(),
		encodedPolicy.SignedPolicy,
		paramsToMap(params),
	)), nil
}

func paramsToMap(params []map[string]string) map[string]string {
	tmp := make(map[string]string)
	for _, param := range params {
		for k, v := range param {
			tmp[k] = v
		}
	}
	return tmp
}

paramsToMapはparamsが[]mapと変な形なのを素直なmapに直してるだけです。

これでPostPolicyで利用する値を生成して返却することが出来ました。

フロントで生成した値を受け取る

ここまで書かなくていいと思ったのですが一応
またフロント周りはまだエラーハンドリングや例外処理に対するガードが全然まともに書かれてないので参考程度に。

getImageUploadUrl: async (): Promise<PageImageUploadUrlResponse> => {
  const res = await fetch(APIRoute.PAGE_IMAGE_UPLOAD_URL.URL, {
    method: APIRoute.PAGE_IMAGE_UPLOAD_URL.METHOD,
    credentials: "include",
    cache: "no-cache",
  });

  return v.parse(PageImageUploadUrlResponseSchema, await res.json());
},

export const PageImageUploadUrlResponseSchema = v.object({
  url: v.string(),
  key: v.string(),
  signature: v.string(),
  params: ObjectToMapSchema,
});

export const ObjectToMapSchema = v.pipe(
  v.unknown(),
  v.transform((v) => {
    return new Map<string, string>(Object.entries(v as Object));
  }),
);

私はvalibotというzodを軽量にしました、みたいなライブラリを利用しています。
バックエンドからmapをjsonにして返すとObjectとして返ってくるので、key, valueが不定な場合扱いづらいです。

ここでは仕方がないので、ObjectToMapを利用して無理やりtransformしてHashMapの形式に落とし込んでいます。

フロントからS3へデータを送る

ここもアホみたいにハマりました。
とりあえずコードは以下です。

put: async (data: PageImageUploadUrlResponse, file: File): Promise<void> => {
  const body = new FormData();
  body.append("key", data.key);
  body.append("Content-Type", file.type);
  body.append("X-Amz-Signature", data.signature);
  data.params.forEach((v, k) => {
    body.append(k, v);
  });
  // file以後はfieldが無視されるので末尾
  body.append("file", file);

  await fetch(data.url, {
    method: "POST",
    body: body,
    mode: "no-cors",
  });
},

コメントにも書いていますが、FormDataに値を詰めるときに順番に注意してください。
file以後のFieldはS3側で無視されてしまいます。

そのためこのコードでfileを先頭に記載するとBadRequestで以下のようなエラーになります。
Bucket POST must contain a field named 'key'. If it is specified, please check the order of the fields.

なんやねんOrderって...

なんかよくわからないですが、たぶん効率的にファイルを配置するためにfileを受け取り次第順次サーバに送るとかしてて、その前にsignatureの検証が終わってる前提みたいな作りになってるんじゃないでしょうか。
それも意味わからないのでホント謎なんですけど、順番変えると動かないのは仕様みたいです。

AWSの公式サイトにも以下のように書いてありました。

<input type="file"   name="file" /> <br />
<!-- The elements after this will be ignored -->

file要素以後のものは無視されるよ〜、とのことです。

疑問

ちなみに疑問に思ったのですが、なんでPostPolicyに関しての記事ってこんなに少ないのでしょうか?
みんなPresignedURLしか使ってないんでしょうか。
アップロードされるファイルに対する制限はどこでかけてるんですかね?

s3に上げるまではある程度自由にしておいて、配置をhookにlambdaを起動してリサイズついでにチェックをしてるんでしょうか。
それとももっと簡単なやり方があるんですかね...

SDK側にもPostPolicy用のコードがなさそうなので、あんまり使われないからなのかなとか思ったり...
jsのsdkにはありそうなんですけどね

誰か教えてくだせぇ。

まとめ

ちょっとコードが長いのと、補足でコード2回書いてるので無駄に長くなってしまいました。

自分でやってみると、何から何までハマってしまって色々考えたり試行錯誤しないと画像アップロード機能一つも作れないのかとつらい気持ちになりました。
ちょっと冗長な記事になってしまったのですが、これも誰かの救いになったらいいなぁと思います。

後誰かスマートなやり方教えてくれないかなぁって気持ちでいつも記事を書いてます。

Discussion