Executing Code Asynchronously

Learn how to use task queues and Cloud Vision to label user-uploaded images as a task executing in the background.

Vision can detect and extract information about entities within an image, and let you assign confidence levels to certain key image attributes, such as locations, products, or activities. For example, for a picture of a person, Vision can give you confidence levels about whether the person is sad, happy, angry, etc, so that you can label appropriately.

This page and sample are part of an extended learning example of a simple blog application where users can upload posts. You should already be familiar with the Go programming language and basic web development. To start from the beginning, go to Building an App with Go.

Costs

There are no costs associated with running this tutorial. Running this sample app alone does not exceed your free quota.

Before you begin

If you have completed the Building an App with Go guide, skip this section. Otherwise, complete the following steps:

  1. Complete the tasks in the Before you begin section in Setting Up Your Project and Application. Then, come back to this page.

  2. In this example, you will add code to the gophers-5 sample application.

    Download the gophers-5 sample and its dependencies to your local machine:

    go get -u -d -v github.com/GoogleCloudPlatform/golang-samples/appengine/gophers/gophers-5/...
    
  3. Navigate to the gophers-5 directory:

    cd go/src/github.com/GoogleCloudPlatform/golang-samples/appengine/gophers/gophers-5
    
  4. Complete steps 2-6 of Adding Firebase to your project from the Authenticating Users page.

  5. Enable Vision:

    Enable Vision

Structuring your application

This sample project has the following structure:

  • go-app/: Project root directory.
    • app.yaml: Configuration settings for your App Engine application.
    • main.go: Your application code.
    • index.html: HTML template to display your homepage.
    • static/: Directory to store your static files.
      • style.css: Stylesheet that formats the look of your HTML files.
      • gcp-gopher.svg: Gopher image.
      • index.js: Configures the Firebase Authentication user interface and handles authentication requests.

Setting up image labels

Configure the backend that adds labels and images to user-uploaded posts. Note that this sample uses Vision.

  1. Download the new packages to your development environment using the following command:

    go get -u cloud.google.com/go/storage cloud.google.com/go/vision/apiv1 github.com/satori/go.uuid golang.org/x/net/context google.golang.org/appengine/delay
    

    The sample uses the following packages:

  2. Add the following packages to your list of imports in your main.go file:

    "context"
    "io"
    "path"
    "strings"
    
    "cloud.google.com/go/storage"
    vision "cloud.google.com/go/vision/apiv1"
    uuid "github.com/satori/go.uuid"
    "google.golang.org/appengine/delay"

  3. Define a post's image description as a struct with two fields: a Description field for the image's label and a Score field for a value ranging from 0 (no confidence) to 1 (very high confidence) for how accurately the label applies to the given image:

    // A Label is a description for a post's image.
    type Label struct {
    	Description string
    	Score       float32
    }
    

    The labels with the five highest scores will show under the image. You will access these fields later with the GetDescription() function and the GetScore() function.

  4. Add the ImageURL and Labels field to your Post data structure:

    type Post struct {
    	Author   string
    	UserID   string
    	Message  string
    	Posted   time.Time
    	ImageURL string
    	Labels   []Label
    }
    

    The ImageURL field is a public Cloud Storage URL to the uploaded image.

Adding labels to an image

