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 individualizados) e os vídeos relacionados têm como objetivo ajudar desenvolvedores sem servidor do Google Cloud a modernizar apps, orientando-os em uma ou mais migrações, principalmente para evitar serviços legados. Isso torna seus apps mais portáteis e oferece mais opções e flexibilidade, o que permite a integração e o acesso a uma variedade maior de produtos do Cloud e o upgrade para versões de idiomas mais recentes com mais facilidade. Embora inicialmente voltada para os 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 outros lugares, 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. Assim você poderá migrar esse uso para o Cloud Storage a seguir no módulo 16.

Você vai aprender a

  • Adicionar uso da biblioteca/API Blobstore do App Engine
  • Armazenar uploads de usuários no serviço blobstore
  • Prepare-se para a próxima etapa da 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 App Engine Blobstore, adicione o uso dela ao app ndb de referência do App Engine no módulo 0. O app de exemplo mostra as dez visitas mais recentes ao 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á um botão "pular" é a melhor opção. Independentemente da decisão do usuário, a próxima página renderiza o mesmo resultado que o aplicativo do Módulo 0 (e muitos dos 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 torna esse exemplo diferente (além de adicionar acesso ao Blobstore) é que ele alterna do 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 frameworks da Web do webapp2 para o Flask. O segundo usa Jinja2 como seu sistema de modelagem padrão, então começamos a seguir nessa direção implementando o Jinja2 e permanecemos em webapp2 para acesso ao Blobstore. Como o Flask usa o Jinja2 por padrão, nenhuma alteração no modelo será necessária no módulo 16.

3. Configuração/Pré-trabalho

Antes de chegarmos à parte principal do tutorial, configure seu projeto, receba o código e implante o aplicativo de referência para começar a trabalhar com o código.

1. Configurar projeto

Se você já implantou o app do Módulo 0, recomendamos reutilizar o mesmo projeto (e código). Outra opção é criar um novo projeto ou reutilizar um projeto existente. 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 deste codelab é ter um app de exemplo do Módulo 0 em funcionamento. Caso não o tenha, pode obtê-lo no Módulo 0 "INICIAR" da pasta (link abaixo). Este codelab orienta você em cada etapa e conclui com um código semelhante ao do Módulo 15, "FINISH". do Compute Engine.

O diretório dos arquivos STARTing do Módulo 0 deve se parecer com este:

$ 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 conferir que seu app da Web funciona (com uma saída semelhante à mostrada abaixo), você poderá usar o armazenamento em cache.

a7a9d2b80d706a2b.png

4. Atualizar os arquivos de configuração

app.yaml

Não há alterações significativas na configuração do aplicativo. No entanto, como mencionado anteriormente, estamos mudando dos modelos Django (padrão) para o Jinja2. Portanto, para alternar, os usuários devem especificar a versão mais recente do Jinja2 disponível nos servidores do App Engine, e você faz isso adicionando-a à seção de bibliotecas de terceiros integrada do app.yaml.

ANTES:

runtime: python27
threadsafe: yes
api_version: 1

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

Edite seu arquivo app.yaml adicionando uma nova seção libraries como esta:

DEPOIS:

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, então vamos passar para os arquivos de aplicativo.

5. Modificar arquivos do aplicativo

Suporte a importações e Jinja2

O primeiro conjunto de alterações para main.py inclui a adição do uso da API Blobstore e a substituição dos modelos Django por Jinja2. Veja o que vai mudar:

  1. O objetivo do módulo os é criar um caminho de arquivo para um modelo Django. Como estamos mudando para Jinja2, onde isso é tratado, o uso de os e do renderizador de modelo Django, google.appengine.ext.webapp.template, não são mais necessários, 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 em webapp2: google.appengine.ext.webapp.blobstore_handlers
  4. Importar o suporte a 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

Após as importações, adicione um código boilerplate para permitir o uso do Jinja2, conforme definido nos documentos do webapp2_extras. O snippet de código a seguir encapsula a classe do gerenciador de solicitações padrão do webapp2 com a funcionalidade Jinja2. Portanto, adicione este 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 a mesma) sem mudanças (muito) na UX, esse exemplo se distingue de maneira mais radical da norma. Em vez de registrar imediatamente uma nova visita e exibir os 10 mais recentes, estamos atualizando o app para solicitar 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, as "visitas mais recentes" é exibida.

Essa mudança permite que nosso aplicativo 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, atualizando especificamente 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 com todo o restante na consulta, fetch_visits() permanece o mesmo.

Confira o que acontece antes e 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)

Veja uma representação pictórica das mudanças feitas até agora:

2270783776759f7f.png

