All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.appengine.api.blobstore.BlobstoreServiceImpl Maven / Gradle / Ivy

/*
 * Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.appengine.api.blobstore;

import static java.util.Objects.requireNonNull;

import com.google.appengine.api.blobstore.BlobstoreServicePb.BlobstoreServiceError;
import com.google.appengine.api.blobstore.BlobstoreServicePb.CreateEncodedGoogleStorageKeyRequest;
import com.google.appengine.api.blobstore.BlobstoreServicePb.CreateEncodedGoogleStorageKeyResponse;
import com.google.appengine.api.blobstore.BlobstoreServicePb.CreateUploadURLRequest;
import com.google.appengine.api.blobstore.BlobstoreServicePb.CreateUploadURLResponse;
import com.google.appengine.api.blobstore.BlobstoreServicePb.DeleteBlobRequest;
import com.google.appengine.api.blobstore.BlobstoreServicePb.FetchDataRequest;
import com.google.appengine.api.blobstore.BlobstoreServicePb.FetchDataResponse;
import com.google.apphosting.api.ApiProxy;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.InvalidProtocolBufferException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * {@code BlobstoreServiceImpl} is an implementation of {@link BlobstoreService} that makes API
 * calls to {@link ApiProxy}.
 *
 */
class BlobstoreServiceImpl implements BlobstoreService {
  static final String PACKAGE = "blobstore";
  static final String SERVE_HEADER = "X-AppEngine-BlobKey";
  static final String UPLOADED_BLOBKEY_ATTR = "com.google.appengine.api.blobstore.upload.blobkeys";
  static final String UPLOADED_BLOBINFO_ATTR =
      "com.google.appengine.api.blobstore.upload.blobinfos";
  static final String BLOB_RANGE_HEADER = "X-AppEngine-BlobRange";
  static final String CREATION_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";

  @Override
  public String createUploadUrl(String successPath) {
    return createUploadUrl(successPath, UploadOptions.Builder.withDefaults());
  }

  @Override
  public String createUploadUrl(String successPath, UploadOptions uploadOptions) {
    if (successPath == null) {
      throw new NullPointerException("Success path must not be null.");
    }

    CreateUploadURLRequest.Builder request =
        CreateUploadURLRequest.newBuilder().setSuccessPath(successPath);

    if (uploadOptions.hasMaxUploadSizeBytesPerBlob()) {
      request.setMaxUploadSizePerBlobBytes(uploadOptions.getMaxUploadSizeBytesPerBlob());
    }

    if (uploadOptions.hasMaxUploadSizeBytes()) {
      request.setMaxUploadSizeBytes(uploadOptions.getMaxUploadSizeBytes());
    }

    if (uploadOptions.hasGoogleStorageBucketName()) {
      request.setGsBucketName(uploadOptions.getGoogleStorageBucketName());
    }

    byte[] responseBytes;
    try {
      responseBytes =
          ApiProxy.makeSyncCall(PACKAGE, "CreateUploadURL", request.build().toByteArray());
    } catch (ApiProxy.ApplicationException ex) {
      switch (BlobstoreServiceError.ErrorCode.forNumber(ex.getApplicationError())) {
        case URL_TOO_LONG:
          throw new IllegalArgumentException("The resulting URL was too long.");
        case INTERNAL_ERROR:
          throw new BlobstoreFailureException("An internal blobstore error occurred.");
        default:
          throw new BlobstoreFailureException("An unexpected error occurred.", ex);
      }
    }

    try {
      CreateUploadURLResponse response =
          CreateUploadURLResponse.parseFrom(
              responseBytes, ExtensionRegistry.getEmptyRegistry());
      if (!response.isInitialized()) {
        throw new BlobstoreFailureException("Could not parse CreateUploadURLResponse");
      }
      return response.getUrl();

    } catch (InvalidProtocolBufferException e) {
      throw new IllegalArgumentException(e);
    }
  }

  @Override
  public void serve(BlobKey blobKey, HttpServletResponse response) {
    serve(blobKey, (ByteRange) null, response);
  }

  @Override
  public void serve(BlobKey blobKey, String rangeHeader, HttpServletResponse response) {
    serve(blobKey, ByteRange.parse(rangeHeader), response);
  }

  @Override
  public void serve(BlobKey blobKey, @Nullable ByteRange byteRange, HttpServletResponse response) {
    if (response.isCommitted()) {
      throw new IllegalStateException("Response was already committed.");
    }

    // N.B.(gregwilkins): Content-Length is not needed by blobstore and causes error in jetty94
    response.setContentLength(-1);

    // N.B.: Blobstore serving is only enabled for 200 responses.
    response.setStatus(HttpServletResponse.SC_OK);
    response.setHeader(SERVE_HEADER, blobKey.getKeyString());
    if (byteRange != null) {
      response.setHeader(BLOB_RANGE_HEADER, byteRange.toString());
    }
  }

  @Override
  public @Nullable ByteRange getByteRange(HttpServletRequest request) {
    @SuppressWarnings("unchecked")
    Enumeration rangeHeaders = request.getHeaders("range");
    if (!rangeHeaders.hasMoreElements()) {
      return null;
    }

    String rangeHeader = rangeHeaders.nextElement();
    if (rangeHeaders.hasMoreElements()) {
      throw new UnsupportedRangeFormatException("Cannot accept multiple range headers.");
    }

    return ByteRange.parse(rangeHeader);
  }

  @Override
  public void delete(BlobKey... blobKeys) {
    DeleteBlobRequest.Builder request = DeleteBlobRequest.newBuilder();
    for (BlobKey blobKey : blobKeys) {
      request.addBlobKey(blobKey.getKeyString());
    }

    if (request.getBlobKeyCount() == 0) {
      return;
    }

    try {
      ApiProxy.makeSyncCall(PACKAGE, "DeleteBlob", request.build().toByteArray());
    } catch (ApiProxy.ApplicationException ex) {
      switch (BlobstoreServiceError.ErrorCode.forNumber(ex.getApplicationError())) {
        case INTERNAL_ERROR:
          throw new BlobstoreFailureException("An internal blobstore error occurred.");
        default:
          throw new BlobstoreFailureException("An unexpected error occurred.", ex);
      }
    }
  }

  @Override
  @Deprecated
  public Map getUploadedBlobs(HttpServletRequest request) {
    Map> blobKeys = getUploads(request);
    Map result = Maps.newHashMapWithExpectedSize(blobKeys.size());

    for (Map.Entry> entry : blobKeys.entrySet()) {
      // In throery it is not possible for the value for an entry to be empty,
      // and the following check is simply defensive against a possible future
      // change to that assumption.
      if (!entry.getValue().isEmpty()) {
        result.put(entry.getKey(), entry.getValue().get(0));
      }
    }
    return result;
  }

  @Override
  public Map> getUploads(HttpServletRequest request) {
    // N.B.: We're storing strings instead of BlobKey
    // objects in the request attributes to avoid conflicts between
    // the BlobKey classes loaded by the two classloaders in the
    // DevAppServer.  We convert back to BlobKey objects here.
    @SuppressWarnings("unchecked")
    Map> attributes =
        (Map>) request.getAttribute(UPLOADED_BLOBKEY_ATTR);
    if (attributes == null) {
      throw new IllegalStateException("Must be called from a blob upload callback request.");
    }
    Map> blobKeys = Maps.newHashMapWithExpectedSize(attributes.size());
    for (Map.Entry> attr : attributes.entrySet()) {
      List blobs = new ArrayList<>(attr.getValue().size());
      for (String key : attr.getValue()) {
        blobs.add(new BlobKey(key));
      }
      blobKeys.put(attr.getKey(), blobs);
    }
    return blobKeys;
  }

  @Override
  public Map> getBlobInfos(HttpServletRequest request) {
    @SuppressWarnings("unchecked")
    Map>> attributes =
        (Map>>) request.getAttribute(UPLOADED_BLOBINFO_ATTR);
    if (attributes == null) {
      throw new IllegalStateException("Must be called from a blob upload callback request.");
    }
    Map> blobInfos = Maps.newHashMapWithExpectedSize(attributes.size());
    for (Map.Entry>> attr : attributes.entrySet()) {
      List blobs = new ArrayList<>(attr.getValue().size());
      for (Map info : attr.getValue()) {
        BlobKey key = new BlobKey(requireNonNull(info.get("key"), "Missing key attribute"));
        String contentType =
            requireNonNull(info.get("content-type"), "Missing content-type attribute");
        String creationDateAttribute =
            requireNonNull(info.get("creation-date"), "Missing creation-date attribute");
        Date creationDate =
            requireNonNull(
                parseCreationDate(creationDateAttribute),
                () -> "Bad creation-date attribute: " + creationDateAttribute);
        String filename = requireNonNull(info.get("filename"), "Missing filename attribute");
        int size = Integer.parseInt(requireNonNull(info.get("size"), "Missing size attribute"));
        String md5Hash = requireNonNull(info.get("md5-hash"), "Missing md5-hash attribute");
        String gsObjectName = info.get("gs-name");
        blobs.add(
            new BlobInfo(key, contentType, creationDate, filename, size, md5Hash, gsObjectName));
      }
      blobInfos.put(attr.getKey(), blobs);
    }
    return blobInfos;
  }

  @Override
  public Map> getFileInfos(HttpServletRequest request) {
    @SuppressWarnings("unchecked")
    Map>> attributes =
        (Map>>) request.getAttribute(UPLOADED_BLOBINFO_ATTR);
    if (attributes == null) {
      throw new IllegalStateException("Must be called from a blob upload callback request.");
    }
    Map> fileInfos = Maps.newHashMapWithExpectedSize(attributes.size());
    for (Map.Entry>> attr : attributes.entrySet()) {
      List files = new ArrayList<>(attr.getValue().size());
      for (Map info : attr.getValue()) {
        String contentType =
            requireNonNull(info.get("content-type"), "Missing content-type attribute");
        String creationDateAttribute =
            requireNonNull(info.get("creation-date"), "Missing creation-date attribute");
        Date creationDate =
            requireNonNull(
                parseCreationDate(creationDateAttribute),
                () -> "Invalid creation-date attribute " + creationDateAttribute);
        String filename = requireNonNull(info.get("filename"), "Missing filename attribute");
        long size = Long.parseLong(requireNonNull(info.get("size"), "Missing size attribute"));
        String md5Hash = requireNonNull(info.get("md5-hash"), "Missing md5-hash attribute");
        String gsObjectName = info.getOrDefault("gs-name", null);
        files.add(new FileInfo(contentType, creationDate, filename, size, md5Hash, gsObjectName));
      }
      fileInfos.put(attr.getKey(), files);
    }
    return fileInfos;
  }

  @VisibleForTesting
  protected static @Nullable Date parseCreationDate(String date) {
    Date creationDate = null;
    try {
      date = date.trim().substring(0, CREATION_DATE_FORMAT.length());
      SimpleDateFormat dateFormat = new SimpleDateFormat(CREATION_DATE_FORMAT);
      // Enforce strict adherence to the format
      dateFormat.setLenient(false);
      creationDate = dateFormat.parse(date);
    } catch (IndexOutOfBoundsException e) {
      // This should never happen. We got a date that is shorter than the format.
      // TODO: add log
    } catch (ParseException e) {
      // This should never happen. We got a date that does not match the format.
      // TODO: add log
    }
    return creationDate;
  }

  @Override
  public byte[] fetchData(BlobKey blobKey, long startIndex, long endIndex) {
    if (startIndex < 0) {
      throw new IllegalArgumentException("Start index must be >= 0.");
    }

    if (endIndex < startIndex) {
      throw new IllegalArgumentException("End index must be >= startIndex.");
    }

    // +1 since endIndex is inclusive
    long fetchSize = endIndex - startIndex + 1;
    if (fetchSize > MAX_BLOB_FETCH_SIZE) {
      throw new IllegalArgumentException(
          "Blob fetch size "
              + fetchSize
              + " is larger "
              + "than maximum size "
              + MAX_BLOB_FETCH_SIZE
              + " bytes.");
    }

    FetchDataRequest request =
        FetchDataRequest.newBuilder()
            .setBlobKey(blobKey.getKeyString())
            .setStartIndex(startIndex)
            .setEndIndex(endIndex)
            .build();

    byte[] responseBytes;
    try {
      responseBytes = ApiProxy.makeSyncCall(PACKAGE, "FetchData", request.toByteArray());
    } catch (ApiProxy.ApplicationException ex) {
      switch (BlobstoreServiceError.ErrorCode.forNumber(ex.getApplicationError())) {
        case PERMISSION_DENIED:
          throw new SecurityException("This application does not have access to that blob.");
        case BLOB_NOT_FOUND:
          throw new IllegalArgumentException("Blob not found.");
        case INTERNAL_ERROR:
          throw new BlobstoreFailureException("An internal blobstore error occurred.");
        default:
          throw new BlobstoreFailureException("An unexpected error occurred.", ex);
      }
    }

    try {
      FetchDataResponse response =
          FetchDataResponse.parseFrom(responseBytes, ExtensionRegistry.getEmptyRegistry());
      if (!response.isInitialized()) {
        throw new BlobstoreFailureException("Could not parse FetchDataResponse");
      }
      return response.getData().toByteArray();
    } catch (InvalidProtocolBufferException e) {
      throw new IllegalArgumentException(e);
    }
  }

  @Override
  public BlobKey createGsBlobKey(String filename) {

    if (!filename.startsWith("/gs/")) {
      throw new IllegalArgumentException(
          "Google storage filenames must be" + " prefixed with /gs/");
    }
    CreateEncodedGoogleStorageKeyRequest request =
        CreateEncodedGoogleStorageKeyRequest.newBuilder().setFilename(filename).build();

    byte[] responseBytes;
    try {
      responseBytes =
          ApiProxy.makeSyncCall(PACKAGE, "CreateEncodedGoogleStorageKey", request.toByteArray());
    } catch (ApiProxy.ApplicationException ex) {
      switch (BlobstoreServiceError.ErrorCode.forNumber(ex.getApplicationError())) {
        case INTERNAL_ERROR:
          throw new BlobstoreFailureException("An internal blobstore error occurred.");
        default:
          throw new BlobstoreFailureException("An unexpected error occurred.", ex);
      }
    }

    try {
      CreateEncodedGoogleStorageKeyResponse response =
          CreateEncodedGoogleStorageKeyResponse.parseFrom(
              responseBytes, ExtensionRegistry.getEmptyRegistry());
      if (!response.isInitialized()) {
        throw new BlobstoreFailureException(
            "Could not parse CreateEncodedGoogleStorageKeyResponse");
      }
      return new BlobKey(response.getBlobKey());
    } catch (InvalidProtocolBufferException e) {
      throw new IllegalArgumentException(e);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy