arrow_back

Developing Applications with Google Cloud: Storing Application Data

Sign in Join
Get access to 700+ labs and courses

Developing Applications with Google Cloud: Storing Application Data

Lab 1 hour 30 minutes universal_currency_alt 5 Credits show_chart Intermediate
info This lab may incorporate AI tools to support your learning.
Get access to 700+ labs and courses

Overview

Cloud Client Libraries are the recommended method for calling Google Cloud APIs from your applications. Cloud Client Libraries use the natural conventions and style of the programming language that you're using for your application. Cloud Client Libraries handle low-level communication with the server, including authentication and retry logic.

Firestore is a fast, fully managed, serverless, NoSQL document database built for automatic scaling, high performance, and ease of application development.

Cloud Storage is a unified object storage that lets you serve, analyze, and archive data anywhere in the world.

In this lab, you create a Python application that manages a list of books. You're able to add, edit, and delete books, collecting data like author, title, and description. The initial application stores the data in an in-memory Python dictionary, which causes all books to be lost when the application crashes.

You modify this application to store all the book data in Firestore, and then add the ability to store a cover image for a book, which you persist in Cloud Storage.

What you will learn

In this lab, you learn to:

  • Create a simple Python Flask web application.
  • Create a Firestore database to store application data.
  • Create a Cloud Storage bucket for storing images for use in the application.

Setup and requirements

For each lab, you get a new Google Cloud project and set of resources for a fixed time at no cost.

  1. Click the Start Lab button. If you need to pay for the lab, a pop-up opens for you to select your payment method. On the left is the Lab Details panel with the following:

    • The Open Google Cloud console button
    • Time remaining
    • The temporary credentials that you must use for this lab
    • Other information, if needed, to step through this lab
  2. Click Open Google Cloud console (or right-click and select Open Link in Incognito Window if you are running the Chrome browser).

    The lab spins up resources, and then opens another tab that shows the Sign in page.

    Tip: Arrange the tabs in separate windows, side-by-side.

    Note: If you see the Choose an account dialog, click Use Another Account.
  3. If necessary, copy the Username below and paste it into the Sign in dialog.

    {{{user_0.username | "Username"}}}

    You can also find the Username in the Lab Details panel.

  4. Click Next.

  5. Copy the Password below and paste it into the Welcome dialog.

    {{{user_0.password | "Password"}}}

    You can also find the Password in the Lab Details panel.

  6. Click Next.

    Important: You must use the credentials the lab provides you. Do not use your Google Cloud account credentials. Note: Using your own Google Cloud account for this lab may incur extra charges.
  7. Click through the subsequent pages:

    • Accept the terms and conditions.
    • Do not add recovery options or two-factor authentication (because this is a temporary account).
    • Do not sign up for free trials.

After a few moments, the Google Cloud console opens in this tab.

Note: To view a menu with a list of Google Cloud products and services, click the Navigation menu at the top-left, or type the service or product name in the Search field.

Activate Google Cloud Shell

Google Cloud Shell is a virtual machine that is loaded with development tools. It offers a persistent 5GB home directory and runs on the Google Cloud.

Google Cloud Shell provides command-line access to your Google Cloud resources.

  1. In Cloud console, on the top right toolbar, click the Open Cloud Shell button.

  2. Click Continue.

It takes a few moments to provision and connect to the environment. When you are connected, you are already authenticated, and the project is set to your PROJECT_ID. For example:

gcloud is the command-line tool for Google Cloud. It comes pre-installed on Cloud Shell and supports tab-completion.

  • You can list the active account name with this command:
gcloud auth list

Output:

Credentialed accounts: - @.com (active)

Example output:

Credentialed accounts: - google1623327_student@qwiklabs.net
  • You can list the project ID with this command:
gcloud config list project

Output:

[core] project =

Example output:

[core] project = qwiklabs-gcp-44776a13dea667a6 Note: Full documentation of gcloud is available in the gcloud CLI overview guide .

Task 1. Create and test a simple Python Flask web application

In this task, you create and test a Python application used to store a list of books.

Note: For most languages, indentation is used to make code more readable. Python uses indentation to indicate a block of code, so indentation must be correct. The number of spaces used for indentation must be consistent. Mixing space and tabs for indentation can also cause issues. This lab uses four spaces for Python indentation.

