Day 32 - 透过手机呼叫 Amazon API Gateway 上传图片到 S3

Day 32 - 透过手机呼叫 Amazon API Gateway 上传图片到 S3

Day 31 - 使用 Amazon API Gateway 上传图片到 S3 演示了如何透过 API Gateway 直接上传一个图片到 S3,但如果要让手机也可以上传图片的话,那必须让这个 API Gateway 所实作的 REST API 可以有跨预存取 (CORS) 的功能,这篇文章的目的为:

  1. 打开跨预存取 (CORS) 的功能。
  2. 使用 curl 指令验证跨预存取的功能。
  3. 如何针对 API Gateway 除错,使用 Cloudwatch Logs。
  4. 实作一个网页进行跨预存取 (CORS) 的操作。

持续 Day 31 - 使用 Amazon API Gateway 上传图片到 S3 的操作,在 {object} 资源上,点击 操作 选择 启用 CORS 进入设定画面,只要点击 启用 CORS 并取代现有的 CORS 标题 按钮,即可,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211006/20129510jXZ3FXHFB8.png
图 1、启用跨预存取 (CORS) 的功能

弹出一个确认视窗,直接点击 是,取代现有数值 即可,如下图所示。主要是会增加一个 OPTIONS 的方法,并在该方法内添加 Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin 等回应标头,以及在 PUT 方法中,添加 Access-Control-Allow-Origin 回应标头,原理部分可以参阅 Day 09 - Amazon Linux 2 上解决跨来源资源共用 (CORS) 与开机自动启动 uwsgi 这篇文章。

https://ithelp.ithome.com.tw/upload/images/20211006/20129510I2EAKQ3l9Q.png
图 2、启用跨预存取 (CORS) 的确认画面

使用 curl 指令来验证 OPTIONS 方法的正确性,指令如下。

curl -v -X OPTIONS https://q56fxsgl10.execute-api.ap-southeast-1.amazonaws.com/v1/yehfishbucket/123.jpg

从下图来看,送出去的请求与回应,回应部分与预期的结果不同,是错误码 500,这表示服务器内发生错误,这是一个很麻烦的事情,因为这是在云端的托管服务,要如何除错?

https://ithelp.ithome.com.tw/upload/images/20211006/2012951053LtCeGKxq.png
图 3、curl 指令验证 OPTIONS 方法

CloudWatch 是云端开发最主要的除错工作,可以看到任何的运算结果,但预设都是不开放的,因为开放记录要占用空间与算力,所以都是要收费的,开发过程中是不得不打开这个功能,於是在左侧的功能导览列中,选择 阶段,并点击 v1,在右手边的主要画面里,选择 日志/追踪 页签,找到 CloudWatch 设定,勾选 启用 CloudWatch 日志,并在 日志等级 中,选择 INFO,勾选 记录完整请求/回应资料,最後点击 储存变更 按钮,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211006/201295100gJ6LD7HF3.png
图 4、API Gateway 启用 CloudWatch 日志功能

打开 CloudWatch 控制台,在左边功能导览列中,选择 日志群组,可以在右边主画面中找到对应的日志群组,前缀词就是 API-Gateway-Execution-Logs,而最後内容可以在 叫用 URL 中 URL 的前面的编号中找到,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211006/20129510MsUd0IhvxS.png
图 5、CloudWatch 日志画面

接着记得在重新执行一次 curl 指令,根据时间找到最接近的一次记录时间,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211006/20129510jG1setebPq.png
图 6、进入特定的日志群组,根据时间锁定要观察的 CloudWatch 日志

观看整个 API Gateway 的呼叫过程,建议从得到 OPTIONS 请求开始,发现错误的是倒数第三行,可以点击倒三角形图示,会显示详细内容,如下所示:

(24f5f845-ca32-40ad-bdd7-5f0b70adcf11) Execution failed due to configuration error: Unable to transform request

根据字面意思似乎是转换内容格式出错,因为 OPTIONS 只是用来预检验用 (preflight) 的请求,并没有内容 (BODY),所以正常来说是不需要进行内容编码的,所以猜测是编码设定出错。

https://ithelp.ithome.com.tw/upload/images/20211006/20129510b50jAkzEiH.png
图 7、CloudWatch 日志的详细讯息

