Como usar o Blobstore do App Engine (módulo 15)

1. Visão geral

A série de codelabs da Serverless Migration Station (tutoriais práticos e autoguiados) e os vídeos relacionados têm como objetivo ajudar os desenvolvedores sem servidor do Google Cloud a modernizar os aplicativos orientando-os em uma ou mais migrações, principalmente a migração de serviços legados. Isso torna seus apps mais portáteis e oferece mais opções e flexibilidade, permitindo que você se integre e acesse uma variedade maior de produtos do Cloud e faça upgrade mais fácil para versões de linguagem mais recentes. Embora o foco inicial seja nos primeiros usuários do Cloud, principalmente desenvolvedores do App Engine (ambiente padrão), esta série é ampla o suficiente para incluir outras plataformas sem servidor, como o Cloud Functions e o Cloud Run, ou em outro lugar, se aplicável.

Este codelab do Módulo 15 explica como adicionar o uso do App Engine blobstore ao app de exemplo do Módulo 0. Depois, você estará pronto para migrar esse uso para o Cloud Storage no Módulo 16.

Você vai aprender a

  • Adicionar o uso da API/biblioteca Blobstore do App Engine
  • Armazenar uploads de usuários no serviço blobstore
  • Preparar a próxima etapa para migrar para o Cloud Storage

O que é necessário

Pesquisa

Como você vai usar este tutorial?

Apenas leitura Ler e fazer os exercícios

Como você classificaria sua experiência com Python?

Iniciante Intermediário Proficiente

Como você classificaria sua experiência de uso dos serviços do Google Cloud?

Iniciante Intermediário Proficiente

2. Contexto

Para migrar da API Blobstore do App Engine, adicione o uso dela ao aplicativo ndb do App Engine de referência do módulo 0. O app de exemplo mostra as dez visitas mais recentes do usuário. Estamos modificando o app para pedir que o usuário final faça upload de um artefato (um arquivo) que corresponda à "visita". Se o usuário não quiser fazer isso, há uma opção de "pular". Independente da decisão do usuário, a próxima página renderiza a mesma saída do app do módulo 0 (e de muitos outros módulos desta série). Com essa integração do App Engine blobstore implementada, podemos migrá-la para o Cloud Storage no próximo codelab (Módulo 16).

O App Engine oferece acesso aos sistemas de modelos Django e Jinja2. Uma coisa que torna este exemplo diferente (além de adicionar acesso ao Blobstore) é que ele muda de usar o Django no módulo 0 para o Jinja2 aqui no módulo 15. Uma etapa fundamental na modernização dos apps do App Engine é migrar os frameworks da Web de webapp2 para o Flask. O último usa o Jinja2 como sistema de modelos padrão. Por isso, começamos a migrar nessa direção implementando o Jinja2 enquanto permanecemos no webapp2 para acesso ao Blobstore. Como o Flask usa o Jinja2 por padrão, isso significa que nenhuma mudança no modelo será necessária no Módulo 16.

3. Configuração/Pré-trabalho

Antes de prosseguirmos com a parte principal do tutorial, configure o projeto, receba o código e implante o app de referência para começar com o código em funcionamento.

1. Configurar projeto

Se você já implantou o app do módulo 0, recomendamos reutilizar o mesmo projeto (e código). Se preferir, crie um novo projeto ou reutilize outro. Verifique se o projeto tem uma conta de faturamento ativa e se o App Engine está ativado.

2. Receber app de amostra do valor de referência

Um dos pré-requisitos para este codelab é ter um app de exemplo do Módulo 0. Se você não tiver um, acesse a pasta "START" do Módulo 0 (link abaixo). Este codelab orienta você em cada etapa, concluindo com um código semelhante ao da pasta "FINISH" do Módulo 15.

O diretório dos arquivos iniciais do Módulo 0 deve ter esta aparência:

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

3. (Re) Implantar aplicativo de referência

