如何使用 App Engine blob (單元 15)

1. 總覽

無伺服器遷移站系列程式碼實驗室 (可自行調整進度的實作教學課程) 和相關影片,旨在協助 Google Cloud 無伺服器開發人員翻新應用程式,引導他們完成一或多項遷移作業,主要是從舊版服務遷移。這樣一來,應用程式的可攜性會更高,您也能享有更多選項和彈性,整合及存取更多元的 Cloud 產品,並更輕鬆地升級至較新的語言版本。雖然本系列文章最初著重於最早的 Cloud 使用者,主要是 App Engine (標準環境) 開發人員,但內容廣泛,也涵蓋其他無伺服器平台,例如 Cloud Functions 和 Cloud Run,或其他適用平台。

本模組 15 程式碼實驗室說明如何將 App Engine blobstore 用途新增至模組 0 的範例應用程式。接著,您就可以在單元 16 中將用量遷移至 Cloud Storage。

在接下來的研究室中

  • 新增 App Engine Blobstore API/程式庫的使用方式
  • 將使用者上傳的內容儲存至 blobstore 服務
  • 準備遷移至 Cloud Storage

軟硬體需求

問卷調查

您會如何使用這個教學課程?

只閱讀內容 閱讀內容並完成練習

您對 Python 的使用體驗評價如何?

新手 中級 精通

您對 Google Cloud 服務的使用體驗滿意度為何?

新手 中級 精通

2. 背景

如要從 App Engine Blobstore API 遷移,請將其用量新增至模組 0 的現有基準 App Engine ndb 應用程式。範例應用程式會向使用者顯示最近十次造訪記錄。我們正在修改應用程式,提示使用者上傳與「造訪」對應的構件 (檔案)。如果使用者不想這麼做,可以選擇「略過」。無論使用者如何決定,下一頁都會顯示與單元 0 (以及本系列許多其他單元) 應用程式相同的輸出內容。實作這項 App Engine blobstore 整合後,我們可以在下一個 (模組 16) 程式碼實驗室中,將其遷移至 Cloud Storage。

App Engine 可讓您存取 Django 和 Jinja2 範本系統,而本範例與其他範例的不同之處 (除了新增 Blobstore 存取權之外),在於它從模組 0 使用 Django 切換至模組 15 使用 Jinja2。翻新 App Engine 應用程式的重要步驟,是將網頁框架從 webapp2 遷移至 Flask。後者使用 Jinja2 做為預設範本系統,因此我們開始朝這個方向移動,實作 Jinja2,同時保留 webapp2 以存取 Blobstore。Flask 預設使用 Jinja2,因此在單元 16 中,您不需要預先變更範本。

3. 設定/事前準備

在進入教學課程的主要部分之前,請先設定專案、取得程式碼,並部署基準應用程式,以便開始使用可運作的程式碼。

1. 設定專案

如果您已部署模組 0 應用程式,建議重複使用相同的專案 (和程式碼)。或者,您也可以建立全新專案,或重複使用其他現有專案。確認專案有有效的帳單帳戶,且已啟用 App Engine。

2. 取得基準範例應用程式

本程式碼實驗室的其中一項先決條件,是具備可運作的模組 0 範例應用程式。如果沒有,可以從模組 0 的「START」資料夾取得 (連結如下)。本程式碼實驗室會逐步引導您完成各個步驟,最後產生的程式碼會與單元 15「FINISH」資料夾中的程式碼類似。

模組 0 的 START 檔案目錄應如下所示:

$ ls
README.md               index.html
app.yaml                main.py

3. (重新) 部署基準應用程式

現在要執行的其餘前置作業步驟:

  1. 重新熟悉 gcloud 指令列工具
  2. 使用 gcloud app deploy 重新部署範例應用程式
  3. 確認應用程式在 App Engine 上順利執行

成功執行上述步驟後,如果網頁應用程式正常運作 (輸出內容與下方類似),就可以在應用程式中加入快取功能。

a7a9d2b80d706a2b.png

4. 更新設定檔

app.yaml

應用程式設定沒有重大變更,但如前所述,我們將從 Django 範本 (預設) 轉換為 Jinja2,因此使用者應指定 App Engine 伺服器上可用的最新版本 Jinja2,方法是將其新增至 app.yaml 的內建第三方程式庫部分。

之前:

