如何使用 App Engine Blob 存储区(模块 15)

1. 概览

无服务器迁移站系列 Codelab(自定进度的实操教程)和相关视频旨在指导 Google Cloud 无服务器开发者完成一次或多次迁移(主要是从旧服务迁移),从而实现应用的现代化改造。这样做可以提高应用的可移植性,为您提供更多选择和灵活性,让您能够集成并访问更广泛的 Cloud 产品,并更轻松地升级到较新版本。虽然本系列最初主要面向的是最早接触 Cloud 的用户(主要是 App Engine(标准环境)开发者),但涵盖的范围非常广泛,涵盖了 Cloud FunctionsCloud Run 等其他无服务器平台,或其他无服务器平台(如适用)。

第 15 单元的 Codelab 说明了如何在单元 0 中向示例应用添加 App Engine blobstore 用法。接下来,您就可以在第 16 单元中将该用量迁移到 Cloud Storage

在接下来的实验中

  • 添加对 App Engine Blob 存储区 API/库的使用
  • 将用户上传内容存储到 blobstore 服务
  • 为下一步迁移到 Cloud Storage 做好准备

所需条件

调查问卷

您将如何使用本教程?

仅阅读教程内容 阅读并完成练习

您如何评价使用 Python 的体验?

新手水平 中等水平 熟练水平

您如何评价自己在使用 Google Cloud 服务方面的经验水平?

<ph type="x-smartling-placeholder"></ph> 新手 中级 熟练

2. 背景

如需从 App Engine Blob 存储区 API 迁移,请从模块 0 将其用法添加到现有基准 App Engine ndb 应用中。示例应用向用户显示最近 10 次访问。我们正在修改该应用,以提示最终用户上传与其“访问”对应的工件(文件)。如果用户不想这样做,则可以“跳过”选项。无论用户决定如何决定,下一页都会呈现与模块 0(以及本系列中的许多其他模块)中的应用相同的输出。实现此 App Engine blobstore 集成后,我们可以在下一个(模块 16)Codelab 中将其迁移到 Cloud Storage

App Engine 提供了对 DjangoJinja2 模板系统的访问权限,使得此示例与众不同的一个因素(除了添加 Blob 存储区访问权限之外)是它在模块 0 中使用 Django 切换到在模块 15 中使用 Jinja2。对 App Engine 应用进行现代化改造的关键步骤是将 Web 框架从 webapp2 迁移到 Flask。后者使用 Jinja2 作为其默认模板系统,因此我们开始朝着这个方向发展,即实现 Jinja2,同时继续使用 webapp2 访问 Blob 存储区。由于 Flask 默认使用 Jinja2,这意味着在模块 16 中不需要对模板进行任何更改。

3. 设置/准备工作

在进入本教程的主要部分之前,请先设置您的项目、获取代码并部署基准应用,以从可正常工作的代码入手。

1. 设置项目

如果您已部署模块 0 应用,我们建议您重复使用同一项目(和代码)。或者,您也可以创建一个全新的项目或重复使用其他现有项目。确保项目具有有效的结算账号并且已启用 App Engine。

2. 获取基准示例应用

此 Codelab 的前提条件之一是拥有有效的模块 0 示例应用。如果没有,可以从模块 0“START”中获取(下方链接即可找到)。此 Codelab 将逐步引导您完成每个步骤,最后使用与第 15 单元“FINISH”中的代码类似的代码文件夹中。

模块 0 起始文件的目录应如下所示:

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

3. (重新)部署基准应用

现在需要执行的其余准备工作步骤:

  1. 重新熟悉 gcloud 命令行工具
  2. 使用 gcloud app deploy 重新部署示例应用
  3. 确认应用在 App Engine 上运行没有任何问题

成功执行这些步骤并看到您的 Web 应用正常运行(输出类似于下文)后,您就可以在应用中使用缓存了。

a7a9d2b80d706a2b.png

4. 更新配置文件

app.yaml

应用配置没有实质性更改,但是如前所述,我们正在从 Django 模板(默认)移至 Jinja2,因此为了进行切换,用户应指定 App Engine 服务器上可用的最新版 Jinja2,并且您需要将其添加到 app.yaml 的内置第三方库部分。

之前

runtime: python27
threadsafe: yes
api_version: 1

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

您可以通过添加新的 libraries 部分来修改 app.yaml 文件,如下所示:

之后

runtime: python27
threadsafe: yes
api_version: 1

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

libraries:
- name: jinja2
  version: latest

其他配置文件不需要更新,所以我们继续看应用文件。

5. 修改应用文件

导入和 Jinja2 支持

main.py 的第一组更改包括使用 Blob 存储区 API,以及用 Jinja2 替换 Django 模板。具体变化如下:

  1. os 模块的用途是创建 Django 模板的文件路径名。由于我们要切换到处理此操作的 Jinja2,因此不再需要使用 os 和 Django 模板渲染程序 google.appengine.ext.webapp.template,因此将将其移除。
  2. 导入 Blob 存储区 API:google.appengine.ext.blobstore
  3. 导入在原始 webapp 框架中找到的 Blob 存储区处理程序 - 这些处理程序在 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 中的当前导入部分替换为以下代码段,从而实现上述列表中的更改。

之后

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

导入后,添加一些样板代码以支持使用 Jinja2,如 webapp2_extras 文档中所定义。以下代码段使用 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))

添加 Blob 存储区支持

本系列中的其他迁移与保持示例应用的功能或输出相同(或几乎相同)而不对用户体验做出(很多更改)的迁移不同,此示例与常态有很大的不同。我们将更新应用以要求用户提供用于注册其访问的文件工件,而不是立即记录新的访问,然后显示最近的十次访问。然后,最终用户可以上传相应文件,或选择“跳过”不上传任何内容完成此步骤后,“最近访问”列页面。

