Ejecutar código en forma asíncrona

Aprende cómo usar las listas de tareas en cola y Cloud Vision para etiquetar las imágenes subidas por el usuario como una tarea que se ejecuta en segundo plano.

Cloud Vision puede detectar y extraer información acerca de entidades dentro de una imagen y te permite asignar niveles de confianza sobre determinados atributos clave, como ubicaciones, productos o actividades que aparecen en la imagen. Por ejemplo, para la fotografía de una persona, Cloud Vision puede darte niveles de confianza acerca de si la persona está triste, contenta, enojada, etc., de modo que puedas etiquetarla de forma adecuada.

Esta página y esta muestra son parte de un ejemplo de aprendizaje extendido de una simple aplicación de blog donde los usuarios pueden subir publicaciones. Ya deberías estar familiarizado con el lenguaje de programación Go y el desarrollo web básico. Para comenzar desde el principio, dirígete a Compilar una aplicación con Go.

Costos

Ejecutar este instructivo no tiene costo. La ejecución de esta app de muestra por sí sola no supera tu cuota gratuita.

Antes de comenzar

Si completaste la guía Cómo compilar una aplicación con Go, omite esta sección. De lo contrario, completa los siguientes pasos:

  1. Completa las tareas en la sección Antes de comenzar en Configuración de tu proyecto y aplicación. Luego, regresa a esta página.

  2. En este ejemplo, agregarás código a la aplicación de muestra gophers-5.

    Descarga la muestra gophers-5 y sus dependencias en tu máquina local:

    go get -u -d -v github.com/GoogleCloudPlatform/golang-samples/appengine/gophers/gophers-5/...
    
  3. Navega al directorio gophers-5:

    cd go/src/github.com/GoogleCloudPlatform/golang-samples/appengine/gophers/gophers-5
    
  4. Completa los pasos 2 a 6 de Cómo agregar Firebase a tu proyecto desde la página Cómo autenticar usuarios.

  5. Habilita Cloud Vision:

    Habilitar Cloud Vision

Estructura tu aplicación

Este proyecto de ejemplo tiene la siguiente estructura:

  • go-app/: directorio raíz del proyecto.
    • app.yaml: ajustes de configuración para tu aplicación de App Engine.
    • main.go: el código de tu aplicación.
    • index.html: plantilla HTML para mostrar tu página de inicio.
    • static/: directorio para almacenar tus archivos estáticos.
      • style.css: hoja de estilo que formatea el aspecto de tus archivos HTML.
      • gcp-gopher.svg: imagen de Gopher.
      • index.js: configura la interfaz de usuario de Firebase Authentication y maneja las solicitudes de autenticación.

Configura etiquetas de imagen

Configura el backend que agrega imágenes y etiquetas a las publicaciones subidas por los usuarios. Ten en cuenta que esta muestra usa Cloud Vision.

  1. Descarga los nuevos paquetes para tu entorno de desarrollo con el siguiente comando:

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

    La muestra usa estos paquetes:

  2. Agrega los siguientes paquetes a tu lista de importaciones en tu archivo main.go:

    "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 la descripción de la imagen de una publicación como una struct con dos campos: un campo Description para la etiqueta de la imagen y un campo Score con un valor entre 0 (sin confianza) y 1 (confianza muy alta) que indica con cuánta precisión se aplica la etiqueta a esa imagen:

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

    Las etiquetas con las cinco puntuaciones más altas se mostrarán debajo de la imagen. Accederás a estos campos más adelante con la función GetDescription() y la función GetScore().

  4. Agrega los campos ImageURL y Labels a tu estructura de datos Post:

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

    El campo ImageURL es una URL pública de Cloud Storage que lleva a la imagen subida.

Agrega etiquetas a una imagen