Confirm that Cloud Shell is authorized

  1. To confirm that Cloud Shell is authorized, in Cloud Shell, run the following command:

    gcloud auth list
  2. If you're asked to authorize Cloud Shell, click Authorize.

Create the app directory

To create the app directory, run the following command:

mkdir ~/bookshelf

The application files are created in the ~/bookshelf directory.

Specify and install the requirements

A Python requirements file is a simple text file that lists the dependencies required by your project. To start, there are three modules we need in our requirements file.

Our app is written with Flask, a web framework module that makes it simple to design web applications by using Python. We run the application by using Gunicorn, a Python HTTP server that runs on Linux. Finally, Cloud Logging is used to log information from our application.

  1. To create the requirements file, run the following command:

    cat > ~/bookshelf/requirements.txt <<EOF Flask==2.3.3 gunicorn==21.2.0 google-cloud-logging==3.6.0 EOF

    The requirements.txt file specifies the versions of Flask, Gunicorn, and the Google Cloud logger that are used by the application.

  2. To install the selected versions of the dependencies, run the following command:

    pip3 install -r ~/bookshelf/requirements.txt --user

    pip is the package installer for Python. This pip3 command installs the packages specified in the requirements.txt file for use with Python version 3.

Create the books database implementation

  1. To create the books database code, run the following command:

    cat > ~/bookshelf/booksdb.py <<EOF db = {} # global in-memory python dictionary, key should always be a string next_id = 1 # next book ID to use def get_next_id(): """ Return the next ID. Automatically increments when retrieving one. """ global next_id id = next_id # next ID is 1 higher next_id = next_id + 1 # return a string version of the ID return str(id) def read(book_id): """ Return the details for a single book. """ # retrieve a book from the database by ID data = db[str(book_id)] return data def create(data): """ Create a new book and return the book details. """ # get a new ID for the book book_id = get_next_id() # set the ID in the book data data['id'] = book_id # store book in database db[book_id] = data return data def update(data, book_id): """ Update an existing book, and return the updated book's details. """ # book ID should always be a string book_id_str = str(book_id) # add ID to the book data data['id'] = book_id_str # update book in the database db[book_id_str] = data return data def delete(book_id): """ Delete a book in the database. """ # remove book from database del db[str(book_id)] # no return required def list(): """ Return a list of all books in the database. """ # empty list of books books = [] # retrieve each item in database and add to the list for k in db: books.append(db[k]) # return the list return books EOF

    Books are being stored in a Python dictionary, which is a data structure for storing key-value pairs. The key must be unique, so the get_next_id() function creates a new ID each time it's called.

    The read(book_id) function retrieves the item corresponding to the provided book_id.

    The create(data) function adds the book to the database by getting a new ID, storing that ID in the data for the book, and then storing the data entry in the dictionary.

    The update(data, book_id) function updates the book in the database by storing the provided ID in the data for the book, and then storing the data entry in the dictionary.

    The delete(book_id) function removes the entry in the database with the provided book_id key.

    The list() function returns a Python list that contains each book in the database. It retrieves this list by looping through the dictionary and retrieving each item. Each item stores its ID by using the id key.

Create the HTML template files

