ブラウザからS3に直接ファイルをアップロード vue.js+VueDropzone

# はじめに
前回までで
[Node.jsのLambdaでPre-Signed URLを発行する](https://qiita.com/gungungggun/items/66d0128436e0dcb176b2)
[Pre-Signed URLでS3に直接ファイルをアップロードする](https://qiita.com/gungungggun/items/c1dd5da5fced8c044e3c)
をしたし、ブラウザからでも簡単にいけるだろうと思ってたけど、かなりハマった。
僕が一番ハマったのは`signatureVersion: 'v4'`が必要ってところでした。
一応手順をメモしておいたので参考になればと思います。

# S3に直接
S3に直接ファイルをアップロードするにはPre-Signed URLを使うと良いです。
ここまでは前回までで実装できています。
ブラウザからとなると、CORSの設定と、クライアントのjsの実装くらいなのでそこまでハマらないはず。って思いますよね？

# CORSの設定
ブラウザからのアクセスなのでCORSの設定が必要です。
S3のバケット＞アクセス権限＞CORSの設定
で以下のようなxmlを記入しましよう。

![s3cors.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128044/39beb84d-62b2-c27c-a665-401d8d0fb51e.jpeg)


```xml:CORS
<?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の取得
ここがハマったポイントでした。

```js:Lambda
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
<?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の書き方が悪くて、署名がうまくマッチしてないのかなとずっとクライアントの書き方を試行錯誤していたんですが、どうやらそうじゃなかったようですｗ
`signatureVersion: 'v4'`という新しい署名の方法にすればよかったみたいです。修正版のコードは以下（設定するだけ）

```js:Lambda修正版
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](https://rowanwins.github.io/vue-dropzone/docs/)というモジュールを使いました。

```vue:component
<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](https://rowanwins.github.io/vue-dropzone/docs/dist/#/aws-s3-upload)にアップロードするような設定があるのでこれをうまく使えばこっちでもいけるかも。
サンプルコードもありますし→[UploadToAWSS3.vue](https://github.com/rowanwins/vue-dropzone/blob/master/docs/src/pages/UploadToAWSS3.vue)
でもメソッドがPOSTになるっぽいし途中でやめましたｗ

# 参考
大変参考になりました！ありがとうございました。

 * [Node.jsで発行したS3の署名付きURLへアップロードすると403エラーになる](https://qiita.com/segur/items/7746ac352afb5152cf46)
 * [Vue.js + Vue2-DropzoneでS3の署名付きURLへファイルアップロード](https://qiita.com/Ryuichi_Fukuda/items/f88f4eac3c29fe6fcef2)