Как использовать хранилище BLOB-объектов App Engine (модуль 15)

1. Обзор

Серия практических работ Serverless Migration Station (самостоятельные практические руководства) и связанные с ними видео призваны помочь разработчикам бессерверных решений Google Cloud модернизировать свои приложения, проведя их через одну или несколько миграций, в первую очередь за счет отказа от устаревших сервисов. Это повысит переносимость приложений, а также предоставит вам больше возможностей и гибкости, что позволит интегрировать и использовать более широкий спектр продуктов Cloud и упростит переход на более новые версии языков. Изначально мы ориентировались на первых пользователей Cloud, в основном разработчиков App Engine (стандартная среда), но эта серия достаточно широка, чтобы включать другие бессерверные платформы, такие как Cloud Functions и Cloud Run, или другие, если применимо.

В этой практической работе (модуль 15) рассказывается, как добавить использование App Engineblobstore в пример приложения из модуля 0. После этого вы будете готовы перенести данные в Cloud Storage (модуль 16).

Вы научитесь:

  • Добавьте использование API/библиотеки хранилища BLOB-объектов App Engine
  • Хранить загруженные пользователями файлы в сервисе blobstore
  • Подготовка к переходу на Cloud Storage

Что вам понадобится

Опрос

Как вы будете использовать это руководство?

Просто прочитаю Прочитаю и выполню упражнения

Как бы вы оценили свой опыт работы с Python?

Начинающий Средний Продвинутый

Как бы вы оценили свой опыт использования сервисов Google Cloud?

Начинающий Средний Продвинутый

2. Фон

Чтобы перейти с App Engine Blobstore API, добавьте его использование в существующее базовое приложение App Engine ndb из модуля 0. В примере приложения пользователю показываются десять последних посещений. Мы изменяем приложение, чтобы конечный пользователь мог загрузить артефакт (файл), соответствующий его "посещению". Если пользователь не хочет этого делать, он может пропустить этот шаг. Независимо от решения пользователя, на следующей странице отображаются те же выходные данные, что и в приложении из модуля 0 (и многих других модулей этой серии). После интеграции blobstore в App Engine мы можем перейти на Cloud Storage в следующей практической работе (модуль 16).

App Engine предоставляет доступ к системам шаблонов Django и Jinja2. Одно из отличий этого примера (помимо добавления доступа к хранилищу BLOB-объектов) заключается в том, что в модуле 15 вместо Django используется Jinja2. Важный шаг в модернизации приложений App Engine – переход с веб-фреймворка webapp2 на Flask. В последней версии в качестве системы шаблонов по умолчанию используется Jinja2, поэтому мы начинаем двигаться в этом направлении, внедряя Jinja2 и сохраняя webapp2 для доступа к Blobstore. Поскольку Flask по умолчанию использует Jinja2, это означает, что в модуле 16 не потребуется вносить изменения в шаблон.

3. Настройка и подготовка

Прежде чем перейти к основной части руководства, настройте проект, получите код и разверните базовое приложение, чтобы начать работу с кодом.

1. Настроить проект

Если вы уже развернули приложение из модуля 0, рекомендуем использовать тот же проект и код. Вы также можете создать новый проект или использовать другой существующий. Убедитесь, что у проекта есть активный платежный аккаунт и включен App Engine.

2. Получить базовый пример приложения

Для выполнения этой практической работы у вас должно быть работающее пример приложения из модуля 0. Если у вас его нет, вы можете скачать его из папки START модуля 0 (ссылка ниже). В этой практической работе мы пошагово разберем, как это сделать. В результате у вас получится код, похожий на тот, что находится в папке FINISH модуля 15.

Каталог файлов START модуля 0 должен выглядеть следующим образом:

$ 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. Чтобы переключиться, пользователи должны указать последнюю версию Jinja2, доступную на серверах App Engine, добавив ее в раздел встроенных сторонних библиотек файла app.yaml.

BEFORE:

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 включает добавление использования Blobstore API и замену шаблонов Django на Jinja2. Вот что изменится:

  1. Цель модуля os – создать путь к файлу шаблона Django. Поскольку мы переходим на Jinja2, где это обрабатывается, использование os, а также средства визуализации шаблонов Django google.appengine.ext.webapp.template больше не требуется, поэтому они удаляются.
  2. Импортируйте Blobstore API: google.appengine.ext.blobstore
  3. Импортируйте обработчики Blobstore, которые есть в исходном фреймворке webapp, но отсутствуют в webapp2: google.appengine.ext.webapp.blobstore_handlers.
  4. Импортируйте поддержку Jinja2 из пакета webapp2_extras.

