將 App Engine Blobstore 遷移至 Cloud Storage

本指南說明如何從 App Engine Blobstore 遷移至 Cloud Storage

Cloud Storage 與 App Engine Blobstore 類似,您可以使用 Cloud Storage 提供大型資料物件 (blob),例如影片或圖片檔,並讓使用者上傳大型資料檔案。App Engine Blobstore 只能透過 App Engine 舊版套裝服務存取,但 Cloud Storage 是獨立的 Google Cloud產品,可透過 Cloud 用戶端程式庫存取。Cloud Storage 為應用程式提供更現代化的物件儲存解決方案,並讓您日後能彈性遷移至 Cloud Run 或其他 Google Cloud 應用程式 Google Cloud 代管平台。

如果是 2016 年 11 月後建立的 Google Cloud 專案,Blobstore 會在幕後使用 Cloud Storage 值區。也就是說,將應用程式遷移至 Cloud Storage 時,現有 Cloud Storage 值區中的所有物件和權限都會維持不變。您也可以使用 Cloud Storage 適用的 Cloud 用戶端程式庫,開始存取現有值區。

主要差異和相似之處

Cloud Storage 不支援下列 Blobstore 依附元件和限制:

  • Python 2 適用的 Blobstore API 依附於 webapp。
  • Python 3 適用的 Blobstore API 會使用公用程式類別,以便使用 Blobstore 處理常式
  • 如果是 Blobstore,上傳至 Blobstore 的檔案數量上限為 500 個。您可以在 Cloud Storage 值區中建立的物件數量沒有限制。

Cloud Storage 不支援:

  • Blobstore 處理常式類別
  • Blobstore 物件

Cloud Storage 和 App Engine Blobstore 的相似之處:

  • 可在執行階段環境中讀取及寫入大型資料物件,以及儲存和提供靜態大型資料物件,例如影片、圖片或其他靜態內容。Cloud Storage 的物件大小上限為 5 TiB。
  • 可讓您將物件儲存在 Cloud Storage bucket 中。
  • 提供免費方案。

事前準備

  • 請詳閱並瞭解 Cloud Storage 定價和配額:
  • 現有的 Python 2 或 Python 3 App Engine 應用程式使用 Blobstore。
  • 本指南中的範例會顯示使用 Flask 架構遷移至 Cloud Storage 的應用程式。請注意,遷移至 Cloud Storage 時,您可以使用任何網頁架構,包括繼續使用 webapp2

總覽

從 App Engine Blobstore 遷移至 Cloud Storage 的程序大致包含下列步驟:

  1. 更新設定檔
  2. 更新 Python 應用程式
    • 更新網路框架
    • 匯入及初始化 Cloud Storage
    • 更新 Blobstore 處理常式
    • 選用:如果您使用 Cloud NDB 或 App Engine NDB,請更新資料模型
  3. 測試及部署應用程式

更新設定政策