As etapas de pré-trabalho restantes para serem executadas agora:

  1. Conheça melhor a ferramenta de linha de comando gcloud
  2. Reimplantar o aplicativo de amostra com gcloud app deploy
  3. Confirmar se o aplicativo é executado no App Engine sem problemas

Depois de executar essas etapas com sucesso e verificar se o app da Web funciona (com uma saída semelhante à abaixo), você estará pronto para adicionar o uso de armazenamento em cache ao app.

a7a9d2b80d706a2b.png

4. Atualizar os arquivos de configuração

app.yaml

Não há mudanças significativas na configuração do aplicativo. No entanto, como mencionado anteriormente, estamos migrando do modelo de programação do Django (padrão) para o Jinja2. Para fazer a troca, os usuários precisam especificar a versão mais recente do Jinja2 disponível nos servidores do App Engine. Para isso, adicione-o à seção de bibliotecas integradas de terceiros do app.yaml.

ANTES:

runtime: python27
threadsafe: yes
api_version: 1

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

Edite o arquivo app.yaml adicionando uma nova seção libraries, como mostrado aqui:

AFTER:

runtime: python27
threadsafe: yes
api_version: 1

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

libraries:
- name: jinja2
  version: latest

Nenhum outro arquivo de configuração precisa ser atualizado. Vamos passar para os arquivos de aplicativos.

5. Modificar arquivos do aplicativo

Importações e suporte ao Jinja2

O primeiro conjunto de mudanças para main.py inclui a adição do uso da API Blobstore e a substituição de modelos do Django pelo Jinja2. Confira o que vai mudar:

  1. O objetivo do módulo os é criar um nome de caminho de arquivo para um modelo do Django. Como estamos mudando para o Jinja2, em que isso é processado, o uso de os e do renderizador de modelos do Django, google.appengine.ext.webapp.template, não é mais necessário. Portanto, eles estão sendo removidos.
  2. Importe a API Blobstore: google.appengine.ext.blobstore
  3. Importe os gerenciadores do Blobstore encontrados no framework webapp original. Eles não estão disponíveis no webapp2: google.appengine.ext.webapp.blobstore_handlers
  4. Importe o suporte do Jinja2 do pacote webapp2_extras.

ANTES:

import os
import webapp2
from google.appengine.ext import ndb
from google.appengine.ext.webapp import template

Implemente as mudanças na lista acima substituindo a seção de importação atual em main.py pelo snippet de código abaixo.

AFTER:

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

Depois das importações, adicione um código boilerplate para oferecer suporte ao uso do Jinja2, conforme definido na documentação do webapp2_extras. O snippet de código a seguir envolve a classe padrão do gerenciador de solicitações webapp2 com a funcionalidade do Jinja2. Adicione esse bloco de código a main.py logo após as importações:

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

Adicionar suporte ao Blobstore

Ao contrário de outras migrações nesta série, em que mantemos a funcionalidade ou a saída do app de exemplo idênticas (ou quase iguais) sem (muita) mudança na UX, este exemplo se afasta mais radicalmente da norma. Em vez de registrar imediatamente uma nova visita e mostrar as dez mais recentes, estamos atualizando o app para pedir ao usuário um artefato de arquivo para registrar a visita. Os usuários finais podem fazer upload de um arquivo correspondente ou selecionar "Pular" para não fazer upload de nada. Depois que essa etapa for concluída, a página "Visitas mais recentes" será exibida.

Essa mudança permite que nosso app use o serviço Blobstore para armazenar (e possivelmente renderizar mais tarde) essa imagem ou outro tipo de arquivo na página de visitas mais recentes.

Atualizar o modelo de dados e implementar o uso dele

Estamos armazenando mais dados, especificamente atualizando o modelo de dados para armazenar o ID (chamado de "BlobKey") do arquivo enviado ao Blobstore e adicionando uma referência para salvar isso em store_visit(). Como esses dados extras são retornados junto com todo o resto na consulta, fetch_visits() permanece o mesmo.