runtime: python27
threadsafe: yes
api_version: 1

handlers:
- url: /.*
  script: main.app

編輯 app.yaml 檔案,新增 libraries 區段,如下所示:

AFTER:

runtime: python27
threadsafe: yes
api_version: 1

handlers:
- url: /.*
  script: main.app

libraries:
- name: jinja2
  version: latest

其他設定檔都不需要更新,因此我們繼續處理應用程式檔案。

5. 修改應用程式檔案

匯入和 Jinja2 支援

main.py 的第一組變更包括新增 Blobstore API 的使用,以及將 Django 範本替換為 Jinja2。異動內容如下:

  1. os 模組的用途是建立 Django 範本的檔案路徑名稱。由於我們改用 Jinja2 處理這項作業,因此不再需要使用 os 和 Django 範本轉譯器 google.appengine.ext.webapp.template,因此會將其移除。
  2. 匯入 Blobstore API:google.appengine.ext.blobstore
  3. 匯入原始 webapp 架構中的 Blobstore 處理常式,webapp2 不支援這些處理常式:google.appengine.ext.webapp.blobstore_handlers
  4. 從 webapp2_extras 套件匯入 Jinja2 支援

之前:

import os
import webapp2
from google.appengine.ext import ndb
from google.appengine.ext.webapp import template

請將 main.py 中目前的匯入部分替換為下列程式碼片段,實作上述清單中的變更。

AFTER:

import webapp2
from webapp2_extras import jinja2
from google.appengine.ext import blobstore, ndb
from google.appengine.ext.webapp import blobstore_handlers

匯入後,請新增一些樣板程式碼,以支援 webapp2_extras 文件中定義的 Jinja2。下列程式碼片段會使用 Jinja2 功能包裝標準 webapp2 要求處理常式類別,因此請在匯入後將這個程式碼區塊新增至 main.py:

class BaseHandler(webapp2.RequestHandler):
    'Derived request handler mixing-in Jinja2 support'
    @webapp2.cached_property
    def jinja2(self):
        return jinja2.get_jinja2(app=self.app)

    def render_response(self, _template, **context):
        self.response.write(self.jinja2.render_template(_template, **context))

新增 Blobstore 支援

在本系列的其他遷移作業中,我們保留了範例應用程式的功能或輸出內容,使用者體驗幾乎沒有變化。但這個範例與一般做法截然不同。我們正在更新應用程式,要求使用者提供檔案構件來註冊造訪,而不是立即註冊新的造訪,然後顯示最近的十次造訪。使用者可以上傳對應的檔案,或選取「略過」不予上傳。完成這個步驟後,系統會顯示「最近造訪」頁面。

這項變更可讓應用程式使用 Blobstore 服務,在最近造訪頁面上儲存 (並可能在日後算繪) 該圖片或其他檔案類型。

更新資料模型並實作

我們將儲存更多資料,具體來說,就是更新資料模型,儲存上傳至 Blobstore 的檔案 ID (稱為「BlobKey」),並新增參照,將其儲存在 store_visit() 中。由於這項額外資料會與其他所有資料一起傳回,因此 fetch_visits() 保持不變。

以下是更新前後的對比,其中 file_blob 是ndb.BlobKeyProperty:

之前:

class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)

def store_visit(remote_addr, user_agent):
    'create new Visit entity in Datastore'
    Visit(visitor='{}: {}'.format(remote_addr, user_agent)).put()

def fetch_visits(limit):
    'get most recent visits'
    return Visit.query().order(-Visit.timestamp).fetch(limit)

AFTER:

class Visit(ndb.Model):
    'Visit entity registers visitor IP address & timestamp'
    visitor   = ndb.StringProperty()
    timestamp = ndb.DateTimeProperty(auto_now_add=True)
    file_blob = ndb.BlobKeyProperty()

def store_visit(remote_addr, user_agent, upload_key):
    'create new Visit entity in Datastore'
    Visit(visitor='{}: {}'.format(remote_addr, user_agent),
            file_blob=upload_key).put()

def fetch_visits(limit):
    'get most recent visits'
    return Visit.query().order(-Visit.timestamp).fetch(limit)

以下是目前為止所做變更的圖示:

2270783776759f7f.png

支援上傳檔案