修改應用程式程式碼,從 Blobstore 遷移至 Cloud Storage 之前,請先更新設定檔,使用 Cloud Storage 程式庫。

  1. 更新 app.yaml 檔案。請按照適用於您 Python 版本的操作說明操作:

    Python 2

    如果是 Python 2 應用程式:

    1. 移除 handlers 區段,以及 libraries 區段中任何不必要的網頁應用程式依附元件。
    2. 如果您使用 Cloud 用戶端程式庫,請新增最新版本的 grpciosetuptools 程式庫。
    3. 新增 ssl 程式庫,因為 Cloud Storage 需要這個程式庫。

    以下是經過變更的 app.yaml 檔案範例:

    runtime: python27
    threadsafe: yes
    api_version: 1
    
    handlers:
    - url: /.*
      script: main.app
    
    libraries:
    - name: grpcio
      version: latest
    - name: setuptools
      version: latest
    - name: ssl
      version: latest
    

    Python 3

    如果是 Python 3 應用程式,請刪除 runtime 元素以外的所有行。 例如:

    runtime: python310 # or another support version
    

    Python 3 執行階段會自動安裝程式庫,因此您不需要指定先前 Python 2 執行階段的內建程式庫。如果 Python 3 應用程式在遷移至 Cloud Storage 時使用其他舊版套裝服務,請保留 app.yaml 檔案。

  2. 更新 requirements.txt 檔案。請按照您使用的 Python 版本,依下列指示操作:

    Python 2

    將 Cloud Storage 適用的 Cloud 用戶端程式庫新增至 requirements.txt 檔案的依附元件清單。

    google-cloud-storage
    

    然後執行 pip install -t lib -r requirements.txt,更新應用程式可用的程式庫清單。

    Python 3

    將 Cloud Storage 的 Cloud 用戶端程式庫新增至 requirements.txt 檔案的依附元件清單。

    google-cloud-storage
    

    在 Python 3 執行階段中部署應用程式時,App Engine 會自動安裝這些依附元件,因此請刪除 lib 資料夾 (如有)。

  3. 如果是 Python 2 應用程式,且應用程式使用內建或複製的程式庫,您必須在 appengine_config.py 檔案中指定這些路徑:

    import pkg_resources
    from google.appengine.ext import vendor
    
    # Set PATH to your libraries folder.
    PATH = 'lib'
    # Add libraries installed in the PATH folder.
    vendor.add(PATH)
    # Add libraries to pkg_resources working set to find the distribution.
    pkg_resources.working_set.add_entry(PATH)
    

更新 Python 應用程式

修改設定檔後,請更新 Python 應用程式。

更新 Python 2 網頁架構

如果 Python 2 應用程式使用 webapp2 框架,建議您從過時的 webapp2 框架遷移。如要瞭解 Python 2 的終止支援日期,請參閱「執行階段支援時間表」。

您可以遷移至其他網路架構,例如 Flask、Django 或 WSGI。由於 Cloud Storage 排除對 webapp2 的依附元件,且不支援 Blobstore 處理常式,因此您可以刪除或取代其他與 webapp 相關的程式庫。

如果您選擇繼續使用 webapp2,請注意本指南中的範例會搭配 Flask 使用 Cloud Storage。

如果您打算使用 Google Cloud 服務 (除了 Cloud Storage 之外),或是想存取最新版本的執行階段,建議將應用程式升級至 Python 3 執行階段。詳情請參閱 Python 2 遷移至 Python 3 的總覽

匯入及初始化 Cloud Storage

更新匯入和初始化行,藉此修改應用程式檔案:

  1. 移除 Blobstore 匯入陳述式,例如:

    import webapp2
    from google.appengine.ext import blobstore
    from google.appengine.ext.webapp import blobstore_handlers
    
  2. 新增 Cloud Storage 和 Google 驗證程式庫的匯入陳述式,如下所示:

    import io
    from flask import (Flask, abort, redirect, render_template,
    request, send_file, url_for)
    from google.cloud import storage
    import google.auth
    

    您需要 Google 驗證程式庫,才能取得 Blobstore 用於 Cloud Storage 的專案 ID。匯入其他程式庫,例如 Cloud NBD (如適用於您的應用程式)。

  3. 為 Cloud Storage 建立新用戶端,並指定 Blobstore 中使用的 bucket。例如:

    gcs_client = storage.Client()
    _, PROJECT_ID = google.auth.default()
    BUCKET = '%s.appspot.com' % PROJECT_ID
    

    如果是 2016 年 11 月之後的專案,Blobstore 會寫入以應用程式網址命名的 Cloud Storage 值區,並採用 PROJECT_ID.appspot.com 格式。 Google Cloud 您可以使用 Google 驗證取得專案 ID,指定用於在 Blobstore 中儲存 Blob 的 Cloud Storage 值區。

更新 Blobstore 處理常式

由於 Cloud Storage 不支援 Blobstore 上傳和下載處理常式,您必須結合 Cloud Storage 功能、io標準程式庫模組、網路框架和 Python 公用程式,才能在 Cloud Storage 中上傳及下載物件 (Blob)。

