Introducción: Cloud Storage

Aprende cómo almacenar datos en Google Cloud Storage desde una aplicación de Java en App Engine.

Cloud Storage se utiliza para almacenar objetos binarios grandes (BLOB), como imágenes, archivos PDF o archivos de video, y es una opción de almacenamiento disponible en App Engine. Compara Cloud Storage, Cloud Datastore y Cloud SQL y elige el que mejor se adapte a los requisitos de tu aplicación.

Esta guía amplía la aplicación de blog de App Engine para almacenar los datos de texto en Cloud SQL y las imágenes en Cloud Storage.

Antes de comenzar

Configura tu entorno de programación y crea tu proyecto de App Engine.

Cómo crear un depósito de Cloud Storage

Tu aplicación almacenará los datos en un depósito de Cloud Storage para crear el depósito:

  1. En GCP Console, dirígete al navegador de Cloud Storage.

    Ir al navegador de Cloud Storage

  2. Haz clic en Crear depósito.
  3. En el diálogo Crear depósito especifica los siguientes atributos:
  4. Haz clic en Crear.

Cómo descargar la biblioteca de Cloud Storage de App Engine

Tu aplicación debe utilizar la biblioteca de herramientas de App Engine para usar Cloud Storage.

Maven

Incluye lo siguiente en el archivo pom.xml de tu aplicación:

<project>

 [...]

 <dependency>
   <groupId>com.google.appengine.tools</groupId>
   <artifactId>appengine-gcs-client</artifactId>
   <version>0.8</version>
 </dependency>

 [...]

</project>

Gradle

Incluye lo siguiente en el archivo build.gradle de tu aplicación:

compile 'com.google.appengine.tools:appengine-gcs-client:0.8'

Cómo importar bibliotecas

En el código de muestra proporcionado con esta guía, se utilizan las siguientes importaciones de la biblioteca de herramientas de App Engine:

import com.google.appengine.tools.cloudstorage.GcsFileOptions;
import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.appengine.tools.cloudstorage.GcsOutputChannel;
import com.google.appengine.tools.cloudstorage.GcsService;
import com.google.appengine.tools.cloudstorage.GcsServiceFactory;
import com.google.appengine.tools.cloudstorage.RetryParams;

Cómo subir un archivo a Cloud Storage

El siguiente fragmento amplía el código que se muestra en el Manejo de los datos del formulario para incluir un selector de archivos a fin de que el usuario pueda elegir las imágenes que quiera subir.

<form method="POST" action="/create" enctype="multipart/form-data">

  <div>
    <label for="title">Title</label>
    <input type="text" name="title" id="title" size="40" value="" />
  </div>

  <div>
    <label for="author">Author</label>
    <input type="text" name="author" id="author" size="40" value="" />
  </div>

  <div class="form-group">
    <label for="description">Post content</label>
    <textarea name="description" id="description" rows="10" cols="50"></textarea>
  </div>

  <div class="form-group">
    <label for="filename">Upload image</label>
    <!-- Allow only image file types to be selected -->
    <input type="file" name="image" accept="image/*">
  </div>

  <button type="submit">Save</button>

</form>

El método POST envía los datos a /create, en el que un servlet almacena la entrada de texto y el nombre de archivo en una tabla de Cloud SQL y la imagen en Cloud Storage, como se muestra más adelante en esta guía.

Cómo almacenar archivos en Cloud Storage