功能上最重大的變更,是支援檔案上傳,無論是提示使用者上傳檔案、支援「略過」功能,還是轉譯與造訪對應的檔案。這些都是整體考量的一部分。如要支援檔案上傳功能,請進行下列變更:

  1. 主要處理常式 GET 要求不再擷取最近的造訪記錄來顯示。而是會提示使用者上傳。
  2. 當使用者提交要上傳的檔案或略過該程序時,表單中的 POST 會將控制權傳遞給衍生自 google.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler 的新 UploadHandler。
  3. UploadHandler 的 POST 方法會執行上傳作業、呼叫 store_visit() 註冊造訪,並觸發 HTTP 307 重新導向,將使用者送回「/」,其中...
  4. 主要處理常式的 POST 方法會查詢 (透過 fetch_visits()) 並顯示最近的造訪記錄。如果使用者選取「略過」,系統不會上傳任何檔案,但仍會記錄造訪,並重新導向至相同網址。
  5. 最近造訪記錄會向使用者顯示新欄位,如果可以上傳檔案,則會顯示「檢視」超連結,否則會顯示「無」。這些變更會反映在 HTML 範本中,並加入上傳表單 (稍後會詳細說明)。
  6. 如果使用者點選任何造訪記錄的「查看」連結,且該記錄包含上傳的影片,系統會向新的 ViewBlobHandler (衍生自 google.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler) 提出 GET 要求。如果檔案是圖片,系統會轉譯圖片 (如果瀏覽器支援);如果不是圖片,系統會提示下載;如果找不到檔案,則會傳回 HTTP 404 錯誤。
  7. 除了新的處理常式類別和將流量傳送至這些類別的新路徑外,主要處理常式還需要新的 POST 方法,才能接收上述 307 重新導向。

在這些更新之前,模組 0 應用程式只有一個主要處理常式,其中包含 GET 方法和單一路徑:

之前:

class MainHandler(webapp2.RequestHandler):
    'main application (GET) handler'
    def get(self):
        store_visit(self.request.remote_addr, self.request.user_agent)
        visits = fetch_visits(10)
        tmpl = os.path.join(os.path.dirname(__file__), 'index.html')
        self.response.out.write(template.render(tmpl, {'visits': visits}))

app = webapp2.WSGIApplication([
    ('/', MainHandler),
], debug=True)

實作這些更新後,現在有三個處理常式:1) 具有 POST 方法的上傳處理常式、2) 具有 GET 方法的「查看 blob」下載處理常式,以及 3) 具有 GET 和 POST 方法的主要處理常式。完成這些變更後,應用程式其餘部分應如下所示。

AFTER:

class UploadHandler(blobstore_handlers.BlobstoreUploadHandler):
    'Upload blob (POST) handler'
    def post(self):
        uploads = self.get_uploads()
        blob_id = uploads[0].key() if uploads else None
        store_visit(self.request.remote_addr, self.request.user_agent, blob_id)
        self.redirect('/', code=307)

class ViewBlobHandler(blobstore_handlers.BlobstoreDownloadHandler):
    'view uploaded blob (GET) handler'
    def get(self, blob_key):
        self.send_blob(blob_key) if blobstore.get(blob_key) else self.error(404)

class MainHandler(BaseHandler):
    'main application (GET/POST) handler'
    def get(self):
        self.render_response('index.html',
                upload_url=blobstore.create_upload_url('/upload'))

    def post(self):
        visits = fetch_visits(10)
        self.render_response('index.html', visits=visits)

app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/upload', UploadHandler),
    ('/view/([^/]+)?', ViewBlobHandler),
], debug=True)

在剛才新增的程式碼中,有幾個重要呼叫:

  • MainHandler.get 中有對 blobstore.create_upload_url 的呼叫。這項呼叫會產生表單 POST 的網址,並呼叫上傳處理常式,將檔案傳送至 Blobstore。
  • UploadHandler.post 中有對 blobstore_handlers.BlobstoreUploadHandler.get_uploads 的呼叫。這項功能會將檔案放入 Blobstore,並傳回該檔案的專屬永久 ID,也就是 BlobKey。
  • 在 ViewBlobHandler.get 中,使用檔案的 BlobKey 呼叫 blobstore_handlers.BlobstoreDownloadHandler.send 會擷取檔案,並將檔案轉送至使用者的瀏覽器

