Usar Cloud Storage con Node.js

Esta parte del instructivo de Bookshelf en Node.js muestra cómo la app de ejemplo almacena imágenes en Google Cloud Storage.

Esta página forma parte de un instructivo de varias páginas. Ve a la aplicación Bookshelf en Node.js para comenzar desde el principio y leer las instrucciones de configuración.

Crear un depósito de Cloud Storage

Las instrucciones siguientes muestran cómo crear un depósito de Cloud Storage. Los depósitos son los contenedores básicos que conservan tus datos en Cloud Storage.

  1. Ingresa el siguiente comando en una ventana de terminal:

    gsutil mb gs://[YOUR-BUCKET-NAME]

    Donde:

    • [YOUR-BUCKET-NAME] representa el nombre de tu depósito de Cloud Storage.
  2. Para ver las imágenes que se subieron en la aplicación Bookshelf, configura la Lista de control de acceso (LCA) predeterminada del depósito como public-read.

    gsutil defacl set public-read gs://[YOUR-BUCKET-NAME]

    Configuraciones

    Copia el archivo config.json de la sección Datos estructurados de este instructivo al directorio nodejs-getting-started/3-binary-data. Agrega la línea siguiente al archivo copiado:

    "CLOUD_BUCKET": "[YOUR_BUCKET_NAME]"
    

    Reemplaza [YOUR_BUCKET_NAME] por el nombre del depósito de Cloud Storage.

    Instalar dependencias

    Usa npm para instalar las dependencias en el directorio nodejs-getting-started/3-binary-data:

        npm install
    

    Ejecutar la aplicación en la máquina local

    1. Usa npm para iniciar un servidor web local:

          npm start
      
    1. En el navegador web, ingresa la siguiente dirección:

      http://localhost:8080

    Ahora puedes navegar por las páginas web de la aplicación y agregar libros con imágenes de portada, así como editar y borrar libros.

    Implementar la aplicación en el entorno estándar de App Engine

    1. Implementa la app de muestra desde el directorio nodejs-getting-started/3-binary-data:

      gcloud app deploy
      
    2. En el navegador web, ingresa la siguiente dirección. Reemplaza [YOUR_PROJECT_ID] por el ID del proyecto:

      https://[YOUR_PROJECT_ID].appspot.com
      

    Si actualizas tu app, podrás implementar la versión actualizada mediante el mismo comando que usaste para implementar la app por primera vez. La implementación nueva crea una versión nueva de la app y la convierte a la versión predeterminada. Se conservan las versiones anteriores de la aplicación. De forma predeterminada, el entorno estándar de App Engine escala a 0 instancias cuando no hay tráfico entrante a una versión, por lo que las versiones sin usar no deben tener costo. Sin embargo, todas estas versiones de la aplicación son recursos facturables.

    Consulta la sección Limpiar en el paso final de este instructivo para obtener más información sobre la limpieza de recursos facturables, incluidas las versiones de la aplicación no predeterminadas.

    Estructura de la aplicación

    Estructura de muestra de datos binarios

    La aplicación usa Cloud Storage para almacenar datos binarios (en este caso, imágenes), mientras continúa usando una base de datos estructurada para la información de los libros, ya sea Cloud Datastore o Cloud SQL.

    Comprender el código

    En esta sección se explica el código de la aplicación y su funcionamiento.

    Cómo controlar las cargas de los usuarios

    A fin de permitir que los usuarios suban imágenes, el formulario para agregar y editar se modificó y ahora permite subir archivos mediante la configuración de enctype como multipart/form-data, y se agregó un campo nuevo para la imagen:

    block content
      h3 #{action} book
      form(method="POST", enctype="multipart/form-data")
        .form-group
          label(for="title") Title
          input.form-control(type="text", name="title", id="title", value=book.title)
        .form-group
          label(for="author") Author
          input.form-control(type="text", name="author", id="author", value=book.author)
        .form-group
          label(for="publishedDate") Date Published
          input.form-control(type="text", name="publishedDate", id="publishedDate", value=book.publishedDate)
        .form-group
          label(for="description") Description
          input.form-control(type="text", name="description", id="description", value=book.description)
        .form-group
          label(for="image") Cover Image
          input.form-control(type="file", name="image", id="image")
        .form-group.hidden
          label(for="imageUrl") Cover Image URL
          input.form-control(type="text", name="imageUrl", id="imageUrl", value=book.imageUrl)
        button.btn.btn-success(type="submit") Save

    La aplicación usa el middleware Express multer para administrar el análisis de solicitudes de carga de archivos. La aplicación almacena los archivos subidos en la memoria de manera temporal, ya que los subirá a Cloud Storage directamente:

    const Multer = require('multer');
    const multer = Multer({
      storage: Multer.MemoryStorage,
      limits: {
        fileSize: 5 * 1024 * 1024 // no larger than 5mb
      }
    });

    Cómo subir archivos a Cloud Storage

    A continuación, la aplicación usa el middleware sendUploadToGCS para administrar la carga de los archivos que están en la memoria a Cloud Storage:

    function sendUploadToGCS (req, res, next) {
      if (!req.file) {
        return next();
      }
    
      const gcsname = Date.now() + req.file.originalname;
      const file = bucket.file(gcsname);
    
      const stream = file.createWriteStream({
        metadata: {
          contentType: req.file.mimetype
        },
        resumable: false
      });
    
      stream.on('error', (err) => {
        req.file.cloudStorageError = err;
        next(err);
      });
    
      stream.on('finish', () => {
        req.file.cloudStorageObject = gcsname;
        file.makePublic().then(() => {
          req.file.cloudStoragePublicUrl = getPublicUrl(gcsname);
          next();
        });
      });
    
      stream.end(req.file.buffer);
    }

    El middleware verifica cada archivo de la solicitud y los sube a Cloud Storage a través de una transmisión con escritura estándar. Una vez subidos, los archivos tienen visibilidad pública y el middleware configura la propiedad cloudStoragePublicUrl en el archivo. La URL pública se puede usar para publicar la imagen directamente desde Cloud Storage:

    function getPublicUrl (filename) {
      return `https://storage.googleapis.com/${CLOUD_BUCKET}/${filename}`;
    }

    Luego, la aplicación usa esta propiedad para guardar la URL pública de la imagen en la base de datos:

    router.post(
      '/add',
      images.multer.single('image'),
      images.sendUploadToGCS,
      (req, res, next) => {
        let data = req.body;
    
        // Was an image uploaded? If so, we'll use its public URL
        // in cloud storage.
        if (req.file && req.file.cloudStoragePublicUrl) {
          data.imageUrl = req.file.cloudStoragePublicUrl;
        }
    
        // Save the data to the database.
        getModel().create(data, (err, savedData) => {
          if (err) {
            next(err);
            return;
          }
          res.redirect(`${req.baseUrl}/${savedData.id}`);
        });
      }
    );

    Cómo publicar imágenes desde Cloud Storage

    Dado que la app tiene la URL pública de la imagen, publicarla es simple. Publicarla directamente desde Cloud Storage es útil, ya que las solicitudes aprovechan la infraestructura de publicación global de Google y no es necesario que la aplicación responda a las solicitudes de imágenes, lo que libera ciclos de CPU para otras solicitudes.

    block content
      h3 Book
        small
    
      .btn-group
        a(href=`/books/${book.id}/edit`, class='btn btn-primary btn-sm')
          i.glyphicon.glyphicon-edit
          span  Edit book
        a(href=`/books/${book.id}/delete`, class='btn btn-danger btn-sm')
          i.glyphicon.glyphicon-trash
          span  Delete book
    
      .media
        .media-left
          img(src=book.imageUrl || "http://placekitten.com/g/128/192")
        .media-body
          h4= book.title
            |  
            small= book.publishedDate
          h5 By #{book.author||'unknown'}
          p= book.description