进入 API Gateway 的 设定页面,找到二进位媒体类型,将原先的 */* 改成 image/*,也就是只针对请求标题 Content-Type 中值设定为 image 的内容,才进行二进位媒体类型编码,设定完成後,记得点击 储存变更 按钮,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211006/20129510PVa3KvUk2c.png
图 8、Amazon API Gateway 中针对二进位媒体类型的设定

务必要在重新部署後,再执行一次 curl 的检验,此时可以发现回应码变成 200 的成功回应,且也有出现 Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin 等回应标头,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211006/20129510ZhhWM8teFg.png
图 9、curl 的检验结果画面

补充说明一下,要如何调整 OPTIONS 的回应表头,回到方法的整合画面中,可以看到在整合请求中是透过 MOCK ,一个模拟的整合请求,也就是不会接到任何服务,预估先前的错误应该是在方法请求转到整合请求中出错的。接着点击 整合回应 ,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211006/201295106s53AnacQG.png
图 10、OPTIONS 的整合画面

如果在上面的请求中都没有出错的话,正常应该就是回传状态码 200,在整合回应中,指定标题映射,既然是映射,表示真正的配置不在这里,而是在方法回应中设定回应标头的名称,而在这里指定回应的映射值,以下两图表明两者之间的关系,如果要新增回应标头,需要在方法回应中新增,然而,标头内的值,则是在整合回应中指定。

https://ithelp.ithome.com.tw/upload/images/20211006/20129510VfDxNIBvof.png
图 11、OPTIONS 的整合回应

https://ithelp.ithome.com.tw/upload/images/20211006/20129510ULyolIoDaC.png
图 12、接着选择 OPTIONS 的方法回应

https://ithelp.ithome.com.tw/upload/images/20211006/20129510JdsfpEe6ZO.png
图 13、OPTIONS 的方法回应设定画面

确认可以进行跨预存取後,直接使用网页来进行测试,下图中的网址栏中可以显示出这是以本地的方式开启这个档案,并打开开发者工具,以观察程序运作情形。以下为范例程序,注意的是 API_ENDPOINT 这个变数要放的内容除了部署的 URL 外,还要加上储存贮体的名称,而上传的档名,会自动抓取,所以不用事先输入。

<!DOCTYPE html>
<html>
  <head>
  	<meta charset="utf-8"/>
    <title>Upload file to S3</title>
    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
  </head>
  <body>
    <div id="app">
      <h1>S3 Uploader Test</h1>
  
      <div v-if="!image">
        <h2>Select an image</h2>
        <input type="file" @change="onFileChange">
      </div>
      <div v-else>
        <img :src="image" />
        <button v-if="!uploadURL" @click="removeImage">Remove image</button>
        <button v-if="!uploadURL" @click="uploadImage">Upload image</button>
      </div>
      <h2 v-if="uploadURL">Success! Image uploaded to bucket.</h2>
    </div>
  
    <script>
      const MAX_IMAGE_SIZE = 10000000

      /* ENTER YOUR ENDPOINT HERE */

      const API_ENDPOINT = 'API Gateway的存取点' // e.g. https://q56fxsglxx.execute-api.ap-southeast-1.amazonaws.com/v1/xxxfishbucket/ [API URL+bucket]

			uploadFile=''
      new Vue({
        el: "#app",
        data: {
          image: '',
          uploadURL: ''
        },
        methods: {
          onFileChange (e) {
            let files = e.target.files || e.dataTransfer.files
            if (!files.length) return
            for( attr in files[0])
            	console.log(attr)
            console.log(files[0].name)
            uploadFile = files[0].name
            this.createImage(files[0])
          },
          createImage (file) {
            // var image = new Image()
            let reader = new FileReader()
            reader.onload = (e) => {
              console.log('length: ', e.target.result.includes('data:image/jpeg'))
              if (!e.target.result.includes('data:image/jpeg')) {
                return alert('Wrong file type - JPG only.')
              }
              if (e.target.result.length > MAX_IMAGE_SIZE) {
                return alert('Image is loo large.')
              }
              this.image = e.target.result
            }
            reader.readAsDataURL(file)
          },
          removeImage: function (e) {
            console.log('Remove clicked')
            this.image = ''
          },
          uploadImage: async function (e) {
            console.log('Upload clicked')
            console.log('Uploading: ', uploadFile)//this.image)
            let binary = atob(this.image.split(',')[1])
            let array = []
            for (var i = 0; i < binary.length; i++) {
              array.push(binary.charCodeAt(i))
            }
            let blobData = new Blob([new Uint8Array(array)], {type: 'image/jpeg'})
            this.uploadURL = API_ENDPOINT + uploadFile
            console.log('Uploading to: ', this.uploadURL)
            const result = await fetch(this.uploadURL, {
              method: 'PUT',
              body: blobData
            })
            console.log('Result: ', result)
            // Final URL for the user doesn't need the query string params
            //this.uploadURL = uploadURL.split('?')[0]
          }
        }
      })
    </script>
    <style type="text/css">
      body {
        background: #20262E;
        padding: 20px;
        font-family: sans-serif;
      }
      #app {
        background: #fff;
        border-radius: 4px;
        padding: 20px;
        transition: all 0.2s;
        text-align: center;
      }
      #logo {
        width: 100px;
      }
      h2 {
        font-weight: bold;
        margin-bottom: 15px;
      }
      h1, h2 {
        font-weight: normal;
        margin-bottom: 15px;
      }
      a {
        color: #42b983;
      }
      img {
        width: 30%;
        margin: auto;
        display: block;
        margin-bottom: 10px;
      }
    </style>
  </body>
</html>

https://ithelp.ithome.com.tw/upload/images/20211006/201295109SVMo2v2Gk.png
图 14、实作 CORS 的上传网页

最後再检查 S3 确认是否上传成功,如下图所示。

https://ithelp.ithome.com.tw/upload/images/20211006/20129510HP0PZK9kyJ.png
图 15、Amazon API Gateway 管理控制台介面

参考资料


<<:  [Day 23] Facial Landmark

>>:  【学习笔记-JS】处理字串的函式

Re: 新手让网页 act 起来: Day10 - React Hooks 之 useState (1)

前言 React hooks 是在 React 16.8 版本才加进来的功能。那到底什麽是 Hook...

Day23,替你的Gitlab pipeline 添加点搞事

正文 在前面介绍gitlab-ci的pipeline中我仅仅只用到了build stage作为con...

产品设计的孵化过程

在前一篇介绍了需求管理方式後,最终排序好需求的优先级就会带着排序最高的需求进到下一个环节-产品方案设...

Day07:始祖巨人

在学习Java继承的部分时,就想到进击巨人的设定,九大巨人的能力只要被其他人吃掉,能力就会被传承过去...

(Day 29) DevOps Challenges

despite all the benefits we stated yesterday, toa...