Confira o antes e o depois dessas atualizações com file_blob, um ndb.BlobKeyProperty:

ANTES:

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)

AFTER:

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)

Confira uma representação ilustrada das mudanças feitas até agora:

2270783776759f7f.png

Aceitar uploads de arquivos

A mudança mais significativa na funcionalidade é o suporte a uploads de arquivos, seja solicitando um arquivo ao usuário, oferecendo suporte ao recurso "pular" ou renderizando um arquivo correspondente a uma visita. Tudo isso faz parte da imagem. Estas são as mudanças necessárias para oferecer suporte a uploads de arquivos:

  1. A solicitação do manipulador principal GET não busca mais as visitas mais recentes para exibição. Em vez disso, ele pede que o usuário faça um upload.
  2. Quando um usuário final envia um arquivo para upload ou pula esse processo, um POST do formulário passa o controle para o novo UploadHandler, derivado de google.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler.
  3. O método POST de UploadHandler faz o upload, chama store_visit() para registrar a visita e aciona um redirecionamento HTTP 307 para enviar o usuário de volta a "/", onde...
  4. O método POST do manipulador principal consulta (via fetch_visits()) e mostra as visitas mais recentes. Se o usuário selecionar "pular", nenhum arquivo será enviado, mas a visita ainda será registrada, seguida pelo mesmo redirecionamento.
  5. A exibição de visitas mais recentes inclui um novo campo mostrado ao usuário, um "visualizar" com hiperlink se um arquivo de upload estiver disponível ou "nenhum" caso contrário. Essas mudanças são realizadas no modelo HTML, além da adição de um formulário de upload (mais informações em breve).
  6. Se um usuário final clicar no link "Visualizar" de qualquer visita com um vídeo enviado, será feita uma solicitação GET para um novo ViewBlobHandler, derivado de google.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler. Isso vai renderizar o arquivo se for uma imagem (no navegador, se compatível), solicitar o download se não for ou retornar um erro HTTP 404 se não for encontrado.
  7. Além do novo par de classes de gerenciador e de um novo par de rotas para enviar tráfego a elas, o gerenciador principal precisa de um novo método POST para receber o redirecionamento 307 descrito acima.

Antes dessas atualizações, o app do Módulo 0 só tinha um gerenciador principal com um método GET e uma única Routes:

ANTES:

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)

Com essas atualizações implementadas, agora há três gerenciadores: 1) gerenciador de upload com um método POST, 2) um gerenciador de download "visualizar blob" com um método GET e 3) o gerenciador principal com métodos GET e POST. Faça essas mudanças para que o restante do app fique assim:

AFTER:

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)

Há várias chamadas importantes no código que acabamos de adicionar:

  • Em MainHandler.get, há uma chamada para blobstore.create_upload_url. Essa chamada gera o URL para o qual o formulário POSTs, chamando o gerenciador de upload para enviar o arquivo ao Blobstore.
  • Em UploadHandler.post, há uma chamada para blobstore_handlers.BlobstoreUploadHandler.get_uploads. Essa é a verdadeira mágica que coloca o arquivo no Blobstore e retorna um ID exclusivo e persistente para ele, o BlobKey.
  • Em ViewBlobHandler.get, chamar blobstore_handlers.BlobstoreDownloadHandler.send com um BlobKey de arquivo resulta na busca do arquivo e no encaminhamento dele para o navegador do usuário final.

Essas chamadas representam a maior parte do acesso aos recursos adicionados ao app. Veja uma representação ilustrada desse segundo e último conjunto de mudanças em main.py:

da2960525ac1b90d.png

Atualizar modelo HTML

Algumas das atualizações no aplicativo principal afetam a interface do usuário (UI) do app. Por isso, são necessárias mudanças correspondentes no modelo da Web, duas na verdade:

  1. É necessário um formulário de upload de arquivo com três elementos de entrada: um arquivo e um par de botões de envio para upload e ignorar, respectivamente.
  2. Atualize a saída das visitas mais recentes adicionando um link "Visualizar" para visitas com um upload de arquivo correspondente ou "nenhum" caso contrário.

