【React】ブラウザからドラック＆ドロップでaws s3に画像を直接アップロードする

s3に画像をアップロードするために、一度サーバを通すのはリソースの無駄なのでやめておきたいですよね。
今回は、ブラウザから複数画像を直接アップロードし、それをReactを用いて描画するところまでやりたいと思います。

コードはすべてgithubに置いてあります。

- [akameco/react-s3-uploader-sample](https://github.com/akameco/react-s3-uploader-sample)

## スクリーンショット

![ダクネス.gif](https://raw.githubusercontent.com/akameco/react-s3-uploader-sample/master/media/screen.gif)

## aws-sdkを使って署名付きURLを発行

ブラウザに直接アップロードするには、一度サーバに署名付きURLを発行してもらう必要があります。検索するとブラウザ側にアクセスキーやシークレットキーを直接配置している例も見かけましたが、危険なのでやめた方がいいです。署名付きURLですが、aws-sdkを利用することでこの処理を自分で書かずに20行程度で実装可能です。
以下が、s3の署名付きURLを発行するサーバ側のコードです。アクセスキーなどは自分の環境と置き換えてください。
AWSのアクセスキーの管理については、以下の記事が参考になりました。

- [AWSアカウント作ったらこれだけはやっとけ！IAMユーザーとAuthyを使ったMFAで2段階認証 - Qiita](http://qiita.com/yoshidashingo/items/cd206daca0596659b440)

```index.js
const aws = require('aws-sdk');
const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY;
const AWS_SECRET_KEY = process.env.AWS_SECRET_KEY;
const BUCKET = process.env.BUCKET;

aws.config.update({
  accessKeyId: AWS_ACCESS_KEY,
  secretAccessKey: AWS_SECRET_KEY
});

function upload(file) {
  const s3 = new aws.S3();
  const params = {
    Bucket: BUCKET,
    Key: file.filename,
    Expires: 60,
    ContentType: file.filetype
  };

  return new Promise((resolve, reject) => {
    s3.getSignedUrl('putObject', params, (err, url) => {
      if (err) {
        reject(err);
      }
      resolve(url);
    });
  });
}
```

## CORSオリジン対策

上記だけでは、CORSオリジン制限に引っかかりアップロードができないので、AWS側でCROSの設定をする必要があります。
以下の記事を参考にさせてもらいました。

- [S3へ直接ファイルをアップロードする。Node.js版 - Qiita](http://qiita.com/ms2sato/items/f73a02f2ac14361247c3)

今回の設定は以下のようにしました。

```xml
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>Authorization</AllowedHeader>
    </CORSRule>
    <CORSRule>
        <AllowedOrigin>http://localhost:3000</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>
```

## React + react-dropzone

Reactを使ったフロント側のコードは以下です。
ドラック＆ドロップの処理は[react-dropzone](https://github.com/okonet/react-dropzone)を使いました。Dropzoneコンポーネントを提供し、簡単にドラック＆ドロップの処理を実現できます。
Ajaxのライブラリは[axios](https://github.com/mzabriskie/axios)を選択しました。
stateにisUploadingフラグを持ちアップロード中は"アップロードしています"と表示するようにしてます。また、複数画像のアップロードに対応するためPromise.allを使ってすべての画像アップロードが終わりしだいstateを更新し、画像が表示されるようにしています。

```jsx:index.js
import React, {Component} from 'react';
import {render} from 'react-dom';
import Dropzone from 'react-dropzone';
import axios from 'axios';

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

  handleOnDrop(files) {
    this.setState({isUploading: true});

    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) {
    return axios.get('/upload', {
      params: {
        filename: file.name,
        filetype: file.type
      }
    }).then(res => {
      const options = {
        headers: {
          'Content-Type': file.type
        }
      };
      return axios.put(res.data.url, file, options);
    }).then(res => {
      const {name} = res.config.data;
      return {
        name,
        isUploading: true,
        url: `https://[バケット名を入れてください].s3.amazonaws.com/${file.name}`
      };
    });
  }

  render() {
    return (
      <div style={{width: 960, margin: '20px auto'}}>
        <h1>React S3 Image Uploader Sample</h1>
        <Dropzone onDrop={this.handleOnDrop} accept="image/*">
          <div>画像をドラックまたはクリック</div>
        </Dropzone>
        {this.state.isUploading ?
          <div>ファイルをアップロードしています</div> :
          <div>ここに画像をドラックまたはクリック</div>}
        {this.state.images.length > 0 &&
          <div style={{margin: 30}}>
            {this.state.images.map(({name, url}) =>
              <img key={name} src={url} style={{width: 200, height: 200}}/>)}
          </div>}
      </div>
    );
  }
}

render(
  <App/>,
  document.querySelector('#main')
);
```

一応サーバ側のコードの全体も載せておきます。

```main.js
'use strict';
const Express = require('express');
const app = new Express();
const port = 3000;

app.use(Express.static('public'));

app.get('/', (req, res) => {
  res.sendFile(`${__dirname}/index.html`);
});

app.get('/upload', (req, res) => {
  upload(req.query).then(url => {
    res.json({url: url});
  }).catch(e => {
    console.log(e);
  });
});

app.listen(port, error => {
  if (error) {
    console.error(error);
  } else {
    console.info('listen: ', port);
  }
});

const aws = require('aws-sdk');
const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY;
const AWS_SECRET_KEY = process.env.AWS_SECRET_KEY;
const BUCKET = process.env.BUCKET;

aws.config.update({
  accessKeyId: AWS_ACCESS_KEY,
  secretAccessKey: AWS_SECRET_KEY
});

function upload(file) {
  const s3 = new aws.S3();
  const params = {
    Bucket: BUCKET,
    Key: file.filename,
    Expires: 60,
    ContentType: file.filetype
  };

  return new Promise((resolve, reject) => {
    s3.getSignedUrl('putObject', params, (err, url) => {
      if (err) {
        reject(err);
      }
      resolve(url);
    });
  });
}
```

これまでのコードはgithubに置いておきます。

- [akameco/react-s3-uploader-sample](https://github.com/akameco/react-s3-uploader-sample)

訂正やよりよい方法などあればぜひコメントやgithubにプルリクエストお願いします。