はじめに
前回までで
Node.jsのLambdaでPre-Signed URLを発行する
Pre-Signed URLでS3に直接ファイルをアップロードする
をしたし、ブラウザからでも簡単にいけるだろうと思ってたけど、かなりハマった。
僕が一番ハマったのはsignatureVersion: 'v4'
が必要ってところでした。
一応手順をメモしておいたので参考になればと思います。
S3に直接
S3に直接ファイルをアップロードするにはPre-Signed URLを使うと良いです。
ここまでは前回までで実装できています。
ブラウザからとなると、CORSの設定と、クライアントのjsの実装くらいなのでそこまでハマらないはず。って思いますよね?
CORSの設定
ブラウザからのアクセスなのでCORSの設定が必要です。
S3のバケット>アクセス権限>CORSの設定
で以下のようなxmlを記入しましよう。
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>PUT</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
Pre-Signed URLの取得
ここがハマったポイントでした。
const AWS = require('aws-sdk');
const s3 = new AWS.S3()
const params = {
Bucket: '<バケット名>',
Key: '<ファイルパス>',
Expires: 100 // 期限(秒)
}
async function getPresignedUrl(){
return new Promise((resolve,reject)=>{
s3.getSignedUrl('putObject', params, (err, url) => {
if (err) {
reject(err)
} else {
resolve(url)
}
})
})
}
exports.handler = async (event) => {
const url = await getPresignedUrl()
return {
url: url
}
}
上記のコードでurlを取得して、それでブラウザからアップロードすると403のステータスコードでSignatureDoesNotMatch
という以下のようなエラーが返ってきます。
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
<AWSAccessKeyId>xxx</AWSAccessKeyId>
<StringToSign>xxx</StringToSign>
<SignatureProvided>xxx</SignatureProvided>
<StringToSignBytes>xxx</StringToSignBytes>
<RequestId>xxx</RequestId>
<HostId>xxx</HostId>
</Error>
クライアントのjsの書き方が悪くて、署名がうまくマッチしてないのかなとずっとクライアントの書き方を試行錯誤していたんですが、どうやらそうじゃなかったようですw
signatureVersion: 'v4'
という新しい署名の方法にすればよかったみたいです。修正版のコードは以下(設定するだけ)
const AWS = require('aws-sdk');
const s3 = new AWS.S3({
signatureVersion: 'v4',
});
const params = {
Bucket: '<バケット名>',
Key: '<ファイルパス>',
Expires: 100 // 期限(秒)
}
async function getPresignedUrl(){
return new Promise((resolve,reject)=>{
s3.getSignedUrl('putObject', params, (err, url) => {
if (err) {
reject(err)
} else {
resolve(url)
}
})
})
}
exports.handler = async (event) => {
const url = await getPresignedUrl()
return {
url: url
}
}
このコードを実行すると、前とは少し違う形でurlが返ってきます。
フロント
今回はvue.jsでVueDropzoneというモジュールを使いました。
<template>
<div>
<vue-dropzone
ref="myVueDropzone"
id="dropzone"
:options="dropzoneOptions"
@vdropzone-sending="sending"
method="PUT"
>
</vue-dropzone>
<textarea v-model="signurl" placeholder="Signed URL" class="signedurl"></textarea>
<button @click="uploadFile">Upload</button>
</div>
</template>
<script>
import vue2Dropzone from 'vue2-dropzone'
import 'vue2-dropzone/dist/vue2Dropzone.min.css'
export default {
components: {
vueDropzone: vue2Dropzone
},
data() {
return {
signurl: '',
dropzoneOptions: {
url: 'http://localhost',
method: 'PUT',
autoProcessQueue: false,
thumbnailWidth: 150,
maxFilesize: 500
}
}
},
methods: {
sending(file, xhr) {
const _send = xhr.send
xhr.send = () => {
_send.call(xhr, file)
}
},
uploadFile() {
this.$refs.myVueDropzone.dropzone.options.url = this.signurl
this.$refs.myVueDropzone.processQueue()
}
}
}
</script>
<style>
.signedurl {
margin-top: 30px;
border: 1px solid #ccc;
width: 97%;
height: 200px;
padding: 10px;
}
</style>
今回はPre-Signed URLをLambdaで発行してそのURLをコピーしてtextareaに貼っ付ける形で実装してます。
本来であればここをAPI化してアップロードする前にPre-Singed URLを取得するようにすれば良いと思います。
dropzoneはデフォルトでファイルを指定した瞬間にアップロードしてしまうので、autoProcessQueue: false
を入れておくとアップロードを止めることが出来ます。
アップロードするにはthis.$refs.myVueDropzone.processQueue()
を呼んでやると良いようです。
なのでこれをする前にアップロード先のURLをPre-Singed URLに変更してあげましょう。
あとは、PUTメソッドで送ることと、XHRオブジェクトのBodyにFileオブジェクトをセットすることがポイントです。
これでうまくファイルがS3にアップロードできました。
500MBくらいの動画を試しにアップロードしてみましたが、これもかなり早くアップロードすることができました!
ちなみに
vue-dropzoneにAWS S3にアップロードするような設定があるのでこれをうまく使えばこっちでもいけるかも。
サンプルコードもありますし→UploadToAWSS3.vue
でもメソッドがPOSTになるっぽいし途中でやめましたw
参考
大変参考になりました!ありがとうございました。