React(typescript)でAWS S3へFileをUploadする方法

Dropzoneというライブラリを使って、ブラウザ上でドラッグ&ドロップでファイルをアップロード出来るようにしています。

以下が参考にしたサイトです。

1. 公開バケットへのsdkを用いたUpload

アップロード先が公開されているバケットであれば、適切な権限が与えられたaccess_keyとsecret_keyを利用して、aws-sdkを利用してファイルをアップロード出来ます。

SDKを読み込んで、認証情報を追記します。認証情報は.envに記載されているので、その環境変数を読み込みます。

AWSの権限設定とアップロード

import AWS from 'aws-sdk'

require('dotenv').config()

const AWS_ACCESS_KEY = process.env.REACT_APP_AWS_ACCESS_KEY;
const AWS_SECRET_KEY = process.env.REACT_APP_AWS_SECRET_KEY;
const BUCKET = process.env.BUCKET;

AWS.config.update({
  accessKeyId: AWS_ACCESS_KEY,
  secretAccessKey: AWS_SECRET_KEY,
  region: 'ap-northeast-1'
});

const s3 = new AWS.S3();

const upload_image = (file: any, bucket: string) => {

  try {
    const params = {
      Bucket: bucket,
      Key: file.name,
      ContentType: file.type,
      Body: file,
    };

    const res = s3.putObject(params).promise();

  } catch (error) {
    console.log(error);
    return;
  }
}

2. presigned-url(署名付きURL)を用いたUpload

アップロードするファイルが公開できない場合、サーバーに制限時間のある署名付きURLを発行してもらい、そのURLに対して、HTTPのPUTリクエストを実行することでファイルをアップロード出来ます。

署名付きURLの発行

サーバー側の署名付きURLの発行のコードです。私は機械学習系の開発案件に関わることが多く、バックエンド開発はpythonを利用しますので、pythonの例になります。バックエンドのAPIはAWSのLambdaで作っていますので、それっぽくなっています。

server sideのコード

import boto3, json
from botocore.client import Config


def lambda_handler(event, context):
  try:

    body = json.loads(event['body'])

    file_name = body['file_name']
    file_type = body['file_type']

    s3 = boto3.client('s3', config=Config(signature_version='s3v4'))

    file_upload_key = '{0}/upload/{1}'.format('prd', file_name)

    file_upload_url = s3.generate_presigned_url(
      ClientMethod='put_object',
      Params={'Bucket': S3_INBOX_1DAY, 'Key': file_upload_key},
      ExpiresIn=300,
      HttpMethod='PUT')

    body = {
      'presigned_url': str(file_upload_url)
    }

    res = {
      'isBase64Encoded': False,
      'body': json.dumps(body),
    }

    return res

  except Exception as e:

    res = {
      'isBase64Encoded': False,
      'body': json.dumps(''),
    }

    return res

axiosによるHTTP PUTリクエスト

javascriptからHTTPリクエストをバックエンドサーバーに送ります。そのためにHTTPライブラリ、axiosを利用します。使い方は以下の通りです。

uploadImage = (file: any) => {

  const params = {
    file_type: file.type,
    file_name: file.name
  }

  const options = {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
  }

  return axios.post(
    'https://[domain]/api/v1/xxxxxxx', params, options)
    .then(res => {
      const img_options = {
        headers: {
          'Content-Type': file.type
        }
      };
      return axios.put(res.data.file_upload_url, file, img_options);
    })
}

Dropzoneによるレンダリング実装

画面のプリントスクリーン

  render() {
    return (
      <div>
        <h1>File Upload</h1>
        <Dropzone onDrop={this.handleOnDrop} multiple maxSize={8000000} accept="image/*" >
          {({ getRootProps, getInputProps }) => (
            <div
              {...getRootProps({ className: "dropzone" })}
              style={baseStyle}
            >
              <input {...getInputProps()} />

              {this.state.isUploading ?
                <div>ファイルをアップロードしています</div> :
                <div>ファイルをドラックするかクリックしてファイルを選択してください</div>}
            </div>
          )}
        </Dropzone>
      </div>
    );
  }

結論

最後にこれまでのコードをまとめたものを載せておきます。

このコードはSDKを用いてS3にUploadする場合とpresigned-urlを用いてhttpリクエストを利用してファイルをUploadする二つの場合を含んでいます。

import * as React from "react";
import Dropzone from 'react-dropzone';
import axios from 'axios';
import CSS from 'csstype';
import AWS from 'aws-sdk'

require('dotenv').config()

const AWS_ACCESS_KEY = process.env.REACT_APP_AWS_ACCESS_KEY;
const AWS_SECRET_KEY = process.env.REACT_APP_AWS_SECRET_KEY;
const BUCKET = process.env.BUCKET;

AWS.config.update({
  accessKeyId: AWS_ACCESS_KEY,
  secretAccessKey: AWS_SECRET_KEY,
  region: 'ap-northeast-1'
});

const s3 = new AWS.S3();

interface INbdProps {
}

interface INbdState {
  isUploading: boolean;
  images: any[];
}

// SDKによるアップロード
const upload_image = (file: any, bucket: string) => {

  try {
    const params = {
      Bucket: bucket,
      Key: file.name,
      ContentType: file.type,
      Body: file,
    };

    const res = s3.putObject(params).promise();

  } catch (error) {
    console.log(error);
    return;
  }
}

const baseStyle: CSS.Properties = {
  flex: 1,
  display: "flex",
  flexDirection: "column",
  alignItems: "center",
  padding: "20px",
  // borderWidth: 2,
  // borderRadius: 2,
  borderColor: "#eeeeee",
  borderStyle: "dashed",
  backgroundColor: "#fafafa",
  color: "#bdbdbd",
  outline: "none",
  transition: "border .24s ease-in-out"
};


class UFileUpload extends React.Component<INbdProps, INbdState> {

  constructor(props: INbdProps) {
    super(props);
    this.state = {
      isUploading: false,
      images: []
    };
    this.handleOnDrop = this.handleOnDrop.bind(this);
  }


  handleOnDrop(files: any[]) {

    Promise.all(files.map(file => this.uploadImage(file)))
      .then(images => {
        this.setState({
          isUploading: false,
          images: this.state.images.concat(images)
        });
      }).catch(e => console.log(e));
  }

  uploadImage = (file: any) => {

    // public access s3 へのアップロード
    upload_image(file, '[bucket]')

    const params = {
      file_type: file.type,
      file_name: file.name
    }

    const options = {
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    }

    return axios.post(
      'https://[domain]/api/v1/xxxxxxx', params, options)
      .then(res => {
        const img_options = {
          headers: {
            'Content-Type': file.type
          }
        };
        return axios.put(res.data.file_upload_url, file, img_options);
      })
  }

  render() {
    return (
      <div>
        <h1>File Upload</h1>
        <Dropzone onDrop={this.handleOnDrop} multiple maxSize={8000000} accept="image/*" >
          {({ getRootProps, getInputProps }) => (
            <div
              {...getRootProps({ className: "dropzone" })}
              style={baseStyle}
            >
              <input {...getInputProps()} />

              {this.state.isUploading ?
                <div>ファイルをアップロードしています</div> :
                <div>ファイルをドラックするかクリックしてファイルを選択してください</div>}

            </div>
          )}
        </Dropzone>
      </div>
    );
  }
}


export default UFileUpload;