Para almacenar archivos en Cloud Storage, realiza lo siguiente:

  1. Usa la anotación @MultipartConfig, que se requiere para manejar datos de formularios HTML de varias partes, como los archivos:

    @MultipartConfig(
      maxFileSize = 10 * 1024 * 1024, // max size for uploaded files
      maxRequestSize = 20 * 1024 * 1024, // max size for multipart/form-data
      fileSizeThreshold = 5 * 1024 * 1024 // start writing to Cloud Storage after 5MB
    )
    
  2. Crea un objeto GcsService utilizando GcsServiceFactory.

    private final GcsService gcsService =
        GcsServiceFactory.createGcsService(
            new RetryParams.Builder()
                .initialRetryDelayMillis(10)
                .retryMaxAttempts(10)
                .totalRetryPeriodMillis(15000)
                .build());
    

    Cuando creas un objeto GcsService, debes especificar los parámetros de retirada. En este ejemplo, el servicio realizará 10 reintentos en 15 segundos.

  3. Crea variables para el tamaño del búfer y el depósito de Cloud Storage del objeto gcsService:

    private static final int BUFFER_SIZE = 2 * 1024 * 1024;
    private final String bucket = "[CLOUD-STORAGE-BUCKET-NAME]";
    

    Reemplaza CLOUD-STORAGE-BUCKET-NAME por el nombre del depósito de Cloud Storage.

  4. Extrae el nombre de archivo del archivo que deseas subir, crea un nombre de archivo de Cloud Storage único y almacena el archivo en Cloud Storage:

    private String storeImage(Part image) throws IOException {
    
      String filename = uploadedFilename(image); // Extract filename
      GcsFileOptions.Builder builder = new GcsFileOptions.Builder();
    
      builder.acl("public-read"); // Set the file to be publicly viewable
      GcsFileOptions instance = GcsFileOptions.getDefaultInstance();
      GcsOutputChannel outputChannel;
      GcsFilename gcsFile = new GcsFilename(bucket, filename);
      outputChannel = gcsService.createOrReplace(gcsFile, instance);
      copy(filePart.getInputStream(), Channels.newOutputStream(outputChannel));
    
      return filename; // Return the filename without GCS/bucket appendage
    }
    

    El método storeImage() establece los permisos de archivo en public-read para que sean visibles públicamente. La imagen se escribe en Cloud Storage con la función copy() de la biblioteca de herramientas de App Engine.

    private String uploadedFilename(final Part part) {
    
      final String partHeader = part.getHeader("content-disposition");
    
      for (String content : part.getHeader("content-disposition").split(";")) {
        if (content.trim().startsWith("filename")) {
          // Append a date and time to the filename
          DateTimeFormatter dtf = DateTimeFormat.forPattern("-YYYY-MM-dd-HHmmssSSS");
          DateTime dt = DateTime.now(DateTimeZone.UTC);
          String dtString = dt.toString(dtf);
          final String fileName =
              dtString + content.substring(content.indexOf('=') + 1).trim().replace("\"", "");
    
          return fileName;
        }
      }
      return null;
    }
    

    En el fragmento anterior, uploadedFilename() extrae el nombre de archivo de los encabezados HTTP y agrega una marca de tiempo para crear un nombre de archivo único.

    El método storeImage() escribe en Cloud Storage usando copy:

    private void copy(InputStream input, OutputStream output) throws IOException {
    
      try {
        byte[] buffer = new byte[BUFFER_SIZE];
        int bytesRead = input.read(buffer);
        while (bytesRead != -1) {
          output.write(buffer, 0, bytesRead);
          bytesRead = input.read(buffer);
        }
      } finally {
        input.close();
        output.close();
      }
    
    }
    

    El método copy() toma un objeto InputStream, que es la imagen, y un objeto OutputStream, que en este ejemplo es createOrReplace(), un método que escribe el archivo en Cloud Storage.

  5. Guarda el nombre de archivo de Cloud Storage en una base de datos para búsquedas posteriores. El siguiente código utiliza Cloud SQL para crear una tabla que almacena los nombres de archivos de imágenes.

    final String createImageTableSql =
        "CREATE TABLE IF NOT EXISTS images ( image_id INT NOT NULL "
            + "AUTO_INCREMENT, filename VARCHAR(256) NOT NULL, "
            + "PRIMARY KEY (image_id) )";
    
    conn.createStatement().executeUpdate(createImageTableSql);
    
  6. Asocia cada imagen con la entrada de blog que utiliza la imagen. La muestra usa la tabla blogpostImages para esto:

    final String createBlogPostImageTableSql =
          "CREATE TABLE IF NOT EXISTS blogpostImages ( post_id INT NOT NULL, "
              + "image_id INT NOT NULL, "
              + "INDEX postImages (post_id, image_id), "
              + "FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE, "
              + "FOREIGN KEY (image_id) REFERENCES images(image_id) ON DELETE CASCADE )";
    
    conn.createStatement().executeUpdate(createBlogPostImageTableSql);
    

    Ten en cuenta que la tabla tiene dos claves externas, post_id y también image_id, que se configuran para borrarse si se ha borrado la entrada de blog principal o la imagen.

    if (imageFile.getSize() != 0) { // check if an image has been uploaded
    
      // Grab the last autogenerated post ID to associate with image
      ResultSet lastId = conn.prepareStatement(getLastIdSql).executeQuery();
      lastId.next(); // move the cursor
      int postId = lastId.getInt(1); // store the post's ID to associate with image
    
      // Store the image file
      try {
        String filename = storeImage(imageFile); // Store the image and get the filename
        // Store a record of the filename in the database
        try (PreparedStatement statementCreateImage = conn.prepareStatement(createImageSql)) {
          statementCreateImage.setString(1, filename);
          statementCreateImage.executeUpdate();
    
          lastId = conn.prepareStatement(getLastIdSql).executeQuery();
          lastId.next(); // Move the cursor
          int imageId = lastId.getInt(1); // Store the post's ID for insertion later
    
          // Associate image with blog post
          PreparedStatement statementBlogImage = conn.prepareStatement(imageBlogPostSql);
          statementBlogImage.setInt(1, postId);
          statementBlogImage.setInt(2, imageId);
          statementBlogImage.executeUpdate();
    
        } catch (SQLException e) {
          throw new ServletException("SQL error when storing image details", e);
        }
      } catch (IOException e) {
        throw new IOException("Cloud Storage error when storing file", e);
      }
    }
    

