2019年4月12日 星期五

[GAE] 使用Google Cloud Storage存取圖片

  我們透過簡單的範例應用來學習如何存取在Google Cloud Storage中的圖片,另外我們也將利用 google app engine 提供的image service來動態調整圖片大小。

  首先來看看是怎樣的應用吧!

  使用者上傳名字與圖片,然後在/show頁面可以看到所有上傳的東西,大概就是這樣,那在這個範例裡我們用Google Cloud Storage來存取圖片,另外這個圖片是有經過處理,會將長邊縮放成150px另一邊則等比例縮放。

第一張圖是來自我滿喜歡的動畫
第二張圖是我隨便畫的XD


範例專案結構:

/app
    - app.yaml
    - appengine_config.py
    - router.py
    - main.py
    - /templates
        - upload.html
        - show.html
    - /models
        - users.py
        - init.py
    - /lib
        - /cloudstorage


  要使用google cloud storage需下載其client library,方法請看這篇[GAE] Google Cloud Storage Client Library入門,照著安裝步驟做,專案裡就會有 appengine_config.py和/lib/cloudstorage,也就可以正常使用google cloud storage api了。

Code:

1. /templates/upload.html:上傳用的表單頁面
<html>
<head></head>
    <meta charset="UTF-8">
    <title>Upload</title>
<body>
    <form action="" enctype="multipart/form-data" method="post">
        <div class="form-group">
            <label for="user_name">Name:</label>
            <input type="text" id="user_name" name="user_name">
        </div>
        <div class="form-group">
            <label for="uploaded_file">Photo:</label>
            <input type="file" id="uploaded_file" name="uploaded_file">
        </div>
        <div class="form-group">
            <button type="submit">Send</button>
        </div>
    </form>
</body>
</html>

  * 要注意的是在表單中要加入 enctype="multipart/form-data" 。在傳送POST request時我們需要將body的資料進行encode編碼,HTML提供三種encoding方法,分別是:
    1. application/x-www-form-urlencoded (default)
    2. multipart/form-data
    3. text/plain
  一般若有 <input type="file"> 需要上傳資料時都會使用multipart/form-data,其他則使用application/x-www-form-urlencoded,至於text/plain是完全不介意使用。
