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

1. 概览

无服务器迁移站系列 Codelab(自定进度的动手教程)和相关视频旨在帮助 Google Cloud 无服务器开发者通过一次或多次迁移(主要是从旧版服务迁移)来指导他们的应用现代化。这样做可让您的应用更易于移植,并为您提供更多选择和灵活性,使您能够与更多 Cloud 产品集成并访问这些产品,还能更轻松地升级到新的语言版本。虽然最初侧重于最早的 Cloud 用户(主要是 App Engine [标准环境] 开发者),但本系列文章的范围足够广,可涵盖其他无服务器平台(例如 Cloud FunctionsCloud Run),或者其他平台(如果适用)。

此模块 15 的 Codelab 介绍了如何将 App Engine blobstore 用法添加到模块 0 中的示例应用。然后,您就可以在模块 16 中将该使用情况迁移到 Cloud Storage 了。

在接下来的实验中

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

所需条件

调查问卷

您将如何使用本教程?

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

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

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

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

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

2. 背景

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

App Engine 提供对 DjangoJinja2 模板系统的访问权限,此示例与众不同的一点(除了添加 blobstore 访问权限之外)是,它从在模块 0 中使用 Django 切换到在模块 15 中使用 Jinja2。对 App Engine 应用进行现代化改造的关键一步是将 Web 框架从 webapp2 迁移到 Flask。后者使用 Jinja2 作为其默认模板系统,因此我们开始朝着这个方向发展,在保持 webapp2 以便访问 Blobstore 的同时实现 Jinja2。由于 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 的第一组更改包括添加 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 中的当前导入部分替换为以下代码段,实现上述列表中的更改。

升级后

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))

添加 Blobstore 支持

与本系列中的其他迁移不同,在此示例中,我们对用户体验进行了大幅更改,而其他迁移则在不(大幅)更改用户体验的情况下,保持示例应用的功能或输出完全相同(或几乎相同)。我们不会立即注册新访问记录,然后显示最近的 10 条访问记录,而是会更新应用,要求用户提供文件制品来注册其访问记录。然后,最终用户可以上传相应的文件,也可以选择“跳过”而不上传任何内容。完成此步骤后,系统会显示“最近访问过的网站”页面。

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

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

我们正在存储更多数据,具体来说,就是更新数据模型以存储上传到 Blobstore 的文件的 ID(称为“BlobKey”),并添加一个引用以将该 ID 保存到 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 会将控制权传递给从 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) 具有 GET 方法的“查看 blob”下载处理程序;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 到的网址,并调用上传处理程序将文件发送到 Blobstore。
  • UploadHandler.post 中,有一个对 blobstore_handlers.BlobstoreUploadHandler.get_uploads 的调用。这才是真正的神奇之处,它会将文件放入 Blobstore 中,并返回该文件的唯一且持久的 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.png

最后,Jinja2 倾向于将模板放在 templates 文件夹中,因此请创建该文件夹并将 index.html 移到其中。完成此最后一步后,您就完成了向模块 0 示例应用添加 Blobstore 使用所需的所有更改。

(可选)Cloud Storage“增强”

Blobstore 存储最终演变为 Cloud Storage 本身。这意味着 Blobstore 上传内容会显示在 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. 总结/清理

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

部署并验证应用

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

f5b5f9f19d8ae978.png然后,最终用户可以上传文件并点击“提交”,也可以点击“跳过”而不上传任何内容。无论哪种情况,结果都是最新的访问屏幕,现在在访问时间戳和访问者信息之间添加了“查看”链接或“无”:

f5ac6b98ee8a34cb.png

恭喜您完成本 Codelab,为模块 0 示例应用添加了对 App Engine Blobstore 的使用。您的代码现在应与 FINISH(模块 15)文件夹中的内容一致。该文件夹中还存在替代 main-gcs.py

清理

常规

如果您暂时不想继续操作,建议您停用 App Engine 应用,以免产生结算费用。不过,如果您想进一步测试或实验,App Engine 平台有免费配额,因此只要您不超过该使用层级,就不会产生费用。这是计算费用,但相关 App Engine 服务也可能会产生费用,因此请查看其价格页面了解详情。如果此迁移涉及其他 Cloud 服务,则这些服务会单独计费。在任何一种情况下,如果适用,请参阅下文中的“本 Codelab 特有的问题”部分。

为了完全公开透明,我们在此说明,部署到 Google Cloud 无服务器计算平台(例如 App Engine)会产生少量 build 和存储费用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*ation,例如,如果您的应用托管在美国,则为“us”。

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

此 Codelab 特有的

以下列出的服务是此 Codelab 特有的。如需了解详情,请参阅各个产品的文档:

后续步骤

下一个需要考虑的逻辑迁移在模块 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 的功能。迁移代码库包含所有代码示例,可将您链接到所有可用的 Codelab 和视频,还提供有关应考虑哪些迁移以及任何相关迁移“顺序”的指南。

7. 其他资源

Codelab 问题/反馈

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

迁移时可参考的资源

下表中提供了指向模块 0(开始)和模块 15(完成)的 Repo 文件夹的链接。您还可以从所有 App Engine Codelab 迁移的代码库中访问这些示例,您可以克隆该代码库或下载 ZIP 文件。

Codelab

Python 2

Python 3

模块 0

代码

不适用

第 15 模块(本 Codelab)

代码

不适用

在线资源

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

App Engine

Google Cloud

Python

视频

许可

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