Cuando un usuario sube una imagen a tu página, tu función indexHandler agregará la imagen a tu depósito de Cloud Storage mientras le agrega etiquetas a la imagen dada de forma asíncrona.

  1. Crea la variable labelFunc, que crea una tarea para etiquetar la imagen subida con Cloud Vision. Esta tarea se agrega a la lista de tareas en cola, de modo que el trabajo se pueda ejecutar en segundo plano. Si un usuario sube una imagen en una publicación, tu función indexHandler llama a la función labelFunc.

    // 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. Crea la función uploadFileFromForm, la cual confirma que el archivo subido por el usuario es una imagen y después crea y muestra la URL pública de la imagen en Cloud Storage.

    // 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. En tu función indexHandler, luego de configurar params.name=post.author, configura la variable imageURL para que sea el resultado de la función uploadFileFromForm:

    // 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. Agrega la información de imageURL en tu estructura de datos post. Ahora, tu plantilla index.html extraerá la imagen de la URL de Cloud Storage a la publicación del usuario:

    post.ImageURL = imageURL
  5. Si existe el valor imageURL para una publicación dada, el siguiente código llamará a la función labelFunc a fin de comenzar una nueva tarea para etiquetar la imagen en segundo plano:

    // 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)
    	}
    }

Agrega las imágenes etiquetadas a tu página HTML

En tu archivo index.html, actualiza el formulario para aceptar los envíos de imágenes y mostrar las imágenes y sus etiquetas en las publicaciones subidas por usuarios.

  1. Agrega el atributo enctype con el valor multipart/form-data a la etiqueta del formulario en index.html.

    Esto se requiere cuando se utilizan formularios con un control de carga de archivos.

  2. Crea una etiqueta input para aceptar una imagen en tu formulario HTML:

    <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. Bajo las secciones de plantillas Author y Message en tu variable de plantilla Posts, incluye las siguientes líneas para visualizar una imagen y sus etiquetas en una publicación:

    {{ 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}}

Ejecuta tu aplicación de manera local

Ejecuta y prueba tu aplicación con el servidor de desarrollo local (dev_appserver.py), que se incluye con el SDK de Cloud.

  1. Desde el directorio raíz del proyecto, donde se encuentra el archivo app.yaml de tu aplicación, inicia el servidor de desarrollo local con el siguiente comando:

    dev_appserver.py app.yaml
    

    El servidor de desarrollo local está en ejecución y detecta las solicitudes en el puerto 8080. ¿Algo va mal?

  2. Visita http://localhost:8080/ en tu navegador web para ver la aplicación.

    Final

Ejecuta el servidor de desarrollo local (dev_appserver.py)

Para ejecutar el servidor de desarrollo local, puedes ejecutar dev_appserver.py mediante la especificación de la ruta completa del directorio o puedes agregar dev_appserver.py a tu variable de entorno PATH:

  • Si instalaste el SDK original de App Engine, la herramienta estará en la siguiente ubicación:

    [PATH_TO_APP_ENGINE_SDK]/dev_appserver.py
    
  • Si instalaste el SDK de Google Cloud, la herramienta estará en la siguiente ubicación:

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

    Sugerencia: Para agregar las herramientas del SDK de Google Cloud a tu variable de entorno PATH y habilitar la finalización de comandos en tu shell, puedes ejecutar lo siguiente:

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

Para obtener más información sobre la ejecución del servidor de desarrollo local, que incluye cómo cambiar el número de puerto, consulta la referencia Servidor de desarrollo local.

Realiza cambios en el código

El servidor de desarrollo local detecta cambios en los archivos de tu proyecto, por lo que vuelve a compilar y a reiniciar tu aplicación después de realizar cambios en el código.

  1. Pruébalo ahora: Deja el servidor de desarrollo local en ejecución y, luego, intenta editar index.html para cambiar "The Gopher Network" a otra cosa.

  2. Vuelve a cargar http://localhost:8080/ para ver el cambio.

Implementa tu aplicación

Implementa tu aplicación en App Engine con el siguiente comando desde el directorio raíz del proyecto, donde se encuentra el archivo app.yaml:

gcloud app deploy

Ve tu aplicación

Para iniciar tu navegador y ver tu aplicación en http://[YOUR_PROJECT_ID].appspot.com, ejecuta el siguiente comando:

gcloud app browse

Próximos pasos

Felicitaciones. Creaste una aplicación que puede almacenar y clasificar imágenes subidas. Para aprender cómo agregar otras funciones a tu aplicación, explora las siguientes páginas:

¿Te ha resultado útil esta página? Enviar comentarios:

Enviar comentarios sobre...

Entorno estándar de App Engine para Go