開始使用:Cloud Storage

瞭解如何在 App Engine 上,將 Java 應用程式的資料儲存於 Google Cloud Storage。

Cloud Storage 用於儲存二進位制的大型物件 (BLOB),例如圖片、PDF、或者影片檔,是 App Engine 提供的一種儲存選項。比較 Cloud Storage、Cloud Datastore 以及 Cloud SQL,並選擇符合您應用程式需求的儲存空間。

本指南更進一步探討 App Engine 網誌應用程式,將文字資料儲存在 Cloud SQL,並將圖片資料儲存在 Cloud Storage。

事前準備

設定開發環境並建立 App Engine 專案

建立 Cloud Storage 值區

您的應用程式會把資料儲存在 Cloud Storage 值區中,若要建立新的值區:

下載 App Engine Cloud Storage 程式庫

您的應用程式必須透過 App Engine Tools 程式庫使用 Cloud Storage。

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 Tools 程式庫的匯入功能:

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,其中 servlet 將文字輸入和檔案名稱儲存到 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 Tools 程式庫的 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() 方法使用 copy 寫入 Cloud Storage:

    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 物件為 createOrReplace(),該方法可將檔案寫入 Cloud Storage。

  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,如果父項網誌或圖片已被刪除,則兩個外鍵設定為刪除。

    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]

若要擷取與特定網誌文章相關聯的圖片,您需要從資料庫擷取相關的 URI,如以下針對 Cloud SQL 的 SQL 查詢所示:

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 適用的 App Engine 標準環境