ANTES:

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

Implemente as mudanças na lista acima para incluir o modelo atualizado:

AFTER:

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

Esta imagem ilustra as atualizações necessárias em index.html:

8583e975f25aa9e7.png

Uma última mudança é que o Jinja2 prefere que os modelos estejam em uma pasta templates. Portanto, crie essa pasta e mova index.html para dentro dela. Com essa última etapa, você concluiu todas as mudanças necessárias para adicionar o uso do Blobstore ao app de exemplo do Módulo 0.

(opcional) "Melhoria" do Cloud Storage

O armazenamento do Blobstore evoluiu para o próprio Cloud Storage. Isso significa que os uploads do Blobstore ficam visíveis no console do Cloud, especificamente no navegador do Cloud Storage. A questão é onde. A resposta é o bucket padrão do Cloud Storage do seu app do App Engine. O nome dele é o nome de domínio completo do app do App Engine, PROJECT_ID.appspot.com. É muito conveniente porque todos os IDs de projeto são exclusivos, certo?

As atualizações feitas no aplicativo de exemplo descartam os arquivos enviados nesse bucket, mas os desenvolvedores podem escolher um local mais específico. O bucket padrão pode ser acessado programaticamente via google.appengine.api.app_identity.get_default_gcs_bucket_name(), exigindo uma nova importação se você quiser acessar esse valor, por exemplo, para usar como um prefixo para organizar arquivos enviados. Por exemplo, classificar por tipo de arquivo:

f61f7a23a1518705.png

Para implementar algo assim para imagens, por exemplo, você terá um código como este, além de um código que verifica os tipos de arquivo para escolher o nome do bucket desejado:

ROOT_BUCKET = app_identity.get_default_gcs_bucket_name()
IMAGE_BUCKET = '%s/%s' % (ROOT_BUCKET, 'images')

Você também vai validar as imagens enviadas usando uma ferramenta como o módulo imghdr da biblioteca padrão do Python para confirmar o tipo de imagem. Por fim, talvez seja interessante limitar o tamanho dos envios em caso de usuários mal-intencionados.

Digamos que tudo isso já foi feito. Como podemos atualizar nosso app para especificar onde armazenar os arquivos enviados? A chave é ajustar a chamada para blobstore.create_upload_url em MainHandler.get para especificar o local desejado no Cloud Storage para o upload. Para isso, adicione o parâmetro gs_bucket_name desta forma:

blobstore.create_upload_url('/upload', gs_bucket_name=IMAGE_BUCKET))

Como essa é uma atualização opcional se você quiser especificar para onde os uploads devem ir, ela não faz parte do arquivo main.py no repositório. Em vez disso, uma alternativa chamada main-gcs.py está disponível para análise no repositório. Em vez de usar uma "pasta" de bucket separada, o código em main-gcs.py armazena uploads no bucket "raiz" (PROJECT_ID.appspot.com), assim como main.py, mas fornece a estrutura necessária se você derivar a amostra em algo mais, conforme sugerido nesta seção. Confira abaixo uma ilustração das "diferenças" entre main.py e main-gcs.py.

256e1ea68241a501.png

6. Resumo/limpeza

Esta seção conclui o codelab implantando o app e verificando se ele funciona conforme o esperado e em qualquer saída refletida. Depois da validação do app, faça as etapas de limpeza e considere as próximas etapas.

Implantar e verificar o aplicativo

Implante novamente o app com gcloud app deploy e confirme se ele funciona conforme anunciado, diferenciando-se na experiência do usuário (UX) do app do módulo 0. Agora há duas telas diferentes no app. A primeira é o prompt do formulário de upload de arquivo de visita:

f5b5f9f19d8ae978.pngEm seguida, os usuários finais fazem upload de um arquivo e clicam em "Enviar" ou em "Pular" para não enviar nada. Em ambos os casos, o resultado é a tela de visita mais recente, agora aumentada com links de "visualização" ou "nenhum" entre os carimbos de data/hora da visita e as informações do visitante:

f5ac6b98ee8a34cb.png

Parabéns por concluir este codelab e adicionar o uso do Blobstore do App Engine ao app de exemplo do Módulo 0. Seu código agora deve corresponder ao que está na pasta FINISH (Módulo 15). A alternativa main-gcs.py também está presente nessa pasta.

Limpar

Geral

Se você terminou por enquanto, recomendamos que desative o app do App Engine para evitar cobranças. No entanto, se você quiser testar ou experimentar mais, a plataforma do App Engine tem uma cota sem custo financeiro. Portanto, enquanto você não exceder esse nível de uso, não vai receber cobranças. Isso é para computação, mas também pode haver cobranças por serviços relevantes do App Engine. Consulte a página de preços para mais informações. Se essa migração envolver outros serviços do Cloud, eles serão cobrados separadamente. Em qualquer caso, se aplicável, consulte a seção "Específico para este codelab" abaixo.

Para total transparência, a implantação em uma plataforma de computação sem servidor do Google Cloud, como o App Engine, gera custos mínimos de build e armazenamento. O Cloud Build e o Cloud Storage têm cotas sem custo financeiro próprias. O armazenamento dessa imagem usa parte dessa cota. No entanto, talvez você more em uma região que não tem um nível sem custo financeiro. Por isso, fique de olho no uso do armazenamento para minimizar possíveis custos. As "pastas" específicas do Cloud Storage que você precisa analisar incluem:

  • 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
  • Os links de armazenamento acima dependem da sua PROJECT_ID e *LOC*ação. Por exemplo, "us" se o app estiver hospedado nos EUA.

Por outro lado, se você não quiser continuar com este aplicativo ou outros codelabs de migração relacionados e quiser excluir tudo completamente, desligue seu projeto.

Específico para este codelab

Os serviços listados abaixo são exclusivos deste codelab. Consulte a documentação de cada produto para mais informações:

Próximas etapas

A próxima migração lógica a ser considerada é abordada no módulo 16, mostrando aos desenvolvedores como migrar do serviço Blobstore do App Engine para usar a biblioteca de cliente do Cloud Storage. Os benefícios da atualização incluem o acesso a mais recursos do Cloud Storage e o conhecimento de uma biblioteca de cliente que funciona para apps fora do App Engine, seja no Google Cloud, em outras nuvens ou até mesmo no local. Se você não acha que precisa de todos os recursos disponíveis no Cloud Storage ou está preocupado com os efeitos dele no custo, pode continuar usando o Blobstore do App Engine.

Além do Módulo 16, há várias outras migrações possíveis, como Cloud NDB e Cloud Datastore, Cloud Tasks ou Cloud Memorystore. Também há migrações entre produtos para o Cloud Run e o Cloud Functions. O repositório de migração apresenta todos os exemplos de código, links para todos os codelabs e vídeos disponíveis, além de orientações sobre quais migrações considerar e a "ordem" relevante delas.

7. Outros recursos

Problemas/feedback do codelab

Se você encontrar problemas com este codelab, pesquise seu problema antes de preenchê-lo. Links para pesquisar e criar novos problemas:

Recursos de migração

Os links para as pastas do repositório do módulo 0 (START) e do Módulo 15 (FINISH) podem ser encontrados na tabela abaixo. Elas também podem ser acessadas no repositório de todas as migrações de codelab do App Engine, que você pode clonar ou fazer o download de um arquivo ZIP.

Codelab

Python 2

Python 3

Module 0

código

N/A

Módulo 15 (este codelab)

código

N/A

Recursos on-line

Confira abaixo recursos on-line que podem ser relevantes para este tutorial:

App Engine

Google Cloud

Python

Vídeos

Licença

Este conteúdo está sob a licença Atribuição 2.0 Genérica da Creative Commons.