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

1. 總覽

無伺服器遷移站系列的程式碼研究室系列 (自助式實作教學課程) 和相關影片,旨在引導 Google Cloud 無伺服器開發人員透過一或多種遷移作業 (主要用於遷移舊版服務) 逐步翻新應用程式。這麼一來,您的應用程式就能更具可攜性,並提供更多選擇和使用彈性,進而整合及使用更多 Cloud 產品,也更容易升級至較新的語言版本。本系列課程一開始將著重在最早的 Cloud 使用者 (主要是 App Engine (標準環境) 開發人員),但涵蓋其他無伺服器平台,包括 Cloud FunctionsCloud Run 或其他無伺服器平台 (如適用)。

單元 15 程式碼研究室說明如何從單元 0 將 App Engine blobstore 使用情形新增至範例應用程式。接著,您可以在接下來的單元 16 中,將用量遷移至 Cloud Storage

在接下來的研究室中

  • 新增 App Engine Blob API/程式庫的用法
  • 將使用者上傳內容儲存至 blobstore 服務
  • 為遷移至 Cloud Storage 的後續步驟做好準備

軟硬體需求

問卷調查

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

僅供閱讀 閱讀並完成練習

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

新手 中級 還算容易

針對使用 Google Cloud 服務的經驗,您會給予什麼評價?

新手 中級 還算容易

2. 背景

如要從 App Engine Blob API 遷移,請從模組 0 將其使用新增至現有基準 App Engine ndb 應用程式。範例應用程式會顯示使用者最近十次造訪的情形。我們正在修改應用程式,提示使用者上傳與「造訪」對應的成果 (檔案)。如果使用者不想這麼做,會看到「略過」如果有需要 SQL 指令的分析工作負載 則 BigQuery 可能是最佳選擇無論使用者的決定為何,下一頁會顯示與模組 0 (以及本系列中許多其他模組) 的應用程式輸出內容相同的輸出內容。實作 App Engine blobstore 整合功能後,我們可在下一個 (單元 16) 程式碼研究室中將其遷移至 Cloud Storage

App Engine 提供 DjangoJinja2 範本系統的存取權,而且這個範例改變了 (新增 Blob 存取之外) 的一個原因是,在模組 15 中,將從模組 0 中的 Django 切換為 Jinja2 的 Jinja2。翻新 App Engine 應用程式的一大關鍵步驟,是將網路架構從 webapp2 遷移至 Flask。後者會使用 Jinja2 做為預設範本系統,因此開始朝該方向邁進,實作 Jinja2 並繼續使用 webapp2 進行 Blob 存取工作。根據預設,Flask 會使用 Jinja2,因此之後在單元 16 中不必對範本進行任何變更。

3. 設定/事前作業

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

1. 設定專案

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

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

本程式碼研究室的必要條件之一,是擁有正常運作的模組 0 範例應用程式。如果沒有,可從單元 0「開始」取得資料夾 (連結如下)。本程式碼研究室將引導您完成每個步驟,最後程式碼與單元 15「FINISH」單元中顯示的程式碼類似資料夾。

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

$ 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 部分,如下所示:

變更後:

runtime: python27
threadsafe: yes
api_version: 1

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

libraries:
- name: jinja2
  version: latest

無需更新其他設定檔,讓我們前往應用程式檔案。

5. 修改應用程式檔案

進口和 Jinja2 支援

有關 main.py 的第一組變更包括新增 Blob API 的使用,並將 Django 範本取代為 Jinja2。異動內容如下:

  1. os 模組的目的是建立 Django 範本的檔案路徑名稱。由於我們即將切換至 Jinja2 來處理這個問題,因此不再需要使用 os 和 Django 範本轉譯器 google.appengine.ext.webapp.template,因此系統會將其移除。
  2. 匯入 Blob API:google.appengine.ext.blobstore
  3. 匯入在原始 webapp 架構中找到的 blob 處理常式;這些處理常式不適用於 webapp2google.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 中目前的匯入部分替換為下列程式碼片段,以實作上述清單的變更。

變更後:

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 功能納入標準 Web2 要求處理常式類別,因此請在匯入之後,將這個程式碼區塊新增至 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))

新增 Blob 支援

在本系列的其他遷移作業中,範例應用程式的功能或輸出內容保持相同 (或幾乎相同) 的使用者體驗,且不會 (大幅) 改變使用者體驗,但這個範例從常態改為更明顯。我們不會立即登錄新的造訪,然後顯示最近的十次造訪,而是更新應用程式,要求使用者提供檔案構件來註冊造訪。接著,使用者可以上傳對應的檔案或選取 [略過]完全不必上傳任何內容完成這個步驟後,您就會看到「最近的造訪記錄」。

這項變更可讓應用程式使用 Blob 服務,在最近造訪網頁上儲存 (並可於稍後顯示) 該圖片或其他檔案類型。

更新資料模型及實作用途

我們正在儲存更多資料,具體更新資料模型以儲存上傳至 Blob 之檔案的 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)