此更改允许我们的应用程序使用 Blob 存储区服务在最近的访问页面上存储(并可能稍后呈现)该图片或其他文件类型。

更新数据模型并实现其用途

我们将存储更多数据,具体而言是更新数据模型以存储上传到 Blob 存储区的文件的 ID(称为“BlobKey”),并添加引用以将其保存在 store_visit() 中。由于查询时会随所有其他数据一起返回这些额外数据,因此 fetch_visits() 会保持不变。

以下是包含ndb.BlobKeyPropertyfile_blob的这些更新前后的对比:

之前

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

支持文件上传

功能上最显著的变化是支持文件上传,无论是提示用户文件,还是支持“跳过”功能,或呈现与访问对应的文件。所有这一切都是图片的一部分。以下是支持文件上传所需的更改:

  1. 主处理程序 GET 请求不再提取最近的访问以显示。而是会提示用户进行上传。
  2. 当最终用户提交要上传的文件或跳过该过程时,表单中的 POST 会将控制权传递给派生自 google.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler 的新 UploadHandler
  3. UploadHandlerPOST 方法会执行上传操作,调用 store_visit() 来注册访问,然后触发 HTTP 307 重定向以将用户发送回“/”,其中...
  4. 主处理程序的 POST 方法会查询(通过 fetch_visits())并显示最近的访问。如果用户选择“跳过”未上传任何文件,但访问仍会被记录,且经过同一重定向。
  5. “最近访问”显示内容包含向用户显示的新字段,可能是超链接“查看”如果有上传文件或“无”否则。这些更改是通过 HTML 模板实现的,并且增加了上传表单(即将介绍更多内容)。
  6. 如果最终用户点击“查看”链接,它会向派生自 google.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler 的新 ViewBlobHandler 发出 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)“查看 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),因此需要在 Web 模板中进行相应的更改,实际上有两个更改:

  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

最后一项更改是 Jinja2 倾向于将其模板放在 templates 文件夹中,因此请创建该文件夹并将 index.html 移入其中。在最后这个步骤中,您现在已经完成了将 Blob 存储区添加到模块 0 示例应用的所有必要更改。

(可选)Cloud Storage“增强功能”

Blob 存储区存储最终演化为 Cloud Storage 本身。这意味着,您可以在 Cloud 控制台(尤其是 Cloud Storage 浏览器)中看到 Blob 存储区上传操作。问题出在哪里。答案是 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 模块等工具验证上传的图片,以确认图片类型。最后,您可能需要限制上传内容的大小,以应对不良行为者。

就这么简单。我们如何更新应用以支持指定上传文件的存储位置?关键在于通过添加如下所示的 gs_bucket_name 参数,调整 MainHandler.get 中对 blobstore.create_upload_url 的调用,以指定 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

6. 摘要/清理

在此 Codelab 的最后,本部分将部署应用,验证应用是否按预期以及任何反映的输出中正常运行。应用验证后,执行所有清理步骤并考虑后续步骤。

部署并验证应用

使用 gcloud app deploy 重新部署您的应用,并确认应用按预期运行,这与模块 0 应用的用户体验 (UX) 有所不同。现在,您的应用中有两个不同的屏幕,第一个是访问文件上传表单提示:

f5b5f9f19d8ae978.png最终用户可以在其中上传文件,然后点击“提交”或者点击“跳过”不上传任何内容不管是哪种情况,结果都是最近的访问屏幕,现在通过“view”得到增强链接或“无”访问时间戳和访问者信息之间的具体差异:

f5ac6b98ee8a34cb.png

恭喜您完成此 Codelab,并已将对 App Engine Blob 存储区的使用添加到模块 0 的示例应用中。您的代码现在应与 FINISH (Module 15) 文件夹中的代码一致。该文件夹中还存在备用 main-gcs.py

清理

常规

如果您目前已完成,我们建议您停用 App Engine 应用,以免产生费用。不过,如果您希望测试或实验更多内容,App Engine 平台有免费配额,因此只要您不超过该使用量水平,您就不必支付费用。这只是计算费用,但相关 App Engine 服务可能也会产生费用,因此请查看其价格页面了解详情。如果此迁移涉及其他 Cloud 服务,这些服务单独计费。无论是哪种情况(如适用),请参阅“此 Codelab 的具体说明”部分。

为了全面披露,部署到像 App Engine 这样的 Google Cloud 无服务器计算平台会产生少量构建和存储费用Cloud BuildCloud 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”。

另一方面,如果您不打算继续学习此应用或其他相关的迁移 Codelab,而是想彻底删除所有内容,请关停项目

此 Codelab 的具体内容

下列服务是此 Codelab 独有的服务。有关详情,请参阅各个产品的文档:

后续步骤

单元 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。迁移代码库包含所有代码示例,提供了所有 Codelab 和视频的链接,还提供了关于应考虑哪些迁移以及任何相关“订单”的指导。迁移。

7. 其他资源

Codelab 问题/反馈

如果您在此 Codelab 中发现任何问题,请先搜索您的问题,然后再提交。用于搜索和创建新问题的链接:

迁移时可参考的资源

下表列出了模块 0(开始阶段)和模块 15 (FINISH) 对应的代码库文件夹的链接。您还可以从所有 App Engine Codelab 迁移的代码库访问这些库,您可以克隆或下载 ZIP 文件。

Codelab

Python 2

Python 3

模块 0

代码

不适用

第 15 单元(此 Codelab)

代码

不适用

在线资源

以下是可能与本教程相关的在线资源:

App Engine

Google Cloud

Python

视频

许可

此作品已获得 Creative Commons Attribution 2.0 通用许可授权。