LoginSignup
169
152

More than 3 years have passed since last update.

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

Last updated at Posted at 2016-03-10

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

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

スクリーンショット

ダクネス.gif

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

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

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の設定をする必要があります。
以下の記事を参考にさせてもらいました。

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

<?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を使いました。Dropzoneコンポーネントを提供し、簡単にドラック&ドロップの処理を実現できます。
Ajaxのライブラリはaxiosを選択しました。
stateにisUploadingフラグを持ちアップロード中は"アップロードしています"と表示するようにしてます。また、複数画像のアップロードに対応するためPromise.allを使ってすべての画像アップロードが終わりしだいstateを更新し、画像が表示されるようにしています。

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に置いておきます。

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

169
152
3

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
169
152