iT邦幫忙

2021 iThome 鐵人賽

1
AI & Data

觀賞魚辨識的YOLO全餐系列 第 32

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/axios@0.2.1/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 31 - 使用 Amazon API Gateway 上傳圖片到 S3
下一篇
Day 33 - 實作 S3 驅動 Lambda 函數進行鏡像
系列文
觀賞魚辨識的YOLO全餐38

尚未有邦友留言

立即登入留言