入門: Cloud Storage

App Engine で動作する Java アプリから Google Cloud Storage にデータを保存する方法を学習します。

Cloud Storage は App Engine で利用できるストレージ オプションの 1 つで、画像、PDF、動画ファイルなどのバイナリラージ オブジェクト(BLOB)を保存するために使用されます。Cloud Storage、Cloud Datastore、Cloud SQL を比較し、アプリの要件に適したものを選択してください。

このガイドでは App Engine のブログアプリを拡張し、Cloud SQL 内のテキストデータと Cloud Storage 内の画像を保存します。

始める前に

開発環境を構成し、App Engine プロジェクトを作成します

Cloud Storage バケットの作成

お使いのアプリは Cloud Storage バケットにデータを保存します。バケットは次の手順で作成されます。

  1. GCP Console で、Cloud Storage ブラウザページに移動します。

    Cloud Storage ブラウザページに移動

  2. [バケットを作成] をクリックします。
  3. [バケットを作成] ダイアログ内で、以下の属性を指定します。
  4. [作成] をクリックします。

App Engine の Cloud Storage ライブラリのダウンロード

Cloud Storage を利用するには、アプリケーションで App Engine ツール ライブラリを使用する必要があります。

Maven

アプリケーションの pom.xml ファイルに次の記述を含めます。

<project>

 [...]

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

 [...]

</project>

Gradle

アプリケーションの build.gradle ファイルに次の記述を含めます。

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

ライブラリのインポート

このガイドで提供されるサンプルコードは、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;

Cloud Storage へのファイルのアップロード

以下のスニペットは、フォームデータの処理のために示したコードを拡張したものです。これでファイル選択画面が組み込まれ、ユーザーがアップロードする画像を選択できるようになります。

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

POST メソッドにより、データが /create に送られます。ここでサーブレットがテキスト入力とファイル名を Cloud SQL テーブルに、画像を Cloud Storage に保存します。詳しくはこのガイドの後半で説明します。

Cloud Storage へのファイルの保存

次の手順で Cloud Storage にファイルを保存します。

  1. @MultipartConfig アノテーションを使用します。これはファイルなどの複数のパートからなる HTML 形式のデータ処理に必要です。

    @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. GcsServiceFactory を使用して GcsService オブジェクトを作成します。

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

    GcsService オブジェクトを作成するときは、バックオフ パラメータを指定する必要があります。この例ではサービスが、15 秒以内に 10 回の再試行を行います。

  3. gcsService オブジェクトのバッファサイズと Cloud Storage バケットの変数を作成します。

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

    CLOUD-STORAGE-BUCKET-NAME を Cloud Storage バケットの名前に置き換えます。

  4. アップロードするファイルからファイル名を抽出し、一意の Cloud Storage ファイル名を作成して 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
    }
    

    storeImage() メソッドにより、ファイル権限が public-read に設定され、一般に公開されます。画像は App Engine ツール ライブラリの copy() 関数を使用して Cloud Storage に書き込まれます。

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

    上記のスニペットでは、uploadedFilename() で HTTP ヘッダーからファイル名が抽出され、これにタイムスタンプを付加して一意のファイル名が作成されます。

    storeImage() メソッドでは、Cloud Storage への書き込みに次のように 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();
      }
    
    }
    

    copy() メソッドは画像である InputStream オブジェクトと、OutputStream オブジェクト(この例では、ファイルを Cloud Storage に書き込むメソッドである createOrReplace())を指定しています。

  5. 後で参照できるように、Cloud Storage のファイル名をデータベースに保存します。次のコードで、Cloud SQL を使用して画像ファイル名を保存するテーブルを作成します。

    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. 画像を使用するブログ投稿に各画像を関連付けます。以下のサンプルは、blogpostImages テーブルを使用しています。

    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);
    

    このテーブルには post_idimage_id の 2 つの外部キーがあり、親のブログ投稿または画像が削除されると、こちらも削除されるように設定されています。

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

Cloud Storage からのオブジェクトの取得

このガイドの前のセクションでは、一般公開されている画像を Cloud Storage に保存する方法を説明しました。Cloud Storage は HTML の <img> タグで使用できるリンクを生成します。こうした画像のリンクには URI https://storage.googleapis.com/[BUCKET-NAME]/[OBJECT-NAME] が含まれます。

特定のブログ投稿に関連付けられている画像を取得する場合、次の Cloud SQL の SQL クエリに示すように、データベースから関連する URI を取得する必要があります。

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

selectSql クエリから、特定のブログ投稿 ID に関連付けられている画像のファイル名が戻ります。URI をファイル名に付加すると、HTML の <img> タグの src 属性で使用できる一般公開画像への有効なパスが作成されます。

Cloud Storage でのオブジェクトの変更

Cloud Storage 内の既存のオブジェクトを変更するには、オブジェクトを変更する際に元のファイル名と同じ名前で書き込みます。

Cloud Storage からのオブジェクトの削除

次の手順は、Cloud SQL データベースに Cloud Storage オブジェクトの完全名(ファイル名)を保存するアプリでオブジェクトを削除する方法を示しています。

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

上記のコードで Base64 エンコード済みの画像 ID をデコードし、images テーブルの image_id で識別される画像のファイル名を取得します。ファイル名は GcsFilename によって有効な Cloud Storage ファイル名に変換されます。

ファイルは gcsService.delete によってバケットから削除されます。最後に blogpostImage テーブルからそのファイルの利用状況のレコードが削除されます。

App Engine へのデプロイ

Maven を使用して App Engine にアプリをデプロイします。

プロジェクトのルート ディレクトリに移動し、次のように入力します。

mvn appengine:deploy

Maven によってアプリがデプロイされた後、次のように入力すると、新しいアプリでウェブブラウザのタブが自動的に開きます。

gcloud app browse

次のステップ

App Engine の Cloud Storage にファイルを保存すると、アプリケーションで必要となる大容量ファイルやユーザー アセットを処理できるようになります。場合によっては、既定のタイムアウトよりも時間がかかるタスクや、非同期での実行が必要なタスクを実行する必要が生じることがあります。

次に、タスクキューの使い方について学びます。ここでは、このガイドでアップロードした画像のサイズを変更するため、Images API を使用するサンプルを用いて、非同期タスクを実行します。

このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...

Java 8 の App Engine スタンダード環境