(詳細可以參考這:https://stackoverflow.com/questions/4526273/what-does-enctype-multipart-form-data-mean)

  * form action="" 是指送出表單後將連到原網頁。

2. /models/users.py:建立Users Model,相當於建立一個Users Table,內有name、photo_key與photo_url三個欄位,其中最重要的是photo_url,這個URL是要給Client端讀取圖片用的。
from google.appengine.ext import ndb

class Users(ndb.Model):
    name = ndb.StringProperty(required=True)
    photo_key = ndb.BlobKeyProperty()
    photo_url = ndb.StringProperty()

    @classmethod
    def add_new_user(cls, user_name, photo_key, photo_url):
        user_key = cls(
            name=user_name,
            photo_key=photo_key,
            photo_url=photo_url
        ).put()
        return user_key

* 我們的Users Model有name、photo_key和photo_url三個儲存屬性,photo_url是用來存圖片的公開的URL用,這個URL是透過blobkey來取得,取得的過程請看下面main.py裡的save_image()函式。
  (後來想一想其實可以不用存blobkey,因為我們只要知道圖片的公開URL就可以讀取了,有了blobkey是在之後我們可以使用Blobstore API對這個檔案進行各種修改)

3. router.py:設定url映射,/upload對應到main.py中的UploadHandler,/show則對應到main.py中的ShowPhotoHandler。
import webapp2
from webapp2 import Route

app = webapp2.WSGIApplication([
    Route('/upload', handler='main.UploadHandler'),
    Route('/show', handler='main.ShowPhotoHandler'),
], debug=True)

4. main.py:handler處理。
  Client送出POST表單,我們會執行UploadHandler 裡的 def post(self)的內容,利用self.request.POST['id'] 取出HTTP POST來的資料,在 save_image() 這個函式裡,我們設定好cloud_storage_path決定上傳的檔案要送到Google Cloud Storage的哪裡,之後就像平常的寫入檔案一樣,首先open()再write()寫入。

  那我們要如何取得這個圖片的公開URL呢?

  其實我們在open()檔案時是可以設定檔案的權限,在open()內加入options={'x-goog-acl': 'public-read'}就可以公開檔案,而Public URL會是這樣:https://storage.googleapis.com/[BucketName]/[FileName],但在這裡我們不這麼做。

  而是要使用Google App Engine提供的 Image Service!
  Image Service是種REST API,若不懂 Rest API 就先暫時想成是一種可以讓使用者透過URL連結得到服務這樣的設計。 (建議還是去Google一下就是了)
  總之想使用這個 Image Service 我們就要得到它 serving URL,那我們可以透過BlobKey來取得,具體做法是這樣:
blobstore_key = blobstore.create_gs_key(cloud_storage_path)
blobstore_key = blobstore.BlobKey(blobstore_key) 
serving_url = images.get_serving_url(blobstore_key)
main.py完整code:
# -*- coding: utf-8 -*-
import webapp2
import os
import logging
import jinja2

import cloudstorage as gcs
from google.appengine.api import app_identity
from google.appengine.api import blobstore
from google.appengine.api import images
from google.appengine.api import users

from models.users import Users

template_dir = os.path.join(os.path.dirname(__file__), 'template')
jinja_enviroment = jinja2.Environment(
            loader=jinja2.FileSystemLoader(template_dir)
            )

class UploadHandler(webapp2.RequestHandler):
    def get(self):
        template = jinja_enviroment.get_template('upload.html')
        self.response.write(template.render())

    def post(self):
        user_photo = self.request.POST['uploaded_file'] # 得到POST上傳的file資料
        user_name = self.request.POST['user_name']

        saved_photo = self.save_image(user_photo)
        Users.add_new_user(
            user_name = user_name, 
            photo_key = saved_photo['blobstore_key'], 
            photo_url = saved_photo['serving_url']
        )
        self.response.write('Upload Success')
        
    @classmethod
    def save_image(cls, photo):
        img_title = photo.filename
        img_content = photo.file.read()
        img_type = photo.type

        bucket_name = os.environ.get('BUCKET_NAME',
                               app_identity.get_default_gcs_bucket_name())
        cloud_storage_path = '/gs/'+bucket_name+'/%s' %(img_title)
        blobstore_key = blobstore.create_gs_key(cloud_storage_path) # 使用Blobstore API提供的create_gs_key()得到blobkey字串

        with gcs.open(cloud_storage_path[3:], 'w', content_type=img_type) as f:  # 要注意寫入cloud storage的路徑沒有含 '/gs'
            f.write(img_content)

        blobstore_key = blobstore.BlobKey(blobstore_key)  #將字串轉成BlobKey object
        serving_url = images.get_serving_url(blobstore_key) # 使用images.get_serving_url()從blobkey得到圖片的image serving URL

        return {
            'serving_url': serving_url,
            'blobstore_key': blobstore_key
        }


class ShowPhotoHandler(webapp2.RequestHandler):
    def get(self):
        users = Users.query() # 讀取在Google Cloud Datastore中的所有Users
        template_context = {
            "users": users,
        }
        template = jinja_enviroment.get_template('show.html')
        self.response.write(template.render(template_context))

* App Engine免費提供的 Image Service 功能可以讓我們不需要存複數張圖片,就可以動態調整圖片大小與旋轉方向等,而且要做的只有修改一點 url 就可以,像下面show.html中我們只是在image serving url後加上 '=s150' 就可以讓圖片的最長邊縮成150px,另一邊則等比例縮小,真的很方便。
(詳細image serving url用法請參考: https://cloud.google.com/appengine/docs/standard/python/images/ )

5. /templates/show.html:圖片顯示頁面
<html>
<head></head>
    <meta charset="UTF-8">
    <title>Show</title>
<body>
    <table border=1>
        <tr>
            <td bgcolor=pink align="center">Name</td>
            <td align="center" width="150px">Photo</td>
        </tr>
    {% for user in users%}
        <tr>
            <td bgcolor=pink align="center">{{user.name}}</td>
            <td width="150px" height="150px" align="center"><img src={{user.photo_url + '=s150'}}></td>
        </tr>
    {% endfor %}
    </table>
</body>
</html>


Local測試:
  輸入 dev_appserver.py app.yaml --clear_datastore=yes
  (clear_datastore=yes 幫助我們清除local的資料庫裡之前的資料)

  在瀏覽器輸入 http://localhost:8080/upload 可以看到上傳表單,送出後會看到Upload Success的字串。
  在到 http://localhost:8080/show 看結果。

  最後不妨到 http://localhost:8000/datastore 看一下資料庫,因為目前local端沒有cloud storage,但若你存在default bucket的話應該是能在blobstore viewer那看到資料。


參考資料:
    1. 雲端網頁程式設計:Google App Engine使用Python
    2. Learn to build scalable web applications (特別是Section07)



沒有留言:

張貼留言