Cómo recuperar objetos desde Cloud Storage

Los artículos anteriores de esta guía muestran cómo almacenar imágenes visibles públicamente en Cloud Storage. Cloud Storage genera vínculos que se pueden usar en las etiquetas HTML <img>. Estos vínculos de imágenes tienen la URI https://storage.googleapis.com/[BUCKET-NAME]/[OBJECT-NAME].

Para recuperar imágenes asociadas con una entrada de blog en particular, debes recuperar los URI relacionados de una base de datos, como se muestra en la siguiente consulta de SQL para Cloud SQL:

final String selectSql =
      "SELECT images.filename "
          + "FROM images, blogpostImages "
          + "WHERE (images.image_id = blogpostImages.image_id) AND (blogpostImages.post_id = ?)";

La consulta selectSql muestra el nombre de archivo de una imagen asociada con un ID de entrada de blog en particular. Anexar el URI al nombre de archivo creará una ruta de acceso válida a la imagen visible públicamente que se puede usar en el atributo src en una etiqueta HTML <img>.

Cómo modificar objetos en Cloud Storage

Para modificar un objeto existente en Cloud Storage, escribe el objeto modificado con el mismo nombre de archivo que el original.

Cómo borrar objetos de Cloud Storage

El siguiente procedimiento muestra cómo borrar objetos en una aplicación que almacena el nombre completo del objeto de Cloud Storage (nombre de archivo) en una base de datos de Cloud SQL:

final String bucket = "CLOUD-STORAGE-BUCKET"; // Cloud Storage bucket name
Map<String, String[]> userData = req.getParameterMap();

String[] image = userData.get("id"); // Grab the encoded image ID
String decodedId = new String(Base64.getUrlDecoder().decode(image[0])); // decode the image ID
int imageId = Integer.parseInt(decodedId);

// Grab the filename and build out a Cloud Storage filepath in preperation for deletion
try (PreparedStatement statementDeletePost = conn.prepareStatement(imageFilenameSql)) {
  statementDeletePost.setInt(1, imageId); // cast String to Int
  ResultSet rs = statementDeletePost.executeQuery(); // remove image record
  rs.next(); // move the cursor

  GcsFilename filename = new GcsFilename(bucket, rs.getString("filename"));
  if (gcsService.delete(filename)) {

    // Remove all records of image use in the blog
    // Use of foreign keys with cascading deletes will cause removal from blogpostImages table
    PreparedStatement statementDeleteImageRecord = conn.prepareStatement(deleteSql);
    statementDeleteImageRecord.setInt(1, imageId);
    statementDeleteImageRecord.executeUpdate();

    final String confirmation =
        "Image ID "
            + imageId
            + " has been deleted and record of its use in blog posts have been removed.";

    req.setAttribute("confirmation", confirmation);
    req.getRequestDispatcher("/confirm.jsp").forward(req, resp);
  } else {
    final String confirmation = "File marked for deletion does not exist.";

    req.setAttribute("confirmation", confirmation);
    req.getRequestDispatcher("/confirm.jsp").forward(req, resp);
  }
} catch (SQLException e) {
  throw new ServletException("SQL error", e);
}

El código anterior decodifica el ID de imagen codificada de Base64 y recupera el nombre de archivo de una imagen identificada por su image_id de la tabla images. El nombre de archivo se convierte en un nombre de archivo válido para Cloud Storage con GcsFilename.

El archivo se borra del depósito utilizando gcsService.delete. Finalmente, borra el registro de su uso en la tabla blogpostImage.

Cómo implementar en App Engine

Puedes implementar tu aplicación en App Engine con Maven.

Ve al directorio raíz de tu proyecto y escribe lo siguiente:

mvn appengine:deploy

Luego de que Maven implemente tu aplicación, escribe lo siguiente para abrir una pestaña del navegador web de forma automática en tu nueva aplicación:

gcloud app browse

Pasos siguientes

El almacenamiento de archivos en Cloud Storage en App Engine te permite trabajar con archivos grandes y elementos de usuarios que tu aplicación puede requerir. En algunas situaciones, es posible que debas realizar tareas que demoran más que el tiempo de espera predeterminado o que quieras ejecutar de forma asíncrona.

A continuación, aprende sobre el uso de listas de tareas en cola a fin de realizar tareas asíncronas siguiendo un ejemplo del uso de la API de Images para cambiar el tamaño de las imágenes subidas en esta guía.

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

Enviar comentarios sobre...

Entorno estándar de App Engine para Java 8