When a user uploads an image to your page, your indexHandler function will add the image to your Cloud Storage bucket while asynchronously adding labels to the given image.

  1. Create the labelFunc variable which creates a task to label the uploaded image with Cloud Vision. This task is added to the task queue, so that work can be executed in the background. If a user uploads an image in a post, your indexHandler function calls the labelFunc function.

    // labelFunc will be called asynchronously as a Cloud Task. labelFunc can
    // be executed by calling labelFunc.Call(ctx, postID). If an error is returned
    // the function will be retried.
    var labelFunc = delay.Func("label-image", func(ctx context.Context, id int64) error {
    	// Get the post to label.
    	k := datastore.NewKey(ctx, "Post", "", id, nil)
    	post := Post{}
    	if err := datastore.Get(ctx, k, &post); err != nil {
    		log.Errorf(ctx, "getting Post to label: %v", err)
    		return err
    	}
    	if post.ImageURL == "" {
    		// Nothing to label.
    		return nil
    	}
    
    	// Create a new vision client.
    	client, err := vision.NewImageAnnotatorClient(ctx)
    	if err != nil {
    		log.Errorf(ctx, "NewImageAnnotatorClient: %v", err)
    		return err
    	}
    	defer client.Close()
    
    	// Get the image and label it.
    	image := vision.NewImageFromURI(post.ImageURL)
    	labels, err := client.DetectLabels(ctx, image, nil, 5)
    	if err != nil {
    		log.Errorf(ctx, "Failed to detect labels: %v", err)
    		return err
    	}
    
    	for _, l := range labels {
    		post.Labels = append(post.Labels, Label{
    			Description: l.GetDescription(),
    			Score:       l.GetScore(),
    		})
    	}
    
    	// Update the database with the new labels.
    	if _, err := datastore.Put(ctx, k, &post); err != nil {
    		log.Errorf(ctx, "Failed to update image: %v", err)
    		return err
    	}
    	return nil
    })
    

  2. Create the uploadFileFromForm function which confirms the user-uploaded file is an image, then creates and returns the image's public Cloud Storage URL.

    // uploadFileFromForm uploads a file if it's present in the "image" form field.
    func uploadFileFromForm(ctx context.Context, r *http.Request) (url string, err error) {
    	// Read the file from the form.
    	f, fh, err := r.FormFile("image")
    	if err == http.ErrMissingFile {
    		return "", nil
    	}
    	if err != nil {
    		return "", err
    	}
    
    	// Ensure the file is an image. http.DetectContentType only uses 512 bytes.
    	buf := make([]byte, 512)
    	if _, err := f.Read(buf); err != nil {
    		return "", err
    	}
    	if contentType := http.DetectContentType(buf); !strings.HasPrefix(contentType, "image") {
    		return "", fmt.Errorf("not an image: %s", contentType)
    	}
    	// Reset f so subsequent calls to Read start from the beginning of the file.
    	f.Seek(0, 0)
    
    	// Create a storage client.
    	client, err := storage.NewClient(ctx)
    	if err != nil {
    		return "", err
    	}
    	storageBucket := client.Bucket(firebaseConfig.StorageBucket)
    
    	// Random filename, retaining existing extension.
    	u, err := uuid.NewV4()
    	if err != nil {
    		return "", fmt.Errorf("generating UUID: %v", err)
    	}
    	name := u.String() + path.Ext(fh.Filename)
    
    	w := storageBucket.Object(name).NewWriter(ctx)
    
    	// Warning: storage.AllUsers gives public read access to anyone.
    	w.ACL = []storage.ACLRule{{Entity: storage.AllUsers, Role: storage.RoleReader}}
    	w.ContentType = fh.Header.Get("Content-Type")
    
    	// Entries are immutable, be aggressive about caching (1 day).
    	w.CacheControl = "public, max-age=86400"
    
    	if _, err := io.Copy(w, f); err != nil {
    		w.CloseWithError(err)
    		return "", err
    	}
    	if err := w.Close(); err != nil {
    		return "", err
    	}
    
    	const publicURL = "https://storage.googleapis.com/%s/%s"
    	return fmt.Sprintf(publicURL, firebaseConfig.StorageBucket, name), nil
    }
    

  3. In your indexHandler function, after setting params.name=post.author, set the imageURL variable to be the output from the uploadFileFromForm function:

    // Get the image if there is one.
    imageURL, err := uploadFileFromForm(ctx, r)
    if err != nil {
    	w.WriteHeader(http.StatusBadRequest)
    	params.Notice = "Error saving image: " + err.Error()
    	params.Message = post.Message // Preserve their message so they can try again.
    	indexTemplate.Execute(w, params)
    	return
    }

  4. Add the imageURL information into your post data structure. Now, your index.html template will pull the image from the Cloud Storage URL into the user's post:

    post.ImageURL = imageURL

  5. If the imageURL value exists for a given post, the following code will call the labelFunc function to start a new task to label the image in the background:

    // Only look for labels if the post has an image.
    if imageURL != "" {
    	// Run labelFunc. This will start a new Task in the background.
    	if err := labelFunc.Call(ctx, key.IntID()); err != nil {
    		log.Errorf(ctx, "delay Call %v", err)
    	}
    }

Adding the labelled images to your HTML page

In your index.html file, update the form to accept image submissions and show images and their labels in the user-uploaded posts.

  1. Add the enctype attribute with the multipart/form-data value to the form tag in index.html.

    This is required when using forms with a file upload control.

  2. Create an input tag to accept an image in your HTML form:

    <form id="post-form" enctype="multipart/form-data" action="/" method="post" hidden=true>
      <div>Message: <input name="message" value="{{.Message}}"></div>
      <input name="image" id="image" type="file" accept="image/*">
      <input type="hidden" name="token" id="token">
      <input type="submit">
    </form>

  3. Under the Author and Message templating sections in your Posts template variable, include the following lines to display an image and its labels in a post:

    {{ if .ImageURL }}
    <img src="{{.ImageURL}}">
    {{ if .Labels }}
    <p>Labels:
      {{ range $i, $l := .Labels }}
      {{- if $i }}, {{end}}
      {{ printf "%s (%.3f)" $l.Description $l.Score -}}
      {{end}}
    </p>
    {{end}}
    {{end}}

Running your application locally

Run and test your application using the local development server (dev_appserver.py), which is included with Cloud SDK.

  1. From the project root directory where your application's app.yaml is located, start the local development server with the following command:

    dev_appserver.py app.yaml
    

    The local development server is now running and listening for requests on port 8080. Something go wrong?

  2. Visit http://localhost:8080/ in your web browser to view the app.

    Final

Running the local development server (dev_appserver.py)

To run the local development server, you can either run dev_appserver.py by specifying the full directory path or you can add dev_appserver.py to your PATH environment variable:

  • If you installed the original App Engine SDK, the tool is located at:

    [PATH_TO_APP_ENGINE_SDK]/dev_appserver.py
    
  • If you installed the Google Cloud SDK, the tool is located at:

    [PATH_TO_CLOUD_SDK]/google-cloud-sdk/bin/dev_appserver.py
    

    Tip: To add the Google Cloud SDK tools to your PATH environment variable and enable command-completion in your shell, you can run:

    [PATH_TO_CLOUD_SDK]/google-cloud-sdk/install.sh
    

For more information about running the local development server including how to change the port number, see the Local Development Server reference.

Making code changes

The local development server watches for changes in your project files, so it recompiles and re-launches your application after you make code changes.

  1. Try it now: Leave the local development server running and then try editing the index.html file to change "The Gopher Network" to something else.

  2. Reload http://localhost:8080/ to see the change.

Deploying your application

Deploy your application to App Engine using the following command from the project root directory where the app.yaml file is located:

gcloud app deploy

Viewing your application

To launch your browser and view your application at http://[YOUR_PROJECT_ID].appspot.com, run the following command:

gcloud app browse

Next steps

Congratulations! You built an application that can store and classify uploaded images. Learn how to add other features to your application by exploring the following pages:

Was this page helpful? Let us know how we did:

Send feedback about...

App Engine standard environment for Go