變更後:

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 會將控制項傳遞至新的 UploadHandler (衍生自 google.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler)。
  3. UploadHandlerPOST 方法執行上傳作業、呼叫 store_visit() 登錄造訪,並觸發 HTTP 307 重新導向,將使用者帶回「/」,其中...
  4. 主要處理常式的 POST 方法會查詢 (透過 fetch_visits()),並顯示最近的造訪記錄。如果使用者選取「略過」未上傳任何檔案,但系統還是會登錄造訪,並加上相同的重新導向。
  5. 最近造訪的多媒體廣告包含一個顯示的新欄位,可以是超連結的「檢視」上傳的檔案是否可供使用或「無」反之。HTML 範本除了可導入上傳表單外,也可在 HTML 範本中實現這些變更 (我們即將推出更多功能)。
  6. 如果使用者點按「觀看」連結,系統就會向新的 ViewBlobHandler 發出 GET 要求 (衍生自 google.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler),在圖片 (如果支援的話) 轉譯檔案、提示下載,或傳回 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)「查看 blob」使用 GET 方法下載處理常式,以及 3) 使用 GETPOST 方法的主要處理常式。進行這些變更,讓應用程式的其餘部分看起來如下所示。

變更後:

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 形式的網址,呼叫上傳處理常式以將檔案傳送至 Blob。
  • UploadHandler.post 會呼叫 blobstore_handlers.BlobstoreUploadHandler.get_uploads。這就是將檔案放入 blob 並傳回不重複永久 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>

導入上述清單中的變更,以組成更新後的範本:

變更後:

<!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。在這個最終移動作業中,您已完成所有必要的變更,可將 Blob 使用案例新增至模組 0 範例應用程式。

(選用) Cloud Storage「強化」

Blob 儲存體最終成 Cloud Storage 本身。這表示 blob 上傳的內容會顯示在 Cloud 控制台中,特別是 Cloud Storage 瀏覽器。問題就在眼前。答案是 App Engine 應用程式的預設 Cloud Storage 值區。這個名稱為 App Engine 應用程式的完整網域名稱 (PROJECT_ID.appspot.com)。這會很方便,因為所有專案 ID 都不重複,對吧?

對範例應用程式所做的更新會將上傳的檔案放進該值區,但開發人員可以選擇更確切的位置。預設值區可透過程式,透過 google.appengine.api.app_identity.get_default_gcs_bucket_name() 存取,如要存取這個值 (例如用前置字串來整理上傳檔案),則需要建立新的匯入作業。舉例來說,您可以按照檔案類型排序:

f61f7a23a1518705.png

舉例來說,如要針對圖片實作這類功能,您會得到以下程式碼,以及部分可檢查檔案類型的程式碼,以便挑選需要的值區名稱:

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.pymain-gcs.py 之間。

256e1ea68241a501.png

6. 摘要/清除

本節總結此程式碼研究室的內容,做法是部署應用程式,確認應用程式是否正常運作,以及任何反映的輸出內容。驗證應用程式後,請執行所有清除步驟,並考慮後續步驟。

部署及驗證應用程式

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

f5b5f9f19d8ae978.png接下來,使用者需要上傳檔案,然後按一下 [提交]或按一下「略過」不要上傳任何內容無論如何,搜尋結果都是最近一次造訪畫面,而且現在使用「檢視」功能擴增連結或「無」。

f5ac6b98ee8a34cb.png

恭喜您完成本程式碼研究室,在單元 0 範例應用程式中加入 App Engine Blob 使用。您的程式碼現在應與 FINISH (Module 15) 資料夾中的內容相符。替代的 main-gcs.py 也存在於該資料夾中。

清除所用資源

一般

如果您現階段已完成設定,建議您停用 App Engine 應用程式,以免產生帳單費用。不過,如果您想測試或進行其他測試,App Engine 平台提供免費配額,而且只要不超出用量限制,就不需支付任何費用。這適用於運算,但相關 App Engine 服務可能也會產生費用,詳情請參閱定價頁面。如果這項遷移作業涉及其他 Cloud 服務,我們會另外計費。無論採用哪種情況,請參閱「本程式碼研究室的專屬」以下章節。

如要完整揭露,部署至 Google Cloud 無伺服器運算平台 (如 App Engine) 會產生少許建構和儲存空間費用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 Blob 服務遷移至使用 Cloud Storage 用戶端程式庫。升級的好處包括能存取更多 Cloud Storage 功能,以及熟悉的用戶端程式庫,該用戶端程式庫可用於 App Engine 以外的應用程式,無論是 Google Cloud、其他雲端環境,甚至是地端部署應用程式皆可使用。如果您覺得自己不需要 Cloud Storage 提供的所有功能,或擔心這項服務對成本的影響,則可繼續使用 App Engine Blob。

除了單元 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

程式碼

不適用

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

程式碼

不適用

線上資源

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

App Engine

Google Cloud

Python

影片

授權

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