为 Cloud Storage 使用 App Engine API

本文档介绍如何使用适用于 Cloud Storage 的 App Engine 客户端库存储和检索数据。本文档假定您已阅读 Java 8 App Engine 标准环境快速入门所述内容,了解如何构建 App Engine 应用。此外,本文档还假定您了解在 App Engine 中使用 Cloud Storage 的基本概念

下载适用于 Cloud Storage 的 App Engine 客户端库

您可以使用 Apache MavenApache IvyGit 等常用工具下载客户端库,也可以从 Maven 代码库手动下载。请选择您的首选方法:

Git

如果您安装了 Git,则可以按如下方式克隆 Google Cloud Storage 客户端库的 GitHub 代码库:

git clone https://github.com/GoogleCloudPlatform/appengine-gcs-client.git

Maven

Maven 用户应用的 pom.xml 文件中应包含以下内容:

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

Ivy

Ivy 用户应用的 ivy.xml 文件中应包含以下内容:

<dependency org="com.google.appengine.tools"
            name="appengine-gcs-client"
            rev="latest.integration" />

手动下载

访问库的 Maven 代码库并下载最新的类、源和 JavaDoc JAR 文件:

此外,您还需要下载以下依赖项并将其包含在您的应用中:

ACL 和 App Engine 客户端库

使用客户端库的应用无法更改存储分区 ACL,但可以为其创建的对象指定 ACL 以控制对这些对象的访问权限。FcsFileOptions 类的文档中介绍了可用的 ACL 设置。

Cloud Storage 和子目录

适用于 Cloud Storage 的 App Engine 客户端库允许您在创建对象时提供子目录分隔符,但 Cloud Storage 中实际上不存在子目录,其中的所谓子目录实际上只是对象文件名的一部分。

例如,您可能认为创建对象 somewhere/over/the/rainbow.mp3 会将 rainbow.mp3 文件储存在子目录 somewhere/over/the/ 中。但是,对象名称设置为 somewhere/over/the/rainbow.mp3

将 App Engine 客户端库与开发应用服务器搭配使用

您可以将客户端库与开发服务器搭配使用。但由于没有 Cloud Storage 的本地模拟,因此所有读取和写入文件的请求必须通过互联网发送到实际的 Cloud Storage 存储分区。

要将该客户端库与开发应用服务器搭配使用,请使用标志 --default_gcs_bucket_name [BUCKET_NAME] 运行 dev_appserver.py,注意将 [BUCKET_NAME] 替换为您正在使用的 Cloud Storage 存储分区的名称。

此标志可控制在应用调用 file.DefaultBucketName(ctx) 时返回的存储分区。

对 Google Cloud Storage 执行读写操作

必要的导入

以下代码段显示为了通过客户端库进行 Cloud Storage 访问,您需要进行的导入:

import com.google.appengine.tools.cloudstorage.GcsFileOptions;
import com.google.appengine.tools.cloudstorage.GcsFilename;
import com.google.appengine.tools.cloudstorage.GcsInputChannel;
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;

指定 Cloud Storage 存储分区

以下代码段显示了一种方法,通过这种方法,用户可以在将文件写入存储分区时指定存储分区的名称:

function uploadFile() {
  var bucket = document.forms["putFile"]["bucket"].value;
  var filename = document.forms["putFile"]["fileName"].value;
  if (bucket == null || bucket == "" || filename == null || filename == "") {
    alert("Both Bucket and FileName are required");
    return false;
  } else {
    var postData = document.forms["putFile"]["content"].value;
    document.getElementById("content").value = null;

    var request = new XMLHttpRequest();
    request.open("POST", "/gcs/" + bucket + "/" + filename, false);
    request.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
    request.send(postData);
  }
}

该代码段将必需的 /gcs/ 前置于用户提供的存储分区和文件名。

写入 Cloud Storage

如需将文件写入 Cloud Storage,请使用以下代码:

@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
  GcsFileOptions instance = GcsFileOptions.getDefaultInstance();
  GcsFilename fileName = getFileName(req);
  GcsOutputChannel outputChannel;
  outputChannel = gcsService.createOrReplace(fileName, instance);
  copy(req.getInputStream(), Channels.newOutputStream(outputChannel));
}

此示例显示如何将新建文件写入 Cloud Storage,如果已存在名称相同的文件,则会覆盖现有文件。由于文件一旦写入 Cloud Storage,就无法进行修改,因此这种创建同名文件的方法非常有用。要更改文件,您必须对文件的副本进行修改,然后覆盖旧文件。

从 Cloud Storage 读取数据

如需从 Cloud Storage 读取文件,请使用以下代码:

@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
  GcsFilename fileName = getFileName(req);
  if (SERVE_USING_BLOBSTORE_API) {
    BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService();
    BlobKey blobKey = blobstoreService.createGsBlobKey(
        "/gs/" + fileName.getBucketName() + "/" + fileName.getObjectName());
    blobstoreService.serve(blobKey, resp);
  } else {
    GcsInputChannel readChannel = gcsService.openPrefetchingReadChannel(fileName, 0, BUFFER_SIZE);
    copy(Channels.newInputStream(readChannel), resp.getOutputStream());
  }
}

在包含 gcsService.openPrefetchingReadChannel 的行中,请注意预提取的使用。这种操作会缓冲内存数据,并在需要之前预提取数据,从而避免读取调用发生堵塞。

修改 Cloud Storage 中的对象

要修改 Cloud Storage 中的现有对象,请使用与原始对象相同的文件名写入要修改的对象。

从 Cloud Storage 中删除对象

对于将完整的 Cloud Storage 对象名称(文件名)存储在 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 preparation 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);
}

上述代码会解码 Base64 编码的图片 ID,并从 images 表中检索由 image_id 标识的图片的文件名。该文件名通过 GcsFilename 转换为有效的 Cloud Storage 文件名。

系统会使用 gcsService.delete 从存储分区中删除该文件。 最后,系统还将删除 blogpostImage 表中的图片使用记录。

后续步骤