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
- Um projeto do Google Cloud Platform com uma conta de faturamento do GCP ativa
- Habilidades básicas em Python
- Conhecimento prático de comandos comuns do Linux
- Conhecimento básico sobre desenvolvimento e implantação de apps do App Engine
- Um aplicativo do App Engine do módulo 0 funcional (extraído do repositório)
Pesquisa
Como você vai usar este tutorial?
Como você classificaria sua experiência com Python?
Como você classificaria sua experiência de uso dos serviços do Google Cloud?
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.
- INICIAR: pasta do módulo 0 (Python 2)
- CONCLUIR: pasta do módulo 15 (Python 2)
- Repositório completo (para clonar ou fazer o download do arquivo ZIP)
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:
- Conheça melhor a ferramenta de linha de comando
gcloud - Reimplantar o aplicativo de amostra com
gcloud app deploy - 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.

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:
- 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 deose do renderizador de modelos do Django,google.appengine.ext.webapp.template, não é mais necessário. Portanto, eles estão sendo removidos. - Importe a API Blobstore:
google.appengine.ext.blobstore - Importe os gerenciadores do Blobstore encontrados no framework
webapporiginal. Eles não estão disponíveis nowebapp2:google.appengine.ext.webapp.blobstore_handlers - 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:

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:
- A solicitação do manipulador principal
GETnão busca mais as visitas mais recentes para exibição. Em vez disso, ele pede que o usuário faça um upload. - Quando um usuário final envia um arquivo para upload ou pula esse processo, um
POSTdo formulário passa o controle para o novoUploadHandler, derivado degoogle.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler. - O método
POSTdeUploadHandlerfaz o upload, chamastore_visit()para registrar a visita e aciona um redirecionamento HTTP 307 para enviar o usuário de volta a "/", onde... - O método
POSTdo manipulador principal consulta (viafetch_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. - 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).
- Se um usuário final clicar no link "Visualizar" de qualquer visita com um vídeo enviado, será feita uma solicitação
GETpara um novoViewBlobHandler, derivado degoogle.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. - 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
POSTpara 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 parablobstore.create_upload_url. Essa chamada gera o URL para o qual o formulárioPOSTs, chamando o gerenciador de upload para enviar o arquivo ao Blobstore. - Em
UploadHandler.post, há uma chamada parablobstore_handlers.BlobstoreUploadHandler.get_uploads. Essa é a verdadeira mágica que coloca o arquivo no Blobstore e retorna um ID exclusivo e persistente para ele, oBlobKey. - Em
ViewBlobHandler.get, chamarblobstore_handlers.BlobstoreDownloadHandler.sendcom umBlobKeyde 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:

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:
- É 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.
- 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:

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:

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.

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:
Em 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:

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/imagesconsole.cloud.google.com/storage/browser/staging.PROJECT_ID.appspot.com- Os links de armazenamento acima dependem da sua
PROJECT_IDe *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:
- O serviço Blobstore do App Engine está sujeito às cotas e limites de dados armazenados. Consulte essa página e a página de preços dos serviços agrupados legados.
- O serviço App Engine Datastore é fornecido pelo Cloud Datastore (Cloud Firestore no modo Datastore), que também tem um nível sem custo financeiro. Consulte a página de preços 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 | N/A | |
Módulo 15 (este codelab) | N/A |
Recursos on-line
Confira abaixo recursos on-line que podem ser relevantes para este tutorial:
App Engine
- Serviço Blobstore do App Engine
- Cotas e limites de dados armazenados do App Engine
- Documentação do App Engine
- Tempo de execução do Python 2 no App Engine (ambiente padrão)
- Como usar bibliotecas integradas do App Engine no App Engine do Python 2
- Informações sobre preços e cotas do App Engine
- Lançamento da plataforma App Engine de segunda geração (2018)
- Comparação entre plataformas de primeira e segunda geração
- Suporte de longo prazo para ambientes de execução legados
- Repositório de exemplos de migração da documentação
- Repositório de exemplos de migração gerado pela comunidade
Google Cloud
- Python no Google Cloud Platform
- Bibliotecas de cliente do Python para Google Cloud
- Nível "Sempre sem custo financeiro" do Google Cloud
- SDK do Google Cloud (ferramenta de linha de comando gcloud)
- Toda a documentação do Google Cloud
Python
- Sistemas de modelos Django e Jinja2
webapp2framework da Web- Documentação do
webapp2(link em inglês) - Vínculos do
webapp2_extras - Documentação do
webapp2_extrasJinja2 (link em inglês)
Vídeos
- Serverless Migration Station (em inglês)
- Expedições sem servidor
- Inscreva-se no Google Cloud Tech
- Inscreva-se no Google Developers
Licença
Este conteúdo está sob a licença Atribuição 2.0 Genérica da Creative Commons.