這些呼叫代表存取應用程式新增功能的大部分。以下是 main.py 第二組和最後一組變更的圖示:

da2960525ac1b90d.png

更新 HTML 範本

主應用程式的部分更新會影響應用程式的使用者介面 (UI),因此網頁範本也需要進行相應變更,實際上需要進行兩項變更:

  1. 檔案上傳表單必須包含 3 個輸入元素:檔案,以及一組分別用於上傳檔案和略過的提交按鈕。
  2. 更新最近造訪輸出,為有對應檔案上傳的造訪新增「查看」連結,否則新增「無」。

之前:

<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<body>

<h1>VisitMe example</h1>
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
    <li>{{ visit.timestamp.ctime }} from {{ visit.visitor }}</li>
{% endfor %}
</ul>

</body>
</html>

請按照上方清單中的變更,更新範本:

AFTER:

<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<body>

<h1>VisitMe example</h1>
{% if upload_url %}

<h3>Welcome... upload a file? (optional)</h3>
<form action="{{ upload_url }}" method="POST" enctype="multipart/form-data">
    <input type="file" name="file"><p></p>
    <input type="submit"> <input type="submit" value="Skip">
</form>

{% else %}

<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
<li>{{ visit.timestamp.ctime() }}
    <i><code>
    {% if visit.file_blob %}
        (<a href="/view/{{ visit.file_blob }}" target="_blank">view</a>)
    {% else %}
        (none)
    {% endif %}
    </code></i>
    from {{ visit.visitor }}
</li>
{% endfor %}
</ul>

{% endif %}

</body>
</html>

下圖說明 index.html 的必要更新:

8583e975f25aa9e7.png

最後一項變更是,Jinja2 偏好將範本放在 templates 資料夾中,因此請建立該資料夾,並將 index.html 移至其中。完成最後這個步驟後,您就完成了所有必要變更,可將 Blobstore 的使用方式新增至模組 0 範例應用程式。

(選用) Cloud Storage「強化」

Blobstore 儲存空間最終演變成 Cloud Storage。也就是說,Blobstore 上傳內容會顯示在 Cloud 控制台中,特別是 Cloud Storage 瀏覽器。問題在於要從何處著手。答案是 App Engine 應用程式的預設 Cloud Storage bucket。名稱是 App Engine 應用程式的完整網域名稱 PROJECT_ID.appspot.com。所有專案 ID 都是不重複的,所以非常方便,對吧?

範例應用程式更新後,會將上傳的檔案放入該 bucket,但開發人員可以選擇更具體的位置。預設 bucket 可透過 google.appengine.api.app_identity.get_default_gcs_bucket_name() 以程式輔助方式存取,如要存取這個值 (例如做為前置字串來整理上傳的檔案),則需要重新匯入。例如,依檔案類型排序:

f61f7a23a1518705.png

舉例來說,如要為圖片實作類似功能,您需要使用類似下方的程式碼,以及檢查檔案類型以選擇所需 bucket 名稱的程式碼:

ROOT_BUCKET = app_identity.get_default_gcs_bucket_name()
IMAGE_BUCKET = '%s/%s' % (ROOT_BUCKET, 'images')

您也會使用 Python 標準程式庫 imghdr 模組等工具,驗證上傳的圖片,確認圖片類型。最後,為防範惡意人士,建議您限制上傳檔案的大小。

假設上述步驟都已完成,如何更新應用程式,支援指定上傳檔案的儲存位置?關鍵在於調整 MainHandler.get 中對 blobstore.create_upload_url 的呼叫,並新增 gs_bucket_name 參數,指定要上傳至 Cloud Storage 的位置,如下所示:

blobstore.create_upload_url('/upload', gs_bucket_name=IMAGE_BUCKET))

這項更新為選用,可指定上傳內容的目的地,因此不屬於存放區中 main.py 檔案的一部分。不過,您可以在存放區中查看名為 main-gcs.py 的替代方案。main-gcs.py 中的程式碼不會使用獨立的值區「資料夾」,而是將上傳內容儲存在「根」值區 (PROJECT_ID.appspot.com),就像 main.py 一樣,但會提供您需要的架構,以便您按照本節的提示,將範例衍生為更多內容。下圖說明 main.py 和 main-gcs.py 之間的「差異」。

256e1ea68241a501.png

6. 摘要/清除

