s3に画像をアップロードするために、一度サーバを通すのはリソースの無駄なのでやめておきたいですよね。
今回は、ブラウザから複数画像を直接アップロードし、それをReactを用いて描画するところまでやりたいと思います。
コードはすべてgithubに置いてあります。
スクリーンショット
aws-sdkを使って署名付きURLを発行
ブラウザに直接アップロードするには、一度サーバに署名付きURLを発行してもらう必要があります。検索するとブラウザ側にアクセスキーやシークレットキーを直接配置している例も見かけましたが、危険なのでやめた方がいいです。署名付きURLですが、aws-sdkを利用することでこの処理を自分で書かずに20行程度で実装可能です。
以下が、s3の署名付きURLを発行するサーバ側のコードです。アクセスキーなどは自分の環境と置き換えてください。
AWSのアクセスキーの管理については、以下の記事が参考になりました。
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を更新し、画像が表示されるようにしています。
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')
);
一応サーバ側のコードの全体も載せておきます。
'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にプルリクエストお願いします。