BEFORE:

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. В следующем фрагменте кода стандартный класс обработчика запросов webapp2 дополняется функциями Jinja2. Добавьте этот блок кода в файл 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-объектов

В отличие от других переносов в этой серии, где мы сохраняем функциональность или выходные данные примера приложения идентичными (или почти такими же) без (больших) изменений в UX, этот пример представляет собой более радикальное отклонение от нормы. Вместо того чтобы сразу регистрировать новое посещение, а затем показывать последние десять, мы обновляем приложение, чтобы оно запрашивало у пользователя файл для регистрации посещения. Конечные пользователи могут загрузить соответствующий файл или нажать "Пропустить", чтобы ничего не загружать. После этого откроется страница "Последние посещения".

Это изменение позволяет нашему приложению использовать сервис Blobstore для хранения (и, возможно, последующего рендеринга) этого изображения или файла другого типа на странице последних посещений.

Обновите модель данных и реализуйте ее использование

Мы сохраняем больше данных, в частности обновляем модель данных, чтобы сохранять идентификатор (называемый BlobKey) файла, загруженного в Blobstore, и добавляем ссылку, чтобы сохранить его в store_visit(). Поскольку эти дополнительные данные возвращаются вместе со всеми остальными при запросе, fetch_visits() остается прежним.

Вот как выглядели результаты до и после обновления на примере запроса "file_blob" (ndb.BlobKeyProperty):

BEFORE:

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. Метод POST класса UploadHandler выполняет загрузку, вызывает store_visit() для регистрации посещения и запускает перенаправление HTTP 307, чтобы отправить пользователя обратно на страницу "/", где…
  4. Метод POST основного обработчика запрашивает (через fetch_visits()) и показывает самые последние посещения. Если пользователь нажмет "Пропустить", файл не будет загружен, но посещение все равно будет зарегистрировано, после чего произойдет перенаправление.
  5. В списке последних посещений появилось новое поле. Если файл загружен, в нем будет ссылка "Посмотреть", а если нет – "Нет". Эти изменения реализованы в шаблоне HTML вместе с добавлением формы загрузки (подробнее об этом ниже).
  6. Если конечный пользователь нажимает на ссылку "Просмотр" для любого посещения с загруженным видео, он отправляет запрос GET на новый ViewBlobHandler, полученный из google.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler. При этом либо отображается файл, если это изображение (в браузере, если поддерживается), либо предлагается скачать его, если нет, либо возвращается ошибка HTTP 404, если файл не найден.
  7. Помимо новой пары классов обработчиков и новой пары маршрутов для отправки им трафика, основному обработчику требуется новый метод POST для получения описанного выше перенаправления 307.

До этих изменений в приложении Module 0 был только основной обработчик с методом GET и один маршрут:

BEFORE:

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) обработчик скачивания "view blob" с методом GET и 3) основной обработчик с методами GET и POST. Внесите эти изменения, чтобы остальная часть приложения выглядела так, как показано ниже.

ПОСЛЕ

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. Этот вызов генерирует URL, на который отправляется формаPOST, вызывая обработчик загрузки для отправки файла в Blobstore.
  • В UploadHandler.post у вас звонок: blobstore_handlers.BlobstoreUploadHandler.get_uploads. Это настоящая магия, которая помещает файл в Blobstore и возвращает уникальный и постоянный идентификатор для этого файла, его BlobKey.
  • В ViewBlobHandler.get вызов blobstore_handlers.BlobstoreDownloadHandler.send с BlobKey файла приводит к получению файла и его пересылке в браузер конечного пользователя.

Эти вызовы представляют собой основную часть доступа к функциям, добавленным в приложение. Ниже приведено графическое представление этого второго и последнего набора изменений в main.py:

da2960525ac1b90d.png

Обновить HTML-шаблон

Некоторые обновления основного приложения влияют на его пользовательский интерфейс, поэтому в веб-шаблон необходимо внести два изменения:

  1. A file upload form is required with 3 input elements: a file and a pair of submit buttons for file upload and skip, respectively.
  2. Обновите вывод последних посещений, добавив ссылку "Просмотр" для посещений с соответствующей загрузкой файла или "Нет" в противном случае.