本節將部署應用程式,並確認應用程式和任何反映的輸出內容是否正常運作,為這個程式碼實驗室畫下句點。應用程式驗證完成後,請執行所有清理步驟,並思考後續步驟。

部署及驗證應用程式

使用 gcloud app deploy 重新部署應用程式,並確認應用程式運作正常,使用者體驗 (UX) 與單元 0 應用程式不同。應用程式現在有兩個不同的畫面,第一個是造訪檔案上傳表單提示:

f5b5f9f19d8ae978.png使用者可以上傳檔案並點選「提交」,或點選「略過」而不上傳任何內容。無論是哪一種情況,結果都是最近一次造訪畫面,現在造訪時間戳記和訪客資訊之間會顯示「查看」連結或「無」:

f5ac6b98ee8a34cb.png

恭喜您完成本程式碼實驗室,成功在模組 0 範例應用程式中加入 App Engine Blobstore。您的程式碼現在應與 FINISH (模組 15) 資料夾中的程式碼相同。替代 main-gcs.py 也會出現在該資料夾中。

清除所用資源

一般

如果目前已完成,建議您停用 App Engine 應用程式,以免產生費用。不過,如果您想進一步測試或實驗,App Engine 平台提供免費配額,只要不超過該用量層級,就不會產生費用。這是運算費用,但相關 App Engine 服務可能也會產生費用,詳情請參閱定價頁面。如果這項遷移作業涉及其他 Cloud 服務,則會另外計費。無論是哪一種情況,請視需要參閱下方的「本程式碼實驗室專屬」一節。

為求資訊公開,部署至 App Engine 等 Google Cloud 無伺服器運算平台,會產生少許建構和儲存費用。Cloud Build 和 Cloud Storage 都有自己的免費配額。儲存該圖片會消耗部分配額。不過,您所在的地區可能不提供這類免費方案,因此請留意儲存空間用量,盡可能降低潛在費用。您應查看的特定 Cloud Storage「資料夾」包括:

  • console.cloud.google.com/storage/browser/LOC.artifacts.PROJECT_ID.appspot.com/containers/images
  • console.cloud.google.com/storage/browser/staging.PROJECT_ID.appspot.com
  • 上述儲存空間連結取決於您的 PROJECT_ID 和 *LOC*位置,例如,如果您的應用程式託管於美國,則為「us」。

另一方面,如果您不打算繼續使用這個應用程式或其他相關遷移程式碼實驗室,並想完全刪除所有內容,請關閉專案。

本程式碼實驗室專用

下列服務是本程式碼實驗室專屬。詳情請參閱各項產品的說明文件:

後續步驟

下一個要考慮的邏輯遷移作業會在模組 16 中說明,向開發人員展示如何從 App Engine Blobstore 服務遷移至使用 Cloud Storage 用戶端程式庫。升級的好處包括可存取更多 Cloud Storage 功能,以及熟悉適用於 App Engine 以外應用程式的用戶端程式庫,無論是在 Google Cloud、其他雲端,甚至是在地端部署環境。如果您認為不需要 Cloud Storage 的所有功能,或擔心費用受到影響,可以繼續使用 App Engine Blobstore。

除了模組 16 之外,還有許多其他可能的遷移作業,例如 Cloud NDB 和 Cloud Datastore、Cloud Tasks 或 Cloud Memorystore。此外,您也可以跨產品遷移至 Cloud Run 和 Cloud Functions。遷移存放區提供所有程式碼範例、所有可用程式碼實驗室和影片的連結,以及應考慮的遷移作業和任何相關遷移「順序」的指引。

7. 其他資源

程式碼實驗室問題/意見回饋

如果您發現這個程式碼實驗室有任何問題,請先搜尋問題,再提出回報。搜尋及建立新問題的連結:

遷移資源

下表提供模組 0 (START) 和模組 15 (FINISH) 的存放區資料夾連結。您也可以從所有 App Engine 程式碼實驗室遷移作業的存放區存取這些檔案,並複製或下載 ZIP 檔案。

Codelab

Python 2

Python 3

單元 0

code

不適用

單元 15 (本程式碼研究室)

code

不適用

線上資源

以下是與本教學課程相關的線上資源:

App Engine

Google Cloud

Python

影片

授權

這項內容採用的授權為創用 CC 姓名標示 2.0 通用授權。