Suporte a uploads de arquivos

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

  1. A solicitação GET do gerenciador principal 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 a "/", em que...
  4. O método POST do gerenciador principal consulta (via fetch_visits()) e exibe as visitas mais recentes. Se o usuário selecionar "pular", nenhum arquivo é enviado, mas a visita continua registrada e, em seguida, pelo mesmo redirecionamento.
  5. A exibição de visitas mais recentes inclui um novo campo exibido ao usuário, uma "visualização" com hiperlink se um arquivo de upload estiver disponível ou "nenhum" caso contrário. Essas alterações são percebidas no modelo HTML em conjunto com a adição de um formulário de upload (mais sobre isso em breve).
  6. Se um usuário final clicar na "visualização" para qualquer visita com um vídeo enviado, ele faz 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 compatível), solicitando o download, caso contrário, ou retornando 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 Module 0 tinha 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 e 2) um "blob de visualização" de download 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 o exemplo 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 nesse código que acabamos de adicionar:

  • Em MainHandler.get, há uma chamada para blobstore.create_upload_url. Essa chamada gera o URL para 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 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 e encaminhamento dele para o navegador do usuário final.

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

da2960525ac1b90d.png

Atualizar modelo HTML

Algumas das atualizações do aplicativo principal afetam a interface do usuário (IU) do aplicativo. Portanto, são necessárias alterações correspondentes no modelo da Web: duas delas:

  1. É necessário um formulário de upload de arquivos com três elementos de entrada: um arquivo e um par de botões de envio para fazer upload e pular o arquivo, respectivamente.
  2. Atualizar o resultado das visitas mais recentes adicionando uma "visualização" 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 da 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 alteração final é que o Jinja2 prefere seus modelos em uma pasta templates, portanto, crie essa pasta e mova index.html para dentro dela. Com essa mudança final, você concluiu todas as mudanças necessárias para adicionar o uso do Blobstore ao app de exemplo do Módulo 0.

(opcional) "aprimoramento" 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 no aplicativo do App Engine. O nome é o nome de domínio completo do seu aplicativo do App Engine, PROJECT_ID.appspot.com. Isso é muito conveniente, porque todos os IDs de projeto são exclusivos, certo?

As atualizações feitas no aplicativo de exemplo descartam os arquivos enviados para esse bucket, mas os desenvolvedores têm a opção de escolher um local mais específico. O bucket padrão pode ser acessado de maneira programática via google.appengine.api.app_identity.get_default_gcs_bucket_name(), o que exige uma nova importação se você quiser acessar esse valor, por exemplo, para usar como prefixo para organizar os 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, junto com um código que verificou 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 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, é uma boa ideia limitar o tamanho dos uploads em caso de usuários de má-fé.

Digamos que tudo isso foi feito. Como podemos atualizar nosso aplicativo para oferecer suporte à especificação de 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 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 caso você queira especificar para onde os uploads precisam 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 revisão no repositório. Em vez de usar uma "pasta" separada com um bucket, o código em main-gcs.py armazena os uploads na pasta "raiz". bucket (PROJECT_ID.appspot.com), assim como main.py, mas fornece a estrutura necessária para derivar a amostra em algo mais conforme 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 este codelab implantando o app, verificando se ele funciona conforme o esperado e em qualquer saída refletida. Após a validação do app, execute as etapas de limpeza e considere as próximas etapas.

Implante e verifique 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 seu app tem duas telas diferentes. A primeira é a solicitação do formulário de upload de arquivos de visita:

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

f5ac6b98ee8a34cb.png

Parabéns por concluir este codelab adicionando 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). O main-gcs.py alternativo também está presente nessa pasta.

Limpar

Geral

Se você já tiver terminado por enquanto, recomendamos que desative seu aplicativo do App Engine para evitar cobranças. No entanto, se você quiser fazer mais testes, saiba que a plataforma do App Engine tem uma cota sem custo financeiro e, 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. Portanto, consulte a página de preços para mais informações. Se essa migração envolver outros serviços do Cloud, eles serão faturados separadamente. Em ambos os casos, se aplicável, consulte a seção "Específico para este codelab". seção abaixo.

Para divulgação completa, a implantação em uma plataforma de computação sem servidor do Google Cloud, como o App Engine, incorre em menores custos de criação e armazenamento. O Cloud Build tem a própria cota sem custo financeiro, assim como o Cloud Storage. O armazenamento da imagem consome parte da cota. No entanto, talvez você more em uma região que não tenha esse nível sem custo financeiro, portanto, esteja ciente do uso do armazenamento para minimizar os possíveis custos. "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 PROJECT_ID e da *LOC*ação, por exemplo, "us" caso seu app esteja hospedado nos EUA.

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

Específicos deste 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 a possibilidade de acessar mais recursos do Cloud Storage, familiarizar-se com uma biblioteca de cliente que funciona para aplicativos fora do App Engine, seja no Google Cloud, em outras nuvens ou até mesmo no local. Se você não precisar de todos os recursos disponíveis no Cloud Storage ou se estiver preocupado com seus efeitos no custo, fique à vontade para usar o Blobstore do App Engine.

Além do módulo 16, há uma série de outras migrações possíveis, como Cloud NBS, Cloud Datastore, Cloud Tasks ou Cloud Memorystore. Há também 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 qualquer "ordem" relevante. o número de migrações.

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 (INÍCIO) e do Módulo 15 (FINISH) podem ser encontrados na tabela abaixo. Eles também podem ser acessados no repositório de todas as migrações de codelab do App Engine. 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

Veja 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.