以下範例說明如何使用 Flask 做為範例網頁框架,更新 Blobstore 處理常式:

  1. 將 Blobstore 上傳處理常式類別替換為 Flask 中的上傳函式。請按照您使用的 Python 版本操作:

    Python 2

    Python 2 中的 Blobstore 處理常式是 webapp2 類別,如下列 Blobstore 範例所示:

    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)
    ...
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    如要使用 Cloud Storage,請按照下列步驟操作:

    1. 將 Webapp 上傳類別替換為 Flask 上傳函式。
    2. 使用以路由裝飾的 Flask POST 方法,取代上傳處理常式和路由。

    更新的程式碼範例

    @app.route('/upload', methods=['POST'])
    def upload():
        'Upload blob (POST) handler'
        fname = None
        upload = request.files.get('file', None)
        if upload:
            fname = secure_filename(upload.filename)
            blob = gcs_client.bucket(BUCKET).blob(fname)
            blob.upload_from_file(upload, content_type=upload.content_type)
        store_visit(request.remote_addr, request.user_agent, fname)
        return redirect(url_for('root'), code=307)
    

    在更新後的 Cloud Storage 程式碼範例中,應用程式現在會依物件名稱 (fname) 識別物件構件,而不是 blob_id。路由也會在應用程式檔案底部發生。

    如要取得上傳的物件,請將 Blobstore 的 get_uploads() 方法替換為 Flask 的 request.files.get() 方法。在 Flask 中,您可以使用 secure_filename() 方法取得沒有路徑字元的檔案名稱 (例如 /),並使用 gcs_client.bucket(BUCKET).blob(fname) 指定值區名稱和物件名稱,藉此識別物件。

    Cloud Storage upload_from_file() 呼叫會執行上傳作業,如更新後的範例所示。

    Python 3

    Python 3 的 Blobstore 上傳處理常式類別是公用程式類別,需要使用 WSGI environ 字典做為輸入參數,如下列 Blobstore 範例所示:

    class UploadHandler(blobstore.BlobstoreUploadHandler):
        'Upload blob (POST) handler'
        def post(self):
            uploads = self.get_uploads(request.environ)
            if uploads:
                blob_id = uploads[0].key()
                store_visit(request.remote_addr, request.user_agent, blob_id)
            return redirect('/', code=307)
    ...
    @app.route('/upload', methods=['POST'])
    def upload():
        """Upload handler called by blobstore when a blob is uploaded in the test."""
        return UploadHandler().post()
    

    如要使用 Cloud Storage,請將 Blobstore 的 get_uploads(request.environ) 方法替換為 Flask 的 request.files.get() 方法。

    更新的程式碼範例

    @app.route('/upload', methods=['POST'])
    def upload():
        'Upload blob (POST) handler'
        fname = None
        upload = request.files.get('file', None)
        if upload:
            fname = secure_filename(upload.filename)
            blob = gcs_client.bucket(BUCKET).blob(fname)
            blob.upload_from_file(upload, content_type=upload.content_type)
        store_visit(request.remote_addr, request.user_agent, fname)
        return redirect(url_for('root'), code=307)
    

    在更新後的 Cloud Storage 程式碼範例中,應用程式現在會依物件名稱 (fname) 識別物件構件,而不是 blob_id。路由也會在應用程式檔案底部發生。

    如要取得上傳的物件,請將 Blobstore 的 get_uploads() 方法替換為 Flask 的 request.files.get() 方法。在 Flask 中,您可以使用 secure_filename() 方法取得沒有路徑字元的檔案名稱 (例如 /),並使用 gcs_client.bucket(BUCKET).blob(fname) 指定值區名稱和物件名稱,藉此識別物件。

    Cloud Storage upload_from_file() 方法會執行上傳作業,如更新後的範例所示。

  2. 將 Blobstore 下載處理常式類別替換為 Flask 中的下載函式。請按照您使用的 Python 版本操作:

    Python 2

    以下下載處理常式範例顯示如何使用 BlobstoreDownloadHandler 類別 (使用 webapp2):

    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)
    ...
    app = webapp2.WSGIApplication([
        ('/', MainHandler),
        ('/upload', UploadHandler),
        ('/view/([^/]+)?', ViewBlobHandler),
    ], debug=True)
    

    如要使用 Cloud Storage,請按照下列步驟操作:

    1. 更新 Blobstore 的 send_blob() 方法,以使用 Cloud Storage 的 download_as_bytes() 方法。
    2. 將路由從 webapp2 變更為 Flask。

    更新的程式碼範例

    @app.route('/view/<path:fname>')
    def view(fname):
        'view uploaded blob (GET) handler'
        blob = gcs_client.bucket(BUCKET).blob(fname)
        try:
            media = blob.download_as_bytes()
        except exceptions.NotFound:
            abort(404)
        return send_file(io.BytesIO(media), mimetype=blob.content_type)
    

    在更新後的 Cloud Storage 程式碼範例中,Flask 會裝飾 Flask 函式中的路徑,並使用 '/view/<path:fname>' 識別物件。Cloud Storage 會依據物件名稱和值區名稱識別 blob 物件,並使用 download_as_bytes() 方法以位元組形式下載物件,而不是使用 Blobstore 的 send_blob 方法。如果找不到構件,應用程式會傳回 HTTP 404 錯誤。

    Python 3

    與上傳處理常式類似,Python 3 的 Blobstore 下載處理常式類別也是公用程式類別,需要使用 WSGI environ 字典做為輸入參數,如下列 Blobstore 範例所示:

    class ViewBlobHandler(blobstore.BlobstoreDownloadHandler):
        'view uploaded blob (GET) handler'
        def get(self, blob_key):
            if not blobstore.get(blob_key):
                return "Photo key not found", 404
            else:
                headers = self.send_blob(request.environ, blob_key)
    
            # Prevent Flask from setting a default content-type.
            # GAE sets it to a guessed type if the header is not set.
            headers['Content-Type'] = None
            return '', headers
    ...
    @app.route('/view/<blob_key>')
    def view_photo(blob_key):
        """View photo given a key."""
        return ViewBlobHandler().get(blob_key)
    

    如要使用 Cloud Storage,請將 Blobstore 的 send_blob(request.environ, blob_key) 替換為 Cloud Storage 的 blob.download_as_bytes() 方法。

    更新的程式碼範例

    @app.route('/view/<path:fname>')
    def view(fname):
        'view uploaded blob (GET) handler'
        blob = gcs_client.bucket(BUCKET).blob(fname)
        try:
            media = blob.download_as_bytes()
        except exceptions.NotFound:
            abort(404)
        return send_file(io.BytesIO(media), mimetype=blob.content_type)
    

    在更新後的 Cloud Storage 程式碼範例中,blob_key 會替換為 fname,而 Flask 會使用 '/view/<path:fname>' 網址識別物件。gcs_client.bucket(BUCKET).blob(fname) 方法用於找出檔案名稱和 bucket 名稱。Cloud Storage 的 download_as_bytes() 方法會以位元組形式下載物件,而非使用 Blobstore 的 send_blob() 方法。

  3. 如果應用程式使用主要處理常式,請在 Flask 中將 MainHandler 類別替換為 root() 函式。請按照適用於您 Python 版本的操作說明操作:

    Python 2

    以下是使用 Blobstore 的 MainHandler 類別的範例:

    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)
    

    如要使用 Cloud Storage,請按照下列步驟操作:

    1. 移除 MainHandler(BaseHandler) 類別,因為 Flask 會為您處理路由。
    2. 使用 Flask 簡化 Blobstore 程式碼。
    3. 最後移除網頁應用程式路徑。

    更新的程式碼範例

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

    Python 3

    如果您使用 Flask,就不會有 MainHandler 類別,但如果使用 Blobstore,則需要更新 Flask 根函式。以下範例使用 blobstore.create_upload_url('/upload') 函式:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = blobstore.create_upload_url('/upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

    如要使用 Cloud Storage,請將 blobstore.create_upload_url('/upload') 函式替換為 Flask 的 url_for() 方法,取得 upload() 函式的網址。

    更新的程式碼範例

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload') # Updated to use url_for
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    

測試及部署應用程式

本機開發伺服器可讓您測試應用程式是否能執行,但您必須部署新版本,才能測試 Cloud Storage,因為所有 Cloud Storage 要求都必須透過網際網路傳送至實際的 Cloud Storage bucket。如要瞭解如何在本機執行應用程式,請參閱「測試及部署應用程式」。然後部署新版本,確認應用程式顯示的內容與先前相同。

使用 App Engine NDB 或 Cloud NDB 的應用程式

如果應用程式使用 App Engine NDB 或 Cloud NDB,您必須更新 Datastore 資料模型,才能納入 Blobstore 相關屬性。

更新資料模型

由於 Cloud Storage 不支援 NDB 的 BlobKey 屬性,因此您需要修改 Blobstore 相關行,改用 NDB、網頁架構或其他位置的內建對等項目。

如要更新資料模型,請按照下列步驟操作:

  1. 找出使用 BlobKey 的資料模型行,如下所示:

    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()
    
  2. ndb.BlobKeyProperty() 替換為 ndb.StringProperty()

    class Visit(ndb.Model):
        'Visit entity registers visitor IP address & timestamp'
        visitor   = ndb.StringProperty()
        timestamp = ndb.DateTimeProperty(auto_now_add=True)
        file_blob = ndb.StringProperty() # Modified from ndb.BlobKeyProperty()
    
  3. 如果您在遷移期間也從 App Engine NDB 升級至 Cloud NDB,請參閱 Cloud NDB 遷移指南,瞭解如何重構 NDB 程式碼,以使用 Python 內容管理員。

Datastore 資料模型的回溯相容性

在上一節中,將 ndb.BlobKeyProperty 替換為 ndb.StringProperty 會導致應用程式無法回溯相容,也就是說,應用程式無法處理 Blobstore 建立的舊項目。如要保留舊資料,請為新的 Cloud Storage 項目建立額外欄位,而不是更新 ndb.BlobKeyProperty 欄位,並建立函式來正規化資料。

根據前幾節的範例,進行下列變更:

  1. 定義資料模型時,請建立兩個不同的屬性欄位。使用 file_blob 屬性識別 Blobstore 建立的物件,並使用 file_gcs 屬性識別 Cloud Storage 建立的物件:

    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()  # backwards-compatibility
        file_gcs  = ndb.StringProperty()
    
  2. 找出參照新訪客的程式碼行,例如:

    def store_visit(remote_addr, user_agent, upload_key):
        'create new Visit entity in Datastore'
        with ds_client.context():
            Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                    file_blob=upload_key).put()
    
  3. 變更程式碼,以便用於最近的項目。file_gcs例如:

    def store_visit(remote_addr, user_agent, upload_key):
        'create new Visit entity in Datastore'
        with ds_client.context():
            Visit(visitor='{}: {}'.format(remote_addr, user_agent),
                    file_gcs=upload_key).put() # change file_blob to file_gcs for new requests
    
  4. 建立新函式來正規化資料。以下範例說明如何使用擷取、轉換及載入 (ETL) 迴圈處理所有造訪,並擷取訪客和時間戳記資料,檢查是否存在 file_gcsfile_gcs

    def etl_visits(visits):
        return [{
                'visitor': v.visitor,
                'timestamp': v.timestamp,
                'file_blob': v.file_gcs if hasattr(v, 'file_gcs') \
                        and v.file_gcs else v.file_blob
                } for v in visits]
    
  5. 找出參照 fetch_visits() 函式的程式碼行:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = fetch_visits(10)
        return render_template('index.html', **context)
    
  6. fetch_visits() 包裝在 etl_visits() 函式中,例如:

    @app.route('/', methods=['GET', 'POST'])
    def root():
        'main application (GET/POST) handler'
        context = {}
        if request.method == 'GET':
            context['upload_url'] = url_for('upload')
        else:
            context['visits'] = etl_visits(fetch_visits(10)) # etl_visits wraps around fetch_visits
        return render_template('index.html', **context)
    

範例

後續步驟