Templates are files that contain both static data and placeholders for dynamic data. You render a template to produce a final HTML file.

  1. To create the base template, run the following command:

    mkdir ~/bookshelf/templates cat > ~/bookshelf/templates/base.html <<EOF <!DOCTYPE html> <html lang="en"> <head> <title>Bookshelf</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"> </head> <body> <div class="navbar navbar-default"> <div class="container"> <div class="navbar-header"> <div class="navbar-brand">Bookshelf</div> </div> <ul class="nav navbar-nav"> <li><a href="/">Books</a></li> </ul> </div> </div> <div class="container"> {% block content %}{% endblock %} </div> </body> </html> EOF

    Each page in the application has the same basic layout with a different body. Each of the three main templates (list, view, form) extends this base template by specifying the content to go in the center of the page.

  2. To create the list template, run the following command:

    cat > ~/bookshelf/templates/list.html <<EOF {% extends "base.html" %} {% block content %} <h3>Books</h3> <a href="/books/add" class="btn btn-success btn-sm"> <i class="glyphicon glyphicon-plus"></i> Add book </a> {% for book in books %} <div class="media"> <a href="/books/{{book.id}}"> <div class="media-body"> <h4>{{book.title}}</h4> <p>{{book.author}}</p> </div> </a> </div> {% else %} <p>No books found</p> {% endfor %} {% endblock %} EOF

    The list template loops through the list of books sent to the template and displays the title, author, and link for each book, with the link that takes the user to the view page for that book.

  3. To create the view template, run the following command:

    cat > ~/bookshelf/templates/view.html <<EOF {% extends "base.html" %} {% block content %} <h3>Book</h3> <div class="btn-group"> <a href="/books/{{book.id}}/edit" class="btn btn-primary btn-sm"> <i class="glyphicon glyphicon-edit"></i> Edit book </a> <a href="/books/{{book.id}}/delete" class="btn btn-danger btn-sm"> <i class="glyphicon glyphicon-trash"></i> Delete book </a> </div> <div class="media"> <div class="media-body"> <h4 class="book-title"> {{book.title}} <small>{{book.publishedDate}}</small> </h4> <h5 class="book-author">By {{book.author|default('Unknown', True)}}</h5> <p class="book-description">{{book.description}}</p> </div> </div> {% endblock %} EOF

    The view template shows the book details: the title, author, date published, and description. It also provides two buttons: one to edit the book (sending the user to the form template), and one to delete the book (which deletes the book and sends the user back to the book list).

  4. To create the form template, run the following command:

    cat > ~/bookshelf/templates/form.html <<EOF {# [START form] #} {% extends "base.html" %} {% block content %} <h3>{{action}} book</h3> <form method="POST" enctype="multipart/form-data"> <div class="form-group"> <label for="title">Title</label> <input type="text" name="title" id="title" value="{{book.title}}" class="form-control"/> </div> <div class="form-group"> <label for="author">Author</label> <input type="text" name="author" id="author" value="{{book.author}}" class="form-control"/> </div> <div class="form-group"> <label for="publishedDate">Date Published</label> <input type="text" name="publishedDate" id="publishedDate" value="{{book.publishedDate}}" class="form-control"/> </div> <div class="form-group"> <label for="description">Description</label> <textarea name="description" id="description" class="form-control">{{book.description}}</textarea> </div> <button type="submit" class="btn btn-success">Save</button> </form> {% endblock %} {# [END form] #} EOF

    The form template serves two purposes. When updating a book, the template displays the current book details in editable boxes. When the Save button is clicked, the updated form fields are saved into the database.

    When creating a new book, the book details boxes start empty. When the Save button is clicked, the book data is saved into the database as a new book.

Create the main code file

  1. To create the main code file, run the following command:

    cat > ~/bookshelf/main.py <<EOF from flask import current_app, Flask, redirect, render_template from flask import request, url_for import logging from google.cloud import logging as cloud_logging import booksdb app = Flask(__name__) app.config.update( SECRET_KEY='secret', # don't store SECRET_KEY in code in a production app MAX_CONTENT_LENGTH=8 * 1024 * 1024, ) app.debug = True app.testing = False # configure logging if not app.testing: logging.basicConfig(level=logging.INFO) # attach a Cloud Logging handler to the root logger client = cloud_logging.Client() client.setup_logging() def log_request(req): """ Log request """ current_app.logger.info('REQ: {0} {1}'.format(req.method, req.url)) @app.route('/') def list(): """ Display all books. """ log_request(request) # get list of books books = booksdb.list() # render list of books return render_template('list.html', books=books) @app.route('/books/<book_id>') def view(book_id): """ View the details of a specified book. """ log_request(request) # retrieve a specific book book = booksdb.read(book_id) # render book details return render_template('view.html', book=book) @app.route('/books/add', methods=['GET', 'POST']) def add(): """ If GET, show the form to collect details of a new book. If POST, create the new book based on the specified form. """ log_request(request) # Save details if form was posted if request.method == 'POST': # get book details from form data = request.form.to_dict(flat=True) # add book book = booksdb.create(data) # render book details return redirect(url_for('.view', book_id=book['id'])) # render form to add book return render_template('form.html', action='Add', book={}) @app.route('/books/<book_id>/edit', methods=['GET', 'POST']) def edit(book_id): """ If GET, show the form to collect updated details for a book. If POST, update the book based on the specified form. """ log_request(request) # read existing book details book = booksdb.read(book_id) # Save details if form was posted if request.method == 'POST': # get book details from form data = request.form.to_dict(flat=True) # update book book = booksdb.update(data, book_id) # render book details return redirect(url_for('.view', book_id=book['id'])) # render form to update book return render_template('form.html', action='Edit', book=book) @app.route('/books/<book_id>/delete') def delete(book_id): """ Delete the specified book and return to the book list. """ log_request(request) # delete book booksdb.delete(book_id) # render list of remaining books return redirect(url_for('.list')) # this is only used when running locally if __name__ == '__main__': app.run(host='127.0.0.1', port=8080, debug=True) EOF

    main.py is the entry point for the application. The file implements the Flask application, which specifies the web URL routing, renders the templates, and manages the book database. Feel free to examine the code, which is well commented.

Try the application

  1. To check the contents of the bookshelf directory, run the following command:

    cd ~ ls -R bookshelf

    You should see a list that contains two Python files, a requirements file, and four template files:

    bookshelf: booksdb.py main.py requirements.txt templates bookshelf/templates: base.html form.html list.html view.html
  2. To run the Gunicorn HTTP server, run the following command:

    cd ~/bookshelf; ~/.local/bin/gunicorn -b :8080 main:app

    If you have successfully created the files, the application should now be hosted on port 8080.

  3. To run the application in the web browser, click Web Preview, and then select Preview on port 8080.

    A new tab is opened in the browser, and the application is running. This is the root URL, which displays a list of all existing books. There are no books yet.

    Note: If asked to authorize Cloud Shell, click Authorize.
  4. In the application tab, click +Add book.

  5. Enter a title, author, date, and description for a book, real or imaginary, and then click Save.

    You're returned to the view page, and your book details are shown. You can edit or delete your book by clicking the appropriate button.

  6. At the top of the page, click Books.

    You're returned to the view page, and any books you have added are shown in the list.

    You can navigate the application to add, edit, or delete books if you want.

  7. In Cloud Shell, to quit the application, enter CTRL-C.

To verify the objective, click Check my progress. Create and test a simple Python Flask web application

Task 2. Use Firestore for the books database

In this task, you use a Firestore database to store the book data.

The application currently uses an in-memory Python dictionary. The books are lost whenever the app exits or crashes. A better solution is to use Firestore for the persistent database.

Create the Firestore database

  1. From Navigation menu (), navigate to View All Products > Databases.

  2. Click the Pin icon next to Firestore to add it to the navigation menu, then click Firestore.

  3. Click Create a Firestore Database.

  4. In Configuration options, select Firestore Native.

  5. For Location type, click Region.

    You should not use a Multi-Region for Firestore in this lab.

  6. For Region, select , and then click Create Database.

    Note: The exact region should be selected for the location. If you don't see this region in the dropdown list, check that you have correctly selected Region, not Multi-Region, for the Location Type.

    If there are no regions in the dropdown list, cancel and return to the previous page, and then try the database creation process again.

    Creating the database might take a few minutes. Collections in Firestore are automatically created when you add a document to a collection, so you don't need to create a book collection right now.

    Creating the Firestore database causes the Firestore API to be enabled.

Modify the application to require the Python client for Firestore

The Python client for the Firestore API is used to access Firestore data from your applications. This client's package name is google-cloud-firestore, and the version to use is 2.12.0.

  1. Modify the application to require version 2.12.0 of the google-cloud-firestore package.
Note: You can use any file editor you'd like, including nano, vi, and the Cloud Code editor.
  1. To install the updated dependency, run the following command:

    pip3 install -r ~/bookshelf/requirements.txt --user

Modify the application to store book data in Firestore

Now that the application requires the google-cloud-firestore package, you can use it in the books database implementation.

With Firestore, the book data is stored in the Firestore database. If your data had a guaranteed unique field that could be used as an ID, you might choose to use that as the ID in Firestore. In this case, the data you're using doesn't have a unique field. Firestore can automatically create the ID for you when you create a new book.

Here's an example of how the Firestore client is used:

from google.cloud import firestore # get the client db = firestore.Client() # create a new document data = {"name": "Sue", "role": "treasurer"} member_ref = db.collection("members").document() member_ref.set(data) member_id = member_ref.get().id # retrieve a document member_ref = db.collection("members").document(member_id) member = member_ref.get() if member.exists: print(f"Document data: {member.to_dict()}") else: print("Member not found.") # update a document new_data = {"name": "Sue", "role": "president"} member_ref = db.Collection("members").document(member_id) member_ref.set(new_data) # get all documents in order members = db.collection("members").order_by("name").stream() for member in members: print(f"{member.id} => {member.to_dict()}") # delete a member member_ref = db.Collection("members").document(member_id) member_ref.delete()

Next, you modify the books database implementation to use Firestore.

Note: Remember, four spaces should be used for Python indentation.

The current implementation uses a global variable named db, which is an in-memory Python dictionary. It also uses the variable next_id and the function get_next_id(), which creates the IDs for items stored in the dictionary.

Firestore manages the creation of IDs for you. Use a collection named books. You must add the created ID to the Python dictionary that contains the details of a book before you return it to the caller.

Note: The hints hide the code changes you need to make. You can try to write the code yourself, or click the hint buttons to get the code to add.
  1. In a file editor, open the file ~/bookshelf/booksdb.py.

  2. Remove the following lines from the file:

    db = {} # global in-memory python dictionary, key should always be a string next_id = 1 # next book ID to use def get_next_id(): """ Return the next ID. Automatically increments when retrieving one. """ global next_id id = next_id # next ID is 1 higher next_id = next_id + 1 # return a string version of the ID return str(id)

    The in-memory database and ID creation functionality are not needed in this implementation.

  3. Add code that imports the Firestore client.

  1. Create a function that converts a Firestore document to a dictionary.

When you return a book to the caller, you don't want the caller to have to understand Firestore documents. The interfaces for the books database functions should remain unchanged, so this implementation continues to return and accept books as Python dictionaries.

The ID for the book must be added to the dictionary before it's returned.

Create a function named document_to_dict() which takes a Firestore document as the input parameter and returns a dictionary. The dictionary includes the key-value pairs in the document, and also returns the document ID as the value for the key id. If the document does not exist, you should return None.

  1. Modify the read() function to retrieve a book from the Firestore collection books. Your updated function should call the function document_to_dict().
  1. Modify the create() function to create a book in the Firestore collection books.
  1. Modify the update() function to update a book in the Firestore collection books.
  1. Modify the delete() function to delete a book in the Firestore collection books.
  1. Modify the list() function to return a list of all books from the Firestore collection books, sorted by title.

That's it! By updating booksdb.py, you modify the app to use Firestore as the database without having to change the calling code.

Test the updated application

  1. In Cloud Shell, run the following command:

    cd ~/bookshelf; ~/.local/bin/gunicorn -b :8080 main:app

    If you have successfully updated the files, the application should now be hosted on port 8080.

    Note: If you're getting an import error, ensure that you have used pip3 to install the updated requirements that now include the google.cloud.firestore package. Note: If asked to authorize Cloud Shell, click Authorize.
  2. To run the application in the web browser, click Web Preview, and then select Preview on port 8080.

    There are no books in the database because books were being stored into an in-memory dictionary.

  3. In the application tab, click +Add book.

  4. Enter the following information into the form:

    Field Value
    Title Hamlet
    Author William Shakespeare
    Date Published 1603
    Description A prince contemplates life, death, and revenge, but mostly just makes puns.
  5. Click Save.

    You're returned to the view page, and your book details are shown.

  6. At the top of the page, click Books.

    You're returned to the list page, and Hamlet is shown in the list.

    Note: You can add other books if you want, but do not modify Hamlet, which is needed for the next task.
  7. In the Google Cloud console, in the Navigation menu (), click Firestore.

    Note: Even if you're already on the Firestore console page, you might need to navigate to the page again to see the database.
  8. Click (default).

    You should see that a document has been created in the books collection for Hamlet.

  9. In Cloud Shell, to quit the application, enter CTRL-C.

Troubleshooting task 2

Errors that are encountered in your application are currently printed in Cloud Shell. If your application is not working or saving the data to Firestore, use the error information to debug and fix the issue.

If you are having problems getting the books database code to work, the command in the following hint provides a command to replace the entire booksdb.py file with working code.

To verify the objective, click Check my progress. Use Firestore for the books database

Task 3. Use Cloud Storage for book covers

In this task, you use Cloud Storage to store cover images for the books.

A database is typically not the right location to store images. You can't store files in Cloud Shell, because you would want to eventually host the application somewhere else. Cloud Storage is a perfect solution for storing assets that you want to share. Cloud Storage is the primary object store for Google Cloud.

Create the Cloud Storage bucket

To use Cloud Storage, you need to create a Cloud Storage bucket, which is a basic container to hold your data.

  1. In the Google Cloud console, in the Navigation menu (), click Cloud Storage > Buckets.

  2. Click +Create.

  3. For the bucket name, use:

    {{{ project_0.project_id | project_id }}}-covers
  4. Click Continue.

  5. Select Region.

  6. Select .

  7. Click Continue.

  8. Leave the storage class unchanged, and click Continue.

  9. Clear Enforce public access prevention on this bucket.

    You're leaving the Access Control as Uniform, which uses bucket-level permissions for all objects added to the bucket.

  10. Click Create.

    To allow the covers to be visible in the application, you must allow all users to read objects within the bucket.

  11. Select the Permissions tab and then click Grant Access.

  12. For New principals, enter allUsers.

  13. For role, select Cloud Storage Legacy > Storage Legacy Object Reader.

    Note: The Cloud Storage > Storage Object Viewer role includes permission to list objects in a bucket, which is not needed for this application. The Cloud Storage Legacy > Storage Legacy Object Reader role only permits the retrieval of objects, which is more appropriate for this use case.
  14. Click Save.

  15. If you're asked for confirmation, click Allow Public Access.

Update the requirements file

  1. In Cloud Shell, in a file editor, open the file ~/bookshelf/requirements.txt.

  2. In the ~/bookshelf/requirements.txt file, add the following line:

    google-cloud-storage==2.10.0

    The requirements.txt file should now look like this:

    Flask==2.3.3 gunicorn==21.2.0 google-cloud-logging==3.6.0 google-cloud-firestore==2.12.0 google-cloud-storage==2.10.0
  3. Save the file.

  4. To install the updated dependency, in Cloud Shell, run the following command:

    pip3 install -r ~/bookshelf/requirements.txt --user

Create code that uploads images to Cloud Storage

The storage.py file contains code to upload a cover image to Cloud Storage.

  1. To create the storage.py file, run the following command:

    cat > ~/bookshelf/storage.py <<EOF from __future__ import absolute_import import datetime import os from flask import current_app from werkzeug.exceptions import BadRequest from werkzeug.utils import secure_filename from google.cloud import storage def _check_extension(filename, allowed_extensions): """ Validates that the filename's extension is allowed. """ _, ext = os.path.splitext(filename) if (ext.replace('.', '') not in allowed_extensions): raise BadRequest( '{0} has an invalid name or extension'.format(filename)) def _safe_filename(filename): """ Generates a safe filename that is unlikely to collide with existing objects in Cloud Storage. filename.ext is transformed into filename-YYYY-MM-DD-HHMMSS.ext """ filename = secure_filename(filename) date = datetime.datetime.utcnow().strftime("%Y-%m-%d-%H%M%S") basename, extension = filename.rsplit('.', 1) return "{0}-{1}.{2}".format(basename, date, extension) def upload_file(file_stream, filename, content_type): """ Uploads a file to a given Cloud Storage bucket and returns the public url to the new object. """ _check_extension(filename, current_app.config['ALLOWED_EXTENSIONS']) filename = _safe_filename(filename) # build the name of the bucket bucket_name = os.getenv('GOOGLE_CLOUD_PROJECT') + '-covers' client = storage.Client() # create a bucket object bucket = client.bucket(bucket_name) # create an object in the bucket for the specified path blob = bucket.blob(filename) # upload the contents of the string into the object blob.upload_from_string( file_stream, content_type=content_type) # get the public URL for the object, which is used for storing a reference # to the image in the database and displaying the image in the app url = blob.public_url return url def upload_image(img): """ Upload the user-uploaded file to Cloud Storage and retrieve its publicly accessible URL. """ if not img: return None public_url = upload_file( img.read(), img.filename, img.content_type ) return public_url EOF

    The upload_file() function accepts a file stream, a filename, and the content type of the file. The filename's extension is first validated against an approved list of extensions that is created in a future step. The filename is then appended with the current date and time, so that book images that use the same filename when uploading won't conflict. The rest of the function interacts with Cloud Storage.

    The bucket name is first built by using the project ID:

    bucket_name = os.getenv('GOOGLE_CLOUD_PROJECT') + '-covers'

    Next, a reference to an object for the specified bucket and filename is created, and the contents of the image file are uploaded:

    client = storage.Client() # create a bucket object bucket = client.bucket(bucket_name) # create an object in the bucket for the specified path blob = bucket.blob(filename)

    The file data is then uploaded to Cloud Storage, and the file is made public so that it can be displayed in the web app:

    # upload the contents of the string into the object blob.upload_from_string( file_stream, content_type=content_type)

    The URL is then returned so that it can be stored in the book database and used to display the image.

Modify templates to display book images

A template is rendered with specific data to produce a web page.

The base template doesn't need to be changed, but the content templates (form, list, view) must be changed to display and upload the book covers.

  1. In a file editor, open the file ~/bookshelf/templates/form.html.

    The form should be modified to collect the image file.

  2. Near the bottom of the form, above the Save button control, add the following lines:

    <div class="form-group"> <label for="image">Cover Image</label> <input type="file" name="image" id="image" class="form-control"/> </div> <div class="form-group hidden"> <label for="imageUrl">Cover Image URL</label> <input type="text" name="imageUrl" id="imageUrl" value="{{book.imageUrl}}" class="form-control"/> </div>

    The image input allows the user to upload an image file, and also displays the current image. The imageUrl input is hidden, but it stores the public URL of the image, which is added to the database entry for the book.

    The form should now look like this:

    <form method="POST" enctype="multipart/form-data"> <div class="form-group"> <label for="title">Title</label> <input type="text" name="title" id="title" value="{{book.title}}" class="form-control"/> </div> <div class="form-group"> <label for="author">Author</label> <input type="text" name="author" id="author" value="{{book.author}}" class="form-control"/> </div> <div class="form-group"> <label for="publishedDate">Date Published</label> <input type="text" name="publishedDate" id="publishedDate" value="{{book.publishedDate}}" class="form-control"/> </div> <div class="form-group"> <label for="description">Description</label> <textarea name="description" id="description" class="form-control">{{book.description}}</textarea> </div> <div class="form-group"> <label for="image">Cover Image</label> <input type="file" name="image" id="image" class="form-control"/> </div> <div class="form-group hidden"> <label for="imageUrl">Cover Image URL</label> <input type="text" name="imageUrl" id="imageUrl" value="{{book.imageUrl}}" class="form-control"/> </div> <button type="submit" class="btn btn-success">Save</button> </form>
  3. Save the file.

  4. In a file editor, open the file ~/bookshelf/templates/view.html.

    The book image should be displayed to the left of the book information.

  5. After the line <div class="media">, add the following lines:

    <div class="media-left"> {% if book.imageUrl %} <img class="book-image" src="{{book.imageUrl}}" width="128" height="192" alt="book cover"> {% else %} <img class="book-image" src="https://storage.googleapis.com/cloud-training/devapps-foundations/no-cover.png" width="128" height="192" alt="no book cover"> {% endif %} </div>

    This adds a new section to the left of the book details. If the book image exists, it displays that image in this section. Otherwise, it displays a placeholder image.

    The media div now looks like this:

    <div class="media"> <div class="media-left"> {% if book.imageUrl %} <img class="book-image" src="{{book.imageUrl}}" width="128" height="192" alt="book cover"> {% else %} <img class="book-image" src="https://storage.googleapis.com/cloud-training/devapps-foundations/no-cover.png" width="128" height="192" alt="no book cover"> {% endif %} </div> <div class="media-body"> <h4 class="book-title"> {{book.title}} <small>{{book.publishedDate}}</small> </h4> <h5 class="book-author">By {{book.author|default('Unknown', True)}}</h5> <p class="book-description">{{book.description}}</p> </div> </div>
  6. Save the file.

  7. In a file editor, open the file ~/bookshelf/templates/list.html.

    The book image should now be displayed to the left of each book in the list.

  8. After the line <a href="/books/{{book.id}}">, add the following lines:

    <div class="media-left"> {% if book.imageUrl %} <img src="{{book.imageUrl}}" width="128" height="192" alt="book cover"> {% else %} <img src="https://storage.googleapis.com/cloud-training/devapps-foundations/no-cover.png" width="128" height="192" alt="no book cover"> {% endif %} </div>

    This contains the same code that you added to the view template.

Modify main.py

The main code file should upload the image to Cloud Storage when the form is posted, and the image URL should be added to the book data.

  1. In a file editor, open the file ~/bookshelf/main.py.

  2. After the import for booksdb, add the following line:

    import storage
  3. After the import lines, add the upload_image_file() method:

    def upload_image_file(img): """ Upload the user-uploaded file to Cloud Storage and retrieve its publicly accessible URL. """ if not img: return None public_url = storage.upload_file( img.read(), img.filename, img.content_type ) current_app.logger.info( 'Uploaded file %s as %s.', img.filename, public_url) return public_url

    This function calls the library function you created in storage.py to upload the cover file to Cloud Storage. It returns the public URL for the uploaded file.

  4. Add the following line to the app.config.update section:

    ALLOWED_EXTENSIONS=set(['png', 'jpg', 'jpeg', 'gif']),

    This limits the extensions that are allowed when uploading a book cover. The configuration should now look like this:

    app.config.update( SECRET_KEY='secret', MAX_CONTENT_LENGTH=8 * 1024 * 1024, ALLOWED_EXTENSIONS=set(['png', 'jpg', 'jpeg', 'gif']), )
  5. In the add() function, after the data = request.form.to_dict(flat=True) line, add the following code:

    image_url = upload_image_file(request.files.get('image')) # If an image was uploaded, update the data to point to the image. if image_url: data['imageUrl'] = image_url

    This code calls the upload_image_file function to upload the image that was added in the form. It also adds the image URL to the book data.

  6. In the edit() function, after the data = request.form.to_dict(flat=True) line, add the following code:

    image_url = upload_image_file(request.files.get('image')) # If an image was uploaded, update the data to point to the image. if image_url: data['imageUrl'] = image_url

    This is the same code you added to the add() function.

  7. Save the file.

Test the updated application

  1. In Cloud Shell, run the following command:

    cd ~/bookshelf; ~/.local/bin/gunicorn -b :8080 main:app

    If you have successfully updated the files, the application should now be hosted on port 8080.

    Note: If asked to authorize Cloud Shell, click Authorize.
  2. To run the application in the web browser, click Web Preview, and then select Preview on port 8080.

    The books that were added when the application used Firestore should be displayed. Each book shows the placeholder cover image, because image URLs were not previously added to the database.

  3. Click Hamlet, and then click Edit book.

  4. Right-click this Hamlet book cover image, and save it to your computer as hamlet.png:

  5. In the Bookshelf app, for Cover Image, click Choose File.

  6. Select the file that you downloaded (hamlet.png), and click Open.

  7. Click Save.

    The book image for Hamlet should now be displayed.

  8. In the Google Cloud console, in the Navigation menu (), click Cloud Storage > Buckets.

  9. Click the bucket name (-covers).

    The cover image is saved to Cloud Storage.

To verify the objective, click Check my progress. Use Cloud Storage for book covers

Congratulations!

You successfully tested an application in Cloud Shell. You modified the application to use Cloud Client Libraries to store its data in Firestore and to store images in Cloud Storage.

Next Steps/Learn More

End your lab

When you have completed your lab, click End Lab. Google Cloud Skills Boost removes the resources you’ve used and cleans the account for you.

You will be given an opportunity to rate the lab experience. Select the applicable number of stars, type a comment, and then click Submit.

The number of stars indicates the following:

  • 1 star = Very dissatisfied
  • 2 stars = Dissatisfied
  • 3 stars = Neutral
  • 4 stars = Satisfied
  • 5 stars = Very satisfied

You can close the dialog box if you don't want to provide feedback.

For feedback, suggestions, or corrections, please use the Support tab.

Copyright 2024 Google LLC All rights reserved. Google and the Google logo are trademarks of Google LLC. All other company and product names may be trademarks of the respective companies with which they are associated.

Before you begin

  1. Labs create a Google Cloud project and resources for a fixed time
  2. Labs have a time limit and no pause feature. If you end the lab, you'll have to restart from the beginning.
  3. On the top left of your screen, click Start lab to begin

This content is not currently available

We will notify you via email when it becomes available

Great!

We will contact you via email if it becomes available

One lab at a time

Confirm to end all existing labs and start this one

Use private browsing to run the lab

Use an Incognito or private browser window to run this lab. This prevents any conflicts between your personal account and the Student account, which may cause extra charges incurred to your personal account.