BEFORE:

<!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, чтобы добавить в него хранилище BLOB-объектов.

"Улучшение" Cloud Storage (необязательно)

Впоследствии хранилище Blobstore превратилось в Cloud Storage. Это означает, что загрузки Blobstore видны в консоли Cloud, в частности в браузере Cloud Storage. Вопрос в том, где. Ответ – сегмент Cloud Storage по умолчанию для приложения App Engine. Его название совпадает с полным доменным именем вашего приложения App Engine – PROJECT_ID.appspot.com. Это очень удобно, потому что все идентификаторы проектов уникальны, верно?

Обновления, внесенные в пример приложения, помещают загруженные файлы в этот сегмент, но у разработчиков есть возможность выбрать более конкретное местоположение. Сегмент по умолчанию доступен программно через 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')

Вы также будете проверять загруженные изображения с помощью такого инструмента, как модуль imghdr стандартной библиотеки Python, чтобы подтвердить тип изображения. Наконец, вы, вероятно, захотите ограничить размер загружаемых файлов на случай злоумышленников.

Допустим, все это уже сделано. Как обновить приложение, чтобы в нем можно было указывать, где хранить загруженные файлы? Для этого нужно изменить вызов blobstore.create_upload_url в MainHandler.get, указав нужное местоположение в Cloud Storage для загрузки, добавив параметр gs_bucket_name, как показано ниже:

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 и убедитесь, что оно работает, как заявлено, и отличается от приложения из модуля 0. Теперь в приложении два экрана. Первый – это форма загрузки файла:

f5b5f9f19d8ae978.pngПосле этого конечные пользователи могут загрузить файл и нажать "Отправить" или нажать "Пропустить", чтобы ничего не загружать. В обоих случаях вы увидите экран с информацией о последнем посещении, дополненный ссылками "Просмотр" или словом "Нет" между временными метками посещений и информацией о посетителях:

f5ac6b98ee8a34cb.png

Поздравляем! Вы завершили практическую работу по добавлению хранилища BLOB-объектов App Engine в пример приложения из модуля 0. Теперь ваш код должен совпадать с кодом в папке 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*ation, например us, если ваше приложение размещено в США.

Если вы не планируете продолжать работу с этим приложением или другими практическими работами по переносу и хотите удалить все полностью, закройте проект.

Особенности этой практической работы

Сервисы, перечисленные ниже, используются только в этой практической работе. Дополнительную информацию можно найти в документации по каждому продукту:

Дальнейшие действия

Следующий логический шаг – переход с хранилища BLOB-объектов App Engine на клиентскую библиотеку Cloud Storage. Он описан в модуле 16. Преимущества обновления включают возможность доступа к дополнительным функциям Cloud Storage, ознакомление с клиентской библиотекой, которая работает для приложений за пределами App Engine, будь то в Google Cloud, других облаках или даже локально. Если вам не нужны все функции Cloud Storage или вы беспокоитесь о расходах, вы можете продолжить использовать хранилище BLOB-объектов App Engine.

После модуля 16 есть множество других возможных миграций, таких как Cloud NDB и Cloud Datastore, Cloud Tasks или Cloud Memorystore. Также возможен перенос в Cloud Run и Cloud Functions. В репозитории вы найдете все примеры кода, ссылки на практические работы и видео, а также рекомендации по переносу и его последовательности.

7. Дополнительные ресурсы

Проблемы с практическими работами/отзывы о них

Если вы обнаружите проблемы в этой практической работе, сначала поищите информацию о них, прежде чем отправлять сообщение. Ссылки для поиска и создания новых проблем:

Информация о переносе данных

Ссылки на папки репозитория для модуля 0 (START) и модуля 15 (FINISH) приведены в таблице ниже. Их также можно найти в репозитории для всех практических работ по переносу App Engine, который можно клонировать или скачать в виде ZIP-файла.

Практическая работа

Python 2

Python 3

Модуль 0

code

–

Модуль 15 (эта практическая работа)

code

–

Ресурсы в интернете

Ниже приведены онлайн-ресурсы, которые могут быть полезны для этого руководства:

App Engine

Google Cloud

Python

Видео

Лицензия

Эта работа лицензирована в соответствии с лицензией Creative Commons Attribution 2.0 Generic License.