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

1. Visão geral

A série de codelabs da Estação de migração sem servidor (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 para deixar de usar 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 para versões mais recentes de linguagens com mais facilidade. 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 outras, se aplicável.

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

Você vai aprender a

  • Adicionar o uso da API/biblioteca do Blobstore do App Engine
  • Armazenar uploads de usuários no serviço blobstore
  • Preparar-se para a próxima etapa de migração 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 de referência do App Engine ndb do módulo 0. O app de exemplo mostra as dez visitas mais recentes ao usuário. Estamos modificando o app para solicitar 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 "pular". Independentemente 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 blobstore do App Engine implementada, podemos migrá-la para o Cloud Storage no próximo codelab (Módulo 16).

O App Engine fornece acesso aos sistemas de modelos Django e Jinja2. Uma coisa que diferencia este exemplo (além de adicionar o acesso ao Blobstore) é que ele muda o uso do Django no módulo 0 para o Jinja2 aqui no módulo 15. Uma etapa importante na modernização de 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 e permanecendo no webapp2 para acesso ao Blobstore. Como o Flask usa o Jinja2 por padrão, isso significa que não será necessário fazer mudanças no modelo no Módulo 16.

3. Configuração/Pré-trabalho

Antes de passarmos para a parte principal do tutorial, configure seu projeto, acesse o código e implante o app de referência para começar com o código de trabalho.

1. Configurar projeto

Se você já implantou o app do Módulo 0, recomendamos reutilizar o mesmo projeto (e código). Como alternativa, é possível criar um projeto totalmente novo ou reutilizar outro projeto. 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 em funcionamento. Se você não tiver, faça o download na pasta "START" do Módulo 0 (link abaixo). Este codelab mostra cada etapa, concluindo com um código semelhante ao que está na pasta "FINISH" do módulo 15.

O diretório de arquivos START do Módulo 0 deve ficar assim:

$ 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 e ver que o app da Web funciona (com uma saída semelhante à mostrada abaixo), você vai poder 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 dos modelos do Django (padrão) para o Jinja2. Portanto, 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-a à seção de bibliotecas de terceiros integradas 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:

DEPOIS:

runtime: python27
threadsafe: yes
api_version: 1

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

libraries:
- name: jinja2
  version: latest

Não é necessário atualizar outros arquivos de configuração. Vamos passar para os arquivos do aplicativo.

5. Modificar arquivos do aplicativo

Importações e suporte ao Jinja2

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

  1. O objetivo do módulo os é criar um 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, então 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, que não estão disponíveis em webapp2: google.appengine.ext.webapp.blobstore_handlers
  4. Importe o suporte ao 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.

DEPOIS:

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 permitir o uso do Jinja2, conforme definido na documentação do webapp2_extras. O snippet de código a seguir une a classe de gerenciador de solicitações padrão do webapp2 com a funcionalidade do Jinja2. Portanto, 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 desta série, em que mantemos a funcionalidade ou a saída do app de exemplo idêntica (ou quase igual) sem (muitas) mudanças na UX, este exemplo se afasta mais radicalmente da norma. Em vez de registrar imediatamente uma nova visita e exibir 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 para o Blobstore e adicionando uma referência para salvar isso em store_visit(). Como esses dados extras são retornados com todo o restante 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)

DEPOIS:

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 gráfica das mudanças feitas até agora:

2270783776759f7f.png

Suporte a 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 do cenário. Estas são as mudanças necessárias para oferecer suporte ao upload de arquivos:

  1. A solicitação do gerenciador principal GET não busca mais as visitas mais recentes para exibição. Em vez disso, ele solicita 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 realiza o upload, chama store_visit() para registrar a visita e aciona um redirecionamento HTTP 307 para enviar o usuário de volta para "/", onde...
  4. O método POST do gerenciador 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 das visitas mais recentes inclui um novo campo para o usuário, que pode ser um hiperlink "visualizar" se um arquivo de upload estiver disponível ou "nenhum" caso contrário. Essas mudanças são realizadas no modelo HTML, junto com a 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, ele vai fazer uma solicitação GET para um novo ViewBlobHandler, derivado de google.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler , renderizando o arquivo se for uma imagem (no navegador, se houver suporte), solicitando o download se não for ou retornando um erro HTTP 404 se não for encontrado.
  7. Além do novo par de classes de gerenciador e do 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 apresentava apenas um gerenciador principal com um método GET e uma única rota:

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 "view blob" com um método GET e 3) o gerenciador principal com os métodos GET e POST. Faça essas mudanças para que o restante do app fique como mostrado abaixo.

DEPOIS:

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 faz o POST, 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 o BlobKey de um 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 gráfica 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 (IU) do app. Por isso, é necessário fazer duas mudanças correspondentes no modelo da Web:

  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 para pular, 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:

DEPOIS:

<!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 para index.html:

8583e975f25aa9e7.png

Uma última mudança é que o Jinja2 prefere os modelos 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 acabou evoluindo 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 seu app do App Engine, PROJECT_ID.appspot.com. É muito conveniente porque todos os IDs do projeto são exclusivos, certo?

As atualizações feitas no aplicativo de exemplo colocam os arquivos enviados nesse bucket, mas os desenvolvedores podem escolher um local mais específico. O bucket padrão é acessível de forma programática por meio de 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 prefixo para organizar arquivos enviados. Por exemplo, classificação 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, é recomendável limitar o tamanho dos uploads para evitar usuários mal-intencionados.

Digamos que tudo isso já tenha sido feito. Como podemos atualizar nosso app para que ele permita especificar onde os arquivos enviados serão armazenados? O segredo é ajustar a chamada para blobstore.create_upload_url em MainHandler.get para especificar o local desejado no Cloud Storage para o upload adicionando o parâmetro gs_bucket_name desta forma:

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

Como essa é uma atualização opcional para especificar 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 o scaffolding necessário se você quiser derivar o exemplo em algo mais, como sugerido nesta seção. Veja abaixo uma ilustração das diferenças entre main.py e main-gcs.py.

256e1ea68241a501.png

6. Resumo/limpeza

Esta seção encerra o codelab com a implantação do app, a verificação de que ele funciona conforme o esperado e a análise de qualquer saída refletida. Após a validação do app, execute as etapas de limpeza e considere as próximas etapas.

Implantar e verificar o aplicativo

Implante o app novamente com gcloud app deploy e confirme se ele funciona conforme anunciado, com uma experiência do usuário (UX) diferente do app do Módulo 0. Agora há duas telas diferentes no app, sendo a primeira o prompt do formulário de upload de arquivos de visita:

f5b5f9f19d8ae978.pngEm seguida, os usuários finais fazem upload de um arquivo e clicam em "Enviar" ou em "Pular" para não fazer upload de nada. Em ambos os casos, o resultado é a tela de visita mais recente, agora 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 App Engine Blobstore 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ê já terminou, recomendamos que desative o aplicativo do App Engine para evitar cobranças. No entanto, se você quiser testar ou experimentar um pouco mais, a plataforma do App Engine tem uma cota sem custo financeiro. Portanto, desde que você não exceda esse nível de uso, não haverá 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 ambos os casos, se aplicável, consulte a seção "Específico para este codelab" abaixo.

Para divulgação completa, a implantação em uma plataforma de computação sem servidor do Google Cloud, como o App Engine, gera pequenos custos de build e armazenamento. O Cloud Build tem uma cota sem custo financeiro, assim como o Cloud Storage. O armazenamento dessa imagem usa parte dessa cota. No entanto, talvez você more em uma região que não tenha esse 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ê deve 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 do seu PROJECT_ID e da *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, encerre o 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, que mostra aos desenvolvedores como migrar do serviço Blobstore do App Engine para usar a biblioteca de cliente do Cloud Storage. Os benefícios do upgrade incluem o acesso a mais recursos do Cloud Storage e a familiarização com 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 precisa de todos os recursos disponíveis no Cloud Storage ou está preocupado com os efeitos dele no custo, pode continuar no Blobstore do App Engine.

Além do Módulo 16, há uma série de outras migrações possíveis, como o Cloud NDB e o Cloud Datastore, o Cloud Tasks ou o Cloud Memorystore. Também há migrações entre produtos para o Cloud Run e o Cloud Functions. O repositório de migração (link em inglês) 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) estão na tabela abaixo. Eles também podem ser acessados no repositório de todas as migrações de codelab do App Engine, em que é possível 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 os 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.