/* * Copyright (c) 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.cloud.backend.spi; import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.EntityNotFoundException; import com.google.appengine.api.datastore.Transaction; import com.google.appengine.api.datastore.TransactionOptions; import com.google.cloud.backend.spi.BlobEndpoint.BlobAccessMode; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.ConcurrentModificationException; import java.util.logging.Level; import java.util.logging.Logger; /** * Internal utility class for blobs stored in Google Cloud Storage and their metadata and * permissions. */ class BlobManager { private static final Logger logger = Logger.getLogger(BlobManager.class.getSimpleName()); static DatastoreService dataStore = DatastoreServiceFactory.getDatastoreService(); /** * Gets blob metadata. * * @param bucketName Google Cloud Storage bucket where the object was uploaded. * @param objectPath path to the object in the bucket. * @return blob metadata or null if there is no object for this objectPath and bucketName. */ public static BlobMetadata getBlobMetadata(String bucketName, String objectPath) { try { Entity metadataEntity = dataStore.get(BlobMetadata.getKey(getCanonicalizedResource(bucketName, objectPath))); return new BlobMetadata(metadataEntity); } catch (EntityNotFoundException e) { return null; } } /** * Stores metadata if this is a new blob or existing blob owned by this user. * * @param bucketName Google Cloud Storage bucket for this blob. * @param objectPath path to the object in the bucket. * @param accessMode controls how the blob can be accessed. * @param ownerId the id of the owner. * @return true if metadata was stored; false if the blob already exists but has a different * owner. */ public static boolean tryStoreBlobMetadata( String bucketName, String objectPath, BlobAccessMode accessMode, String ownerId) { Transaction tx = dataStore.beginTransaction(TransactionOptions.Builder.withXG(true)); try { BlobMetadata metadata = getBlobMetadata(bucketName, objectPath); if (metadata != null) { if (!ownerId.equalsIgnoreCase(metadata.getOwnerId())) { // Object exists and is owned by a different owner. return false; } else if (accessMode == metadata.getAccessMode()) { // The new metadata is the same as the existing one. No need to update anything. return true; } } metadata = new BlobMetadata(getCanonicalizedResource(bucketName, objectPath), accessMode, ownerId); dataStore.put(metadata.getEntity()); tx.commit(); return true; } catch (ConcurrentModificationException e) { return false; } finally { if (tx != null && tx.isActive()) { tx.rollback(); } } } /** * Deletes a blob from Google Cloud Storage and the associated metadata from Datastore. * * @param bucketName Google Cloud Storage bucket for this blob. * @param objectPath path to the object in the bucket. * @return true if the operation succeeded; false otherwise. */ public static boolean deleteBlob(String bucketName, String objectPath) { return deleteBlobFromGcs(bucketName, objectPath) && deleteBlobMetadata(bucketName, objectPath); } /** * Deletes a blob from Google Cloud Storage and associated metadata. * * @param bucketName Google Cloud Storage bucket for this blob. * @param objectPath path to the object in the bucket. * @return true if the operation succeeded; false otherwise. */ private static boolean deleteBlobFromGcs(String bucketName, String objectPath) { /* * Use a signed URL to authenticate with Google Cloud Storage. */ String signedUrl = BlobUrlManager.getDeleteUrl(bucketName, objectPath); /* * If an attempt to delete a blob from GCS fails with a transient error, let's retry it using * exponential back-off (2, 4, 8 ... seconds) with jitter (0..1000 milliseconds). Since the * request needs to complete within 60 seconds, there is enough time to do no more than 5 * attempts. */ for (int attemptNo = 1; attemptNo <= 5; attemptNo++) { try { URL gcsRequestUrl = new URL(signedUrl); HttpURLConnection connection = (HttpURLConnection) gcsRequestUrl.openConnection(); connection.setRequestMethod("DELETE"); int status = connection.getResponseCode(); if (status >= HttpURLConnection.HTTP_OK && status < HttpURLConnection.HTTP_MULT_CHOICE) { // HTTP 2xx response => success. return true; } if (status < HttpURLConnection.HTTP_INTERNAL_ERROR) { // HTTP 3xx or 4xxx => log, do not retry. logger.log(Level.WARNING, "Deleting " + objectPath + " from " + bucketName + " returned " + status); return false; } logger.log(Level.INFO, "Deleting " + objectPath + " from " + bucketName + " failed (attemptNo: " + attemptNo + ") with status code: " + status); } catch (MalformedURLException e) { // This shouldn't happen. logger.log(Level.SEVERE, "URL from BlobUrlManager.getSignedUrl was malformed", e); return false; } catch (IOException e) { logger.log(Level.INFO, "Deleting " + objectPath + " from " + bucketName + " failed (attemptNo: " + attemptNo + ") with " + e.getMessage(), e); } try { Thread.sleep(1000 * (1 << attemptNo) + (int) (Math.random() * 1000)); } catch (InterruptedException e) { return false; } } logger.log(Level.WARNING, "Deleting blob " + objectPath + " from " + bucketName + " failed."); return false; } /** * Deletes blob metadata. * * @param bucketName Google Cloud Storage bucket for this blob. * @param objectPath path to the object in the bucket. * @return true if the operation succeeded; false otherwise. */ private static boolean deleteBlobMetadata(String bucketName, String objectPath) { /* * If an attempt to delete blob metadata fails with a transient error, let's retry it using * exponential back-off (2, 4, 8 ... seconds) with jitter (0..1000 milliseconds). Since the * request needs to complete within 60 seconds, there is enough time to do no more than 5 * attempts. */ for (int attemptNo = 1; attemptNo <= 5; attemptNo++) { try { dataStore.delete(BlobMetadata.getKey(getCanonicalizedResource(bucketName, objectPath))); return true; } catch (ConcurrentModificationException concurrentModificationException) { logger.log(Level.INFO, "Deleting metadata for " + objectPath + " in " + bucketName + " failed with ConcurrentModificationException. Attempt: " + attemptNo); } try { Thread.sleep(1000 * (1 << attemptNo) + (int) (Math.random() * 1000)); } catch (InterruptedException e) { return false; } } logger.log(Level.WARNING, "Deleting blob metadata for " + objectPath + " from " + bucketName + " failed."); return false; } private static String getCanonicalizedResource(String bucketName, String objectPath) { return bucketName + "/" + objectPath; } }