diff --git a/.github/workflows/samples.yaml b/.github/workflows/samples.yaml new file mode 100644 index 000000000..a1d500730 --- /dev/null +++ b/.github/workflows/samples.yaml @@ -0,0 +1,14 @@ +on: + pull_request: +name: samples +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 8 + - name: Run checkstyle + run: mvn -P lint --quiet --batch-mode checkstyle:check + working-directory: samples/snippets diff --git a/.kokoro/continuous/dependencies.cfg b/.kokoro/continuous/dependencies.cfg deleted file mode 100644 index a8b9fbf2d..000000000 --- a/.kokoro/continuous/dependencies.cfg +++ /dev/null @@ -1,12 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/java8" -} - -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/java-storage/.kokoro/dependencies.sh" -} diff --git a/.kokoro/continuous/integration.cfg b/.kokoro/continuous/integration.cfg deleted file mode 100644 index 3b017fc80..000000000 --- a/.kokoro/continuous/integration.cfg +++ /dev/null @@ -1,7 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/java8" -} diff --git a/.kokoro/continuous/java11.cfg b/.kokoro/continuous/java11.cfg deleted file mode 100644 index 709f2b4c7..000000000 --- a/.kokoro/continuous/java11.cfg +++ /dev/null @@ -1,7 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/java11" -} diff --git a/.kokoro/continuous/java7.cfg b/.kokoro/continuous/java7.cfg deleted file mode 100644 index cb24f44ee..000000000 --- a/.kokoro/continuous/java7.cfg +++ /dev/null @@ -1,7 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/java7" -} diff --git a/.kokoro/continuous/java8-osx.cfg b/.kokoro/continuous/java8-osx.cfg deleted file mode 100644 index 79fedbfd8..000000000 --- a/.kokoro/continuous/java8-osx.cfg +++ /dev/null @@ -1,3 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -build_file: "java-storage/.kokoro/build.sh" diff --git a/.kokoro/continuous/java8-win.cfg b/.kokoro/continuous/java8-win.cfg deleted file mode 100644 index 6ae002330..000000000 --- a/.kokoro/continuous/java8-win.cfg +++ /dev/null @@ -1,3 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -build_file: "java-storage/.kokoro/build.bat" diff --git a/.kokoro/continuous/lint.cfg b/.kokoro/continuous/lint.cfg deleted file mode 100644 index 6d323c8ae..000000000 --- a/.kokoro/continuous/lint.cfg +++ /dev/null @@ -1,13 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Configure the docker image for kokoro-trampoline. - -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/java8" -} - -env_vars: { - key: "JOB_TYPE" - value: "lint" -} \ No newline at end of file diff --git a/.kokoro/continuous/propose_release.cfg b/.kokoro/continuous/propose_release.cfg deleted file mode 100644 index b115c5eb8..000000000 --- a/.kokoro/continuous/propose_release.cfg +++ /dev/null @@ -1,53 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Build logs will be here -action { - define_artifacts { - regex: "**/*sponge_log.xml" - } -} - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "java-storage/.kokoro/trampoline.sh" - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/node:10-user" -} - -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/java-storage/.kokoro/continuous/propose_release.sh" -} - -# tokens used by release-please to keep an up-to-date release PR. -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "github-magic-proxy-key-release-please" - } - } -} - -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "github-magic-proxy-token-release-please" - } - } -} - -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "github-magic-proxy-url-release-please" - } - } -} diff --git a/.kokoro/continuous/samples.cfg b/.kokoro/continuous/samples.cfg deleted file mode 100644 index fa7b493d0..000000000 --- a/.kokoro/continuous/samples.cfg +++ /dev/null @@ -1,31 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/java8" -} - -env_vars: { - key: "JOB_TYPE" - value: "samples" -} - -env_vars: { - key: "GCLOUD_PROJECT" - value: "gcloud-devel" -} - -env_vars: { - key: "GOOGLE_APPLICATION_CREDENTIALS" - value: "keystore/73713_java_it_service_account" -} - -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "java_it_service_account" - } - } -} diff --git a/.kokoro/dependencies.sh b/.kokoro/dependencies.sh index cee4f11e7..c91e5a569 100755 --- a/.kokoro/dependencies.sh +++ b/.kokoro/dependencies.sh @@ -43,12 +43,13 @@ function completenessCheck() { # Output dep list with compile scope generated using the original pom # Running mvn dependency:list on Java versions that support modules will also include the module of the dependency. # This is stripped from the output as it is not present in the flattened pom. + # Only dependencies with 'compile' or 'runtime' scope are included from original dependency list. msg "Generating dependency list using original pom..." - mvn dependency:list -f pom.xml -Dsort=true | grep '\[INFO] .*:.*:.*:.*:.*' | sed -e s/\\s--\\smodule.*// | grep -v ':test$' >.org-list.txt + mvn dependency:list -f pom.xml -DincludeScope=runtime -Dsort=true | grep '\[INFO] .*:.*:.*:.*:.*' | sed -e s/\\s--\\smodule.*// >.org-list.txt - # Output dep list generated using the flattened pom (test scope deps are ommitted) + # Output dep list generated using the flattened pom (only 'compile' and 'runtime' scopes) msg "Generating dependency list using flattened pom..." - mvn dependency:list -f .flattened-pom.xml -Dsort=true | grep '\[INFO] .*:.*:.*:.*:.*' >.new-list.txt + mvn dependency:list -f .flattened-pom.xml -DincludeScope=runtime -Dsort=true | grep '\[INFO] .*:.*:.*:.*:.*' >.new-list.txt # Compare two dependency lists msg "Comparing dependency lists..." @@ -85,4 +86,4 @@ then else msg "Errors found. See log statements above." exit 1 -fi +fi \ No newline at end of file diff --git a/.kokoro/nightly/dependencies.cfg b/.kokoro/nightly/dependencies.cfg deleted file mode 100644 index a8b9fbf2d..000000000 --- a/.kokoro/nightly/dependencies.cfg +++ /dev/null @@ -1,12 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/java8" -} - -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/java-storage/.kokoro/dependencies.sh" -} diff --git a/.kokoro/nightly/lint.cfg b/.kokoro/nightly/lint.cfg deleted file mode 100644 index 6d323c8ae..000000000 --- a/.kokoro/nightly/lint.cfg +++ /dev/null @@ -1,13 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Configure the docker image for kokoro-trampoline. - -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/java8" -} - -env_vars: { - key: "JOB_TYPE" - value: "lint" -} \ No newline at end of file diff --git a/.kokoro/release/publish_javadoc.cfg b/.kokoro/release/publish_javadoc.cfg index 82745e582..c4cb55210 100644 --- a/.kokoro/release/publish_javadoc.cfg +++ b/.kokoro/release/publish_javadoc.cfg @@ -1,14 +1,24 @@ # Format: //devtools/kokoro/config/proto/build.proto + +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/doc-templates/" + env_vars: { key: "STAGING_BUCKET" value: "docs-staging" } +env_vars: { + key: "STAGING_BUCKET_V2" + value: "docs-staging-v2-staging" + # Production will be at: docs-staging-v2 +} + env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: "github/java-storage/.kokoro/release/publish_javadoc.sh" } + before_action { fetch_keystore { keystore_resource { diff --git a/.kokoro/release/publish_javadoc.sh b/.kokoro/release/publish_javadoc.sh index 219d37c60..6b5920141 100755 --- a/.kokoro/release/publish_javadoc.sh +++ b/.kokoro/release/publish_javadoc.sh @@ -24,6 +24,11 @@ if [[ -z "${STAGING_BUCKET}" ]]; then exit 1 fi +if [[ -z "${STAGING_BUCKET_V2}" ]]; then + echo "Need to set STAGING_BUCKET_V2 environment variable" + exit 1 +fi + # work from the git root directory pushd $(dirname "$0")/../../ @@ -31,13 +36,13 @@ pushd $(dirname "$0")/../../ python3 -m pip install gcp-docuploader # compile all packages -mvn clean install -B -DskipTests=true +mvn clean install -B -q -DskipTests=true NAME=google-cloud-storage VERSION=$(grep ${NAME}: versions.txt | cut -d: -f3) # build the docs -mvn site -B +mvn site -B -q pushd target/site/apidocs @@ -53,3 +58,19 @@ python3 -m docuploader upload . \ --staging-bucket ${STAGING_BUCKET} popd + +# V2 +mvn clean site -B -q -Ddevsite.template="${KOKORO_GFILE_DIR}/java/" + +pushd target/devsite + +# create metadata +python3 -m docuploader create-metadata \ + --name ${NAME} \ + --version ${VERSION} \ + --language java + +# upload docs +python3 -m docuploader upload . \ + --credentials ${CREDENTIALS} \ + --staging-bucket ${STAGING_BUCKET_V2} diff --git a/.kokoro/release/stage.sh b/.kokoro/release/stage.sh index 3c482cbc5..d19191fc8 100755 --- a/.kokoro/release/stage.sh +++ b/.kokoro/release/stage.sh @@ -16,8 +16,9 @@ set -eo pipefail # Start the releasetool reporter -python3 -m pip install gcp-releasetool -python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script +# Disable reporting due to issue observed with Kokoro blocking releases +# python3 -m pip install gcp-releasetool +# python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script source $(dirname "$0")/common.sh MAVEN_SETTINGS_FILE=$(realpath $(dirname "$0")/../../)/settings.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 38658d9d9..996cdcd05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## [1.112.0](https://www.github.com/googleapis/java-storage/compare/v1.111.2...v1.112.0) (2020-08-27) + + +### Features + +* add support of customTime metadata ([#413](https://www.github.com/googleapis/java-storage/issues/413)) ([6f4585e](https://www.github.com/googleapis/java-storage/commit/6f4585eb6706390865cf5fb565fa8062d0071045)) +* add support of customTimeBefore and daysSinceCustomTime ([#396](https://www.github.com/googleapis/java-storage/issues/396)) ([1af8288](https://www.github.com/googleapis/java-storage/commit/1af8288016f2526ddbe221ef22dc705e28b18b77)) +* add support of daysSinceNoncurrentTime and noncurrentTimeBefore OLM options ([#335](https://www.github.com/googleapis/java-storage/issues/335)) ([1e3e88a](https://www.github.com/googleapis/java-storage/commit/1e3e88a391651421469e5c7a8216a788eaa4ba5a)) +* add support of null to remove the CORS configuration from bucket ([#438](https://www.github.com/googleapis/java-storage/issues/438)) ([f8a4b12](https://www.github.com/googleapis/java-storage/commit/f8a4b12517c661881d7b7c65f796c1c8f1cf3ae9)) +* add support of startOffset and endOffset ([#430](https://www.github.com/googleapis/java-storage/issues/430)) ([38c1c34](https://www.github.com/googleapis/java-storage/commit/38c1c34937eeacd126cf6d62bf85fb9db90e1702)) +* auto content-type on blob creation ([#338](https://www.github.com/googleapis/java-storage/issues/338)) ([66d1eb7](https://www.github.com/googleapis/java-storage/commit/66d1eb793383b9e83992824b392cedd28d54609f)) +* expose updateTime field of the bucket ([#449](https://www.github.com/googleapis/java-storage/issues/449)) ([f0e945e](https://www.github.com/googleapis/java-storage/commit/f0e945e14662b86594298557b83151d3cb7e1ebb)) + + +### Bug Fixes + +* Ignore CONTRIBUTING.md ([#447](https://www.github.com/googleapis/java-storage/issues/447)) ([bdacdc9](https://www.github.com/googleapis/java-storage/commit/bdacdc93a107108add5bd9dc00473997534aa761)), closes [#446](https://www.github.com/googleapis/java-storage/issues/446) [#446](https://www.github.com/googleapis/java-storage/issues/446) +* PostPolicyV4 classes could be improved ([#442](https://www.github.com/googleapis/java-storage/issues/442)) ([8602b81](https://www.github.com/googleapis/java-storage/commit/8602b81eae95868e184fd4ab290396707bd21a8e)) +* **docs:** example of Storage#testIamPermissions ([#434](https://www.github.com/googleapis/java-storage/issues/434)) ([275f452](https://www.github.com/googleapis/java-storage/commit/275f452a5993f95a84fb603a5f4b436238b39439)) +* PostPolicyV4.PostFieldsV4.Builder.addCustomMetadataField() allows to add prefixed an not prefixed custom fields ([#398](https://www.github.com/googleapis/java-storage/issues/398)) ([02dc3b5](https://www.github.com/googleapis/java-storage/commit/02dc3b5e5377d8848c889647e72102cd9acc646d)) + + +### Dependencies + +* update dependency com.google.api-client:google-api-client to v1.30.10 ([#423](https://www.github.com/googleapis/java-storage/issues/423)) ([fbfa9ec](https://www.github.com/googleapis/java-storage/commit/fbfa9ecf277794e07d9a3c46d5b5022f54c37afd)) +* update dependency com.google.api.grpc:grpc-google-cloud-kms-v1 to v0.86.1 ([#463](https://www.github.com/googleapis/java-storage/issues/463)) ([cf94230](https://www.github.com/googleapis/java-storage/commit/cf94230a5f02dcc16e364aa528d97046d80f59a0)) +* update dependency com.google.api.grpc:proto-google-cloud-kms-v1 to v0.86.1 ([#464](https://www.github.com/googleapis/java-storage/issues/464)) ([6c372fa](https://www.github.com/googleapis/java-storage/commit/6c372fa81e49ac74bdda6f9b10914fac42767247)) +* update dependency com.google.apis:google-api-services-storage to v1-rev20200611-1.30.10 ([#428](https://www.github.com/googleapis/java-storage/issues/428)) ([6ef57eb](https://www.github.com/googleapis/java-storage/commit/6ef57ebc9eeddc90f13ef87274e8ab0b7eb53290)) +* update dependency com.google.apis:google-api-services-storage to v1-rev20200727-1.30.10 ([#457](https://www.github.com/googleapis/java-storage/issues/457)) ([edfd1e6](https://www.github.com/googleapis/java-storage/commit/edfd1e69e886adb04b98b54b3a63768c7e82b1e0)) +* update dependency com.google.cloud:google-cloud-shared-dependencies to v0.8.4 ([#452](https://www.github.com/googleapis/java-storage/issues/452)) ([12bc02d](https://www.github.com/googleapis/java-storage/commit/12bc02d7bc05e584cad4362628155333630fbcba)) +* update dependency com.google.cloud:google-cloud-shared-dependencies to v0.8.6 ([#458](https://www.github.com/googleapis/java-storage/issues/458)) ([f8d6e15](https://www.github.com/googleapis/java-storage/commit/f8d6e158a06aec926fb7bc42f10483d56696a37e)) + ### [1.111.2](https://www.github.com/googleapis/java-storage/compare/v1.111.1...v1.111.2) (2020-07-10) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2dbdee06..ccbd5094f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,6 +48,7 @@ integration tests. ```bash export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service/account.json +export IT_SERVICE_ACCOUNT_EMAIL=my-service-account@my-project.iam.gserviceaccount.com mvn -Penable-integration-tests clean verify ``` diff --git a/README.md b/README.md index 230e23076..288383da5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file com.google.cloud libraries-bom - 7.0.0 + 9.1.0 pom import @@ -38,7 +38,7 @@ If you are using Maven without BOM, add this to your dependencies: com.google.cloud google-cloud-storage - 1.111.0 + 1.111.2 ``` @@ -47,11 +47,11 @@ If you are using Maven without BOM, add this to your dependencies: If you are using Gradle, add this to your dependencies ```Groovy -compile 'com.google.cloud:google-cloud-storage:1.111.2' +compile 'com.google.cloud:google-cloud-storage:1.112.0' ``` If you are using SBT, add this to your dependencies ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "1.111.2" +libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "1.112.0" ``` [//]: # ({x-version-update-end}) diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml index bca2faaff..114310c67 100644 --- a/google-cloud-storage/clirr-ignored-differences.xml +++ b/google-cloud-storage/clirr-ignored-differences.xml @@ -21,4 +21,9 @@ com.google.cloud.storage.BucketInfo$Builder deleteLifecycleRules() 7013 + + com/google/cloud/storage/BucketInfo$Builder + com.google.cloud.storage.BucketInfo$Builder setUpdateTime(java.lang.Long) + 7013 + diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml index 196a66538..780862d6b 100644 --- a/google-cloud-storage/pom.xml +++ b/google-cloud-storage/pom.xml @@ -2,7 +2,7 @@ 4.0.0 google-cloud-storage - 1.111.2 + 1.112.0 jar Google Cloud Storage https://github.com/googleapis/java-storage @@ -12,7 +12,7 @@ com.google.cloud google-cloud-storage-parent - 1.111.2 + 1.112.0 google-cloud-storage @@ -33,7 +33,7 @@ com.google.api-client google-api-client - 1.30.9 + 1.30.10 com.google.apis @@ -117,13 +117,13 @@ com.google.api.grpc grpc-google-cloud-kms-v1 - 0.86.0 + 0.86.1 test com.google.api.grpc proto-google-cloud-kms-v1 - 0.86.0 + 0.86.1 test diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java index 679b410a6..398eb3166 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java @@ -434,6 +434,12 @@ Builder setCreateTime(Long createTime) { return this; } + @Override + public Builder setCustomTime(Long customTime) { + infoBuilder.setCustomTime(customTime); + return this; + } + @Override Builder setIsDirectory(boolean isDirectory) { infoBuilder.setIsDirectory(isDirectory); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java index ef38ba033..67256d1c7 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java @@ -84,6 +84,7 @@ public StorageObject apply(BlobInfo blobInfo) { private final String etag; private final String md5; private final String crc32c; + private final Long customTime; private final String mediaLink; private final Map metadata; private final Long metageneration; @@ -260,6 +261,26 @@ public abstract static class Builder { */ public abstract Builder setCrc32c(String crc32c); + /** + * Sets the custom time for an object. Once set it can't be unset and only changed to a custom + * datetime in the future. To unset the custom time, you must either perform a rewrite operation + * or upload the data again. + * + *

Example of setting the custom time. + * + *

{@code
+     * String bucketName = "my-unique-bucket";
+     * String blobName = "my-blob-name";
+     * long customTime = 1598423868301L;
+     * BlobInfo blob = BlobInfo.newBuilder(bucketName, blobName).setCustomTime(customTime).build();
+     * }
+ */ + public Builder setCustomTime(Long customTime) { + throw new UnsupportedOperationException( + "Override setCustomTime with your own implementation," + + " or use com.google.cloud.storage.Blob."); + } + /** * Sets the CRC32C checksum of blob's data as described in RFC 4960, Appendix B; from hex @@ -325,6 +346,7 @@ static final class BuilderImpl extends Builder { private String selfLink; private String md5; private String crc32c; + private Long customTime; private String mediaLink; private Map metadata; private Long metageneration; @@ -360,6 +382,7 @@ static final class BuilderImpl extends Builder { selfLink = blobInfo.selfLink; md5 = blobInfo.md5; crc32c = blobInfo.crc32c; + customTime = blobInfo.customTime; mediaLink = blobInfo.mediaLink; metadata = blobInfo.metadata; metageneration = blobInfo.metageneration; @@ -488,6 +511,12 @@ public Builder setCrc32c(String crc32c) { return this; } + @Override + public Builder setCustomTime(Long customTime) { + this.customTime = customTime; + return this; + } + @Override public Builder setCrc32cFromHexString(String crc32cHexString) { if (crc32cHexString == null) { @@ -619,6 +648,7 @@ public BlobInfo build() { selfLink = builder.selfLink; md5 = builder.md5; crc32c = builder.crc32c; + customTime = builder.customTime; mediaLink = builder.mediaLink; metadata = builder.metadata; metageneration = builder.metageneration; @@ -857,6 +887,11 @@ public Long getCreateTime() { return createTime; } + /** Returns the custom time specified by the user for an object. */ + public Long getCustomTime() { + return customTime; + } + /** * Returns {@code true} if the current blob represents a directory. This can only happen if the * blob is returned by {@link Storage#list(String, Storage.BlobListOption...)} when the {@link @@ -1002,6 +1037,9 @@ public ObjectAccessControl apply(Acl acl) { if (createTime != null) { storageObject.setTimeCreated(new DateTime(createTime)); } + if (customTime != null) { + storageObject.setCustomTime(new DateTime(customTime)); + } if (size != null) { storageObject.setSize(BigInteger.valueOf(size)); } @@ -1125,6 +1163,9 @@ static BlobInfo fromPb(StorageObject storageObject) { if (storageObject.getTimeCreated() != null) { builder.setCreateTime(storageObject.getTimeCreated().getValue()); } + if (storageObject.getCustomTime() != null) { + builder.setCustomTime(storageObject.getCustomTime().getValue()); + } if (storageObject.getSize() != null) { builder.setSize(storageObject.getSize().longValue()); } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java index c64a3dc5e..3714e6e94 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java @@ -601,6 +601,12 @@ Builder setCreateTime(Long createTime) { return this; } + @Override + Builder setUpdateTime(Long updateTime) { + infoBuilder.setUpdateTime(updateTime); + return this; + } + @Override Builder setMetageneration(Long metageneration) { infoBuilder.setMetageneration(metageneration); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java index 9a716d702..602be08ac 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java @@ -84,6 +84,7 @@ public com.google.api.services.storage.model.Bucket apply(BucketInfo bucketInfo) private final List lifecycleRules; private final String etag; private final Long createTime; + private final Long updateTime; private final Long metageneration; private final List cors; private final List acl; @@ -350,7 +351,11 @@ public LifecycleRule(LifecycleAction action, LifecycleCondition condition) { && condition.getAge() == null && condition.getCreatedBefore() == null && condition.getMatchesStorageClass() == null - && condition.getNumberOfNewerVersions() == null) { + && condition.getNumberOfNewerVersions() == null + && condition.getDaysSinceNoncurrentTime() == null + && condition.getNoncurrentTimeBefore() == null + && condition.getCustomTimeBefore() == null + && condition.getDaysSinceCustomTime() == null) { throw new IllegalArgumentException( "You must specify at least one condition to use object lifecycle " + "management. Please see https://cloud.google.com/storage/docs/lifecycle for details."); @@ -418,7 +423,18 @@ Rule toPb() { ? null : transform( lifecycleCondition.getMatchesStorageClass(), - Functions.toStringFunction())); + Functions.toStringFunction())) + .setDaysSinceNoncurrentTime(lifecycleCondition.getDaysSinceNoncurrentTime()) + .setNoncurrentTimeBefore( + lifecycleCondition.getNoncurrentTimeBefore() == null + ? null + : new DateTime( + true, lifecycleCondition.getNoncurrentTimeBefore().getValue(), 0)) + .setCustomTimeBefore( + lifecycleCondition.getCustomTimeBefore() == null + ? null + : new DateTime(true, lifecycleCondition.getCustomTimeBefore().getValue(), 0)) + .setDaysSinceCustomTime(lifecycleCondition.getDaysSinceCustomTime()); rule.setCondition(condition); @@ -461,7 +477,11 @@ static LifecycleRule fromPb(Rule rule) { public StorageClass apply(String storageClass) { return StorageClass.valueOf(storageClass); } - })); + })) + .setDaysSinceNoncurrentTime(condition.getDaysSinceNoncurrentTime()) + .setNoncurrentTimeBefore(condition.getNoncurrentTimeBefore()) + .setCustomTimeBefore(condition.getCustomTimeBefore()) + .setDaysSinceCustomTime(condition.getDaysSinceCustomTime()); return new LifecycleRule(lifecycleAction, conditionBuilder.build()); } @@ -479,6 +499,10 @@ public static class LifecycleCondition implements Serializable { private final Integer numberOfNewerVersions; private final Boolean isLive; private final List matchesStorageClass; + private final Integer daysSinceNoncurrentTime; + private final DateTime noncurrentTimeBefore; + private final DateTime customTimeBefore; + private final Integer daysSinceCustomTime; private LifecycleCondition(Builder builder) { this.age = builder.age; @@ -486,6 +510,10 @@ private LifecycleCondition(Builder builder) { this.numberOfNewerVersions = builder.numberOfNewerVersions; this.isLive = builder.isLive; this.matchesStorageClass = builder.matchesStorageClass; + this.daysSinceNoncurrentTime = builder.daysSinceNoncurrentTime; + this.noncurrentTimeBefore = builder.noncurrentTimeBefore; + this.customTimeBefore = builder.customTimeBefore; + this.daysSinceCustomTime = builder.daysSinceCustomTime; } public Builder toBuilder() { @@ -494,7 +522,11 @@ public Builder toBuilder() { .setCreatedBefore(this.createdBefore) .setNumberOfNewerVersions(this.numberOfNewerVersions) .setIsLive(this.isLive) - .setMatchesStorageClass(this.matchesStorageClass); + .setMatchesStorageClass(this.matchesStorageClass) + .setDaysSinceNoncurrentTime(this.daysSinceNoncurrentTime) + .setNoncurrentTimeBefore(this.noncurrentTimeBefore) + .setCustomTimeBefore(this.customTimeBefore) + .setDaysSinceCustomTime(this.daysSinceCustomTime); } public static Builder newBuilder() { @@ -509,6 +541,10 @@ public String toString() { .add("numberofNewerVersions", numberOfNewerVersions) .add("isLive", isLive) .add("matchesStorageClass", matchesStorageClass) + .add("daysSinceNoncurrentTime", daysSinceNoncurrentTime) + .add("noncurrentTimeBefore", noncurrentTimeBefore) + .add("customTimeBefore", customTimeBefore) + .add("daysSinceCustomTime", daysSinceCustomTime) .toString(); } @@ -532,6 +568,28 @@ public List getMatchesStorageClass() { return matchesStorageClass; } + /** Returns the number of days elapsed since the noncurrent timestamp of an object. */ + public Integer getDaysSinceNoncurrentTime() { + return daysSinceNoncurrentTime; + } + + /** + * Returns the date in RFC 3339 format with only the date part (for instance, "2013-01-15"). + */ + public DateTime getNoncurrentTimeBefore() { + return noncurrentTimeBefore; + } + + /* Returns the date in RFC 3339 format with only the date part (for instance, "2013-01-15").*/ + public DateTime getCustomTimeBefore() { + return customTimeBefore; + } + + /** Returns the number of days elapsed since the user-specified timestamp set on an object. */ + public Integer getDaysSinceCustomTime() { + return daysSinceCustomTime; + } + /** Builder for {@code LifecycleCondition}. */ public static class Builder { private Integer age; @@ -539,6 +597,10 @@ public static class Builder { private Integer numberOfNewerVersions; private Boolean isLive; private List matchesStorageClass; + private Integer daysSinceNoncurrentTime; + private DateTime noncurrentTimeBefore; + private DateTime customTimeBefore; + private Integer daysSinceCustomTime; private Builder() {} @@ -593,6 +655,50 @@ public Builder setMatchesStorageClass(List matchesStorageClass) { return this; } + /** + * Sets the number of days elapsed since the noncurrent timestamp of an object. The + * condition is satisfied if the days elapsed is at least this number. This condition is + * relevant only for versioned objects. The value of the field must be a nonnegative + * integer. If it's zero, the object version will become eligible for Lifecycle action as + * soon as it becomes noncurrent. + */ + public Builder setDaysSinceNoncurrentTime(Integer daysSinceNoncurrentTime) { + this.daysSinceNoncurrentTime = daysSinceNoncurrentTime; + return this; + } + + /** + * Sets the date in RFC 3339 format with only the date part (for instance, "2013-01-15"). + * Note that only date part will be considered, if the time is specified it will be + * truncated. This condition is satisfied when the noncurrent time on an object is before + * this date. This condition is relevant only for versioned objects. + */ + public Builder setNoncurrentTimeBefore(DateTime noncurrentTimeBefore) { + this.noncurrentTimeBefore = noncurrentTimeBefore; + return this; + } + + /** + * Sets the date in RFC 3339 format with only the date part (for instance, "2013-01-15"). + * Note that only date part will be considered, if the time is specified it will be + * truncated. This condition is satisfied when the custom time on an object is before this + * date in UTC. + */ + public Builder setCustomTimeBefore(DateTime customTimeBefore) { + this.customTimeBefore = customTimeBefore; + return this; + } + + /** + * Sets the number of days elapsed since the user-specified timestamp set on an object. The + * condition is satisfied if the days elapsed is at least this number. If no custom + * timestamp is specified on an object, the condition does not apply. + */ + public Builder setDaysSinceCustomTime(Integer daysSinceCustomTime) { + this.daysSinceCustomTime = daysSinceCustomTime; + return this; + } + /** Builds a {@code LifecycleCondition} object. * */ public LifecycleCondition build() { return new LifecycleCondition(this); @@ -1005,6 +1111,8 @@ public abstract static class Builder { abstract Builder setCreateTime(Long createTime); + abstract Builder setUpdateTime(Long updateTime); + abstract Builder setMetageneration(Long metageneration); abstract Builder setLocationType(String locationType); @@ -1090,6 +1198,7 @@ static final class BuilderImpl extends Builder { private String location; private String etag; private Long createTime; + private Long updateTime; private Long metageneration; private List cors; private List acl; @@ -1113,6 +1222,7 @@ static final class BuilderImpl extends Builder { name = bucketInfo.name; etag = bucketInfo.etag; createTime = bucketInfo.createTime; + updateTime = bucketInfo.updateTime; metageneration = bucketInfo.metageneration; location = bucketInfo.location; storageClass = bucketInfo.storageClass; @@ -1232,6 +1342,12 @@ Builder setCreateTime(Long createTime) { return this; } + @Override + Builder setUpdateTime(Long updateTime) { + this.updateTime = updateTime; + return this; + } + @Override Builder setMetageneration(Long metageneration) { this.metageneration = metageneration; @@ -1240,7 +1356,7 @@ Builder setMetageneration(Long metageneration) { @Override public Builder setCors(Iterable cors) { - this.cors = cors != null ? ImmutableList.copyOf(cors) : null; + this.cors = cors != null ? ImmutableList.copyOf(cors) : ImmutableList.of(); return this; } @@ -1337,6 +1453,7 @@ public BucketInfo build() { name = builder.name; etag = builder.etag; createTime = builder.createTime; + updateTime = builder.updateTime; metageneration = builder.metageneration; location = builder.location; storageClass = builder.storageClass; @@ -1468,6 +1585,14 @@ public Long getCreateTime() { return createTime; } + /** + * Returns the last modification time of the bucket's metadata expressed as the number of + * milliseconds since the Unix epoch. + */ + public Long getUpdateTime() { + return updateTime; + } + /** Returns the metadata generation of this bucket. */ public Long getMetageneration() { return metageneration; @@ -1650,6 +1775,9 @@ com.google.api.services.storage.model.Bucket toPb() { if (createTime != null) { bucketPb.setTimeCreated(new DateTime(createTime)); } + if (updateTime != null) { + bucketPb.setUpdated(new DateTime(updateTime)); + } if (metageneration != null) { bucketPb.setMetageneration(metageneration); } @@ -1797,6 +1925,9 @@ static BucketInfo fromPb(com.google.api.services.storage.model.Bucket bucketPb) if (bucketPb.getTimeCreated() != null) { builder.setCreateTime(bucketPb.getTimeCreated().getValue()); } + if (bucketPb.getUpdated() != null) { + builder.setUpdateTime(bucketPb.getUpdated().getValue()); + } if (bucketPb.getLocation() != null) { builder.setLocation(bucketPb.getLocation()); } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/PostPolicyV4.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/PostPolicyV4.java index df2936dc9..96afca06e 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/PostPolicyV4.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/PostPolicyV4.java @@ -18,56 +18,125 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import java.net.URI; +import java.net.URISyntaxException; import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.TimeUnit; /** - * Presigned V4 post policy. + * Presigned V4 post policy. Instances of {@code PostPolicyV4} include a URL and a map of fields + * that can be specified in an HTML form to submit a POST request to upload an object. * - * @see POST Object + *

See POST Object for + * details of upload by using HTML forms. + * + *

See {@link Storage#generateSignedPostPolicyV4(BlobInfo, long, TimeUnit, + * PostPolicyV4.PostFieldsV4, PostPolicyV4.PostConditionsV4, Storage.PostPolicyV4Option...)} for + * example of usage. */ public final class PostPolicyV4 { - private String url; - private Map fields; + private final String url; + private final Map fields; private PostPolicyV4(String url, Map fields) { + try { + if (!new URI(url).isAbsolute()) { + throw new IllegalArgumentException(url + " is not an absolute URL"); + } + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + PostFieldsV4.validateFields(fields); + this.url = url; - this.fields = fields; + this.fields = Collections.unmodifiableMap(fields); } + /** + * Constructs {@code PostPolicyV4} instance of the given URL and fields map. + * + * @param url URL for the HTTP POST request + * @param fields HTML form fields + * @return constructed object + * @throws IllegalArgumentException if URL is malformed or fields are not valid + */ public static PostPolicyV4 of(String url, Map fields) { return new PostPolicyV4(url, fields); } + /** Returns the URL for the HTTP POST request */ public String getUrl() { return url; } + /** Returns the HTML form fields */ public Map getFields() { return fields; } /** - * Class representing which fields to specify in a V4 POST request. + * A helper class to define fields to be specified in a V4 POST request. Instance of this class + * helps to construct {@code PostPolicyV4} objects. Used in: {@link + * Storage#generateSignedPostPolicyV4(BlobInfo, long, TimeUnit, PostPolicyV4.PostFieldsV4, + * PostPolicyV4.PostConditionsV4, Storage.PostPolicyV4Option...)}. * * @see POST * Object Form fields */ public static final class PostFieldsV4 { - private Map fieldsMap; + private final Map fieldsMap; + private static final List VALID_FIELDS = + Arrays.asList( + "acl", + "bucket", + "cache-control", + "content-disposition", + "content-encoding", + "content-type", + "expires", + "file", + "key", + "policy", + "success_action_redirect", + "success_action_status", + "x-goog-algorithm", + "x-goog-credential", + "x-goog-date", + "x-goog-signature"); + + private static void validateFields(Map fields) { + for (String key : fields.keySet()) { + if (!VALID_FIELDS.contains(key.toLowerCase()) + && !key.startsWith(Builder.CUSTOM_FIELD_PREFIX)) { + throw new IllegalArgumentException("Invalid key: " + key); + } + } + } private PostFieldsV4(Builder builder) { - this.fieldsMap = builder.fieldsMap; + this(builder.fieldsMap); } private PostFieldsV4(Map fields) { - this.fieldsMap = fields; + validateFields(fields); + this.fieldsMap = Collections.unmodifiableMap(fields); } + /** + * Constructs {@code PostPolicyV4.PostFieldsV4} object of the given field map. + * + * @param fields a map of the HTML form fields + * @return constructed object + * @throws IllegalArgumentException if an unsupported field is specified + */ public static PostFieldsV4 of(Map fields) { return new PostFieldsV4(fields); } @@ -81,10 +150,11 @@ public Map getFieldsMap() { } public static class Builder { - private Map fieldsMap; + private static final String CUSTOM_FIELD_PREFIX = "x-goog-meta-"; + private final Map fieldsMap; private Builder() { - fieldsMap = new HashMap<>(); + this.fieldsMap = new HashMap<>(); } public PostFieldsV4 build() { @@ -111,8 +181,14 @@ public Builder setContentEncoding(String contentEncoding) { return this; } + /** + * @deprecated Invocation of this method has no effect, because all valid HTML form fields + * except Content-Length can use exact matching. Use {@link + * PostPolicyV4.PostConditionsV4.Builder#addContentLengthRangeCondition(int, int)} to + * specify a range for the content-length. + */ + @Deprecated public Builder setContentLength(int contentLength) { - fieldsMap.put("content-length", "" + contentLength); return this; } @@ -121,7 +197,13 @@ public Builder setContentType(String contentType) { return this; } + /** @deprecated Use {@link #setExpires(String)}. */ + @Deprecated public Builder Expires(String expires) { + return setExpires(expires); + } + + public Builder setExpires(String expires) { fieldsMap.put("expires", expires); return this; } @@ -136,15 +218,26 @@ public Builder setSuccessActionStatus(int successActionStatus) { return this; } + /** @deprecated Use {@link #setCustomMetadataField(String, String)}. */ + @Deprecated public Builder AddCustomMetadataField(String field, String value) { - fieldsMap.put("x-goog-meta-" + field, value); + return setCustomMetadataField(field, value); + } + + public Builder setCustomMetadataField(String field, String value) { + if (!field.startsWith(CUSTOM_FIELD_PREFIX)) { + field = CUSTOM_FIELD_PREFIX + field; + } + fieldsMap.put(field, value); return this; } } } /** - * Class for specifying conditions in a V4 POST Policy document. + * A helper class for specifying conditions in a V4 POST Policy document. Used in: {@link + * Storage#generateSignedPostPolicyV4(BlobInfo, long, TimeUnit, PostPolicyV4.PostFieldsV4, + * PostPolicyV4.PostConditionsV4, Storage.PostPolicyV4Option...)}. * * @see * Policy document @@ -167,14 +260,14 @@ public static Builder newBuilder() { } public Set getConditions() { - return conditions; + return Collections.unmodifiableSet(conditions); } public static class Builder { - Set conditions; + private final Set conditions; private Builder() { - this.conditions = new LinkedHashSet<>(); + this(new LinkedHashSet()); } private Builder(Set conditions) { @@ -190,64 +283,93 @@ public PostConditionsV4 build() { } public Builder addAclCondition(ConditionV4Type type, String acl) { + checkType(type, "acl"); conditions.add(new ConditionV4(type, "acl", acl)); return this; } public Builder addBucketCondition(ConditionV4Type type, String bucket) { + checkType(type, "bucket"); conditions.add(new ConditionV4(type, "bucket", bucket)); return this; } public Builder addCacheControlCondition(ConditionV4Type type, String cacheControl) { + checkType(type, "cache-control"); conditions.add(new ConditionV4(type, "cache-control", cacheControl)); return this; } public Builder addContentDispositionCondition( ConditionV4Type type, String contentDisposition) { + checkType(type, "content-disposition"); conditions.add(new ConditionV4(type, "content-disposition", contentDisposition)); return this; } public Builder addContentEncodingCondition(ConditionV4Type type, String contentEncoding) { + checkType(type, "content-encoding"); conditions.add(new ConditionV4(type, "content-encoding", contentEncoding)); return this; } + /** + * @deprecated Invocation of this method has no effect. Use {@link + * #addContentLengthRangeCondition(int, int)} to specify a range for the content-length. + */ public Builder addContentLengthCondition(ConditionV4Type type, int contentLength) { - conditions.add(new ConditionV4(type, "content-length", "" + contentLength)); return this; } public Builder addContentTypeCondition(ConditionV4Type type, String contentType) { + checkType(type, "content-type"); conditions.add(new ConditionV4(type, "content-type", contentType)); return this; } + /** @deprecated Use {@link #addExpiresCondition(long)} */ + @Deprecated public Builder addExpiresCondition(ConditionV4Type type, long expires) { - conditions.add(new ConditionV4(type, "expires", dateFormat.format(expires))); - return this; + return addExpiresCondition(expires); } + /** @deprecated Use {@link #addExpiresCondition(String)} */ + @Deprecated public Builder addExpiresCondition(ConditionV4Type type, String expires) { - conditions.add(new ConditionV4(type, "expires", expires)); + return addExpiresCondition(expires); + } + + public Builder addExpiresCondition(long expires) { + return addExpiresCondition(dateFormat.format(expires)); + } + + public Builder addExpiresCondition(String expires) { + conditions.add(new ConditionV4(ConditionV4Type.MATCHES, "expires", expires)); return this; } public Builder addKeyCondition(ConditionV4Type type, String key) { + checkType(type, "key"); conditions.add(new ConditionV4(type, "key", key)); return this; } public Builder addSuccessActionRedirectUrlCondition( ConditionV4Type type, String successActionRedirectUrl) { + checkType(type, "success_action_redirect"); conditions.add(new ConditionV4(type, "success_action_redirect", successActionRedirectUrl)); return this; } + /** @deprecated Use {@link #addSuccessActionStatusCondition(int)} */ + @Deprecated public Builder addSuccessActionStatusCondition(ConditionV4Type type, int status) { - conditions.add(new ConditionV4(type, "success_action_status", "" + status)); + return addSuccessActionStatusCondition(status); + } + + public Builder addSuccessActionStatusCondition(int status) { + conditions.add( + new ConditionV4(ConditionV4Type.MATCHES, "success_action_status", "" + status)); return this; } @@ -260,18 +382,24 @@ Builder addCustomCondition(ConditionV4Type type, String field, String value) { conditions.add(new ConditionV4(type, field, value)); return this; } + + private void checkType(ConditionV4Type type, String field) { + if (type != ConditionV4Type.MATCHES && type != ConditionV4Type.STARTS_WITH) { + throw new IllegalArgumentException("Field " + field + " can't use " + type); + } + } } } /** - * Class for a V4 POST Policy document. + * Class for a V4 POST Policy document. Used by Storage to construct {@code PostPolicyV4} objects. * * @see * Policy document */ public static final class PostPolicyV4Document { - private String expiration; - private PostConditionsV4 conditions; + private final String expiration; + private final PostConditionsV4 conditions; private PostPolicyV4Document(String expiration, PostConditionsV4 conditions) { this.expiration = expiration; @@ -351,9 +479,20 @@ public String toJson() { } public enum ConditionV4Type { - MATCHES, - STARTS_WITH, - CONTENT_LENGTH_RANGE + MATCHES("eq"), + STARTS_WITH("starts-with"), + CONTENT_LENGTH_RANGE("content-length-range"); + + private final String name; + + ConditionV4Type(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } } /** @@ -362,12 +501,12 @@ public enum ConditionV4Type { * @see * Policy document */ - static final class ConditionV4 { - ConditionV4Type type; - String operand1; - String operand2; + public static final class ConditionV4 { + public final ConditionV4Type type; + public final String operand1; + public final String operand2; - private ConditionV4(ConditionV4Type type, String operand1, String operand2) { + ConditionV4(ConditionV4Type type, String operand1, String operand2) { this.type = type; this.operand1 = operand1; this.operand2 = operand2; @@ -385,5 +524,18 @@ public boolean equals(Object other) { public int hashCode() { return Objects.hash(type, operand1, operand2); } + + /** + * Examples of returned strings: {@code ["eq", "$key", "test-object"]}, {@code ["starts-with", + * "$acl", "public"]}, {@code ["content-length-range", 246, 266]}. + */ + @Override + public String toString() { + String body = + type == ConditionV4Type.CONTENT_LENGTH_RANGE + ? operand1 + ", " + operand2 + : "\"$" + operand1 + "\", \"" + operand2 + "\""; + return "[\"" + type + "\", " + body + "]"; + } } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java index eb15dd37a..6b8fc4990 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java @@ -521,6 +521,15 @@ public static BlobTargetOption disableGzipContent() { return new BlobTargetOption(StorageRpc.Option.IF_DISABLE_GZIP_CONTENT, true); } + /** + * Returns an option for detecting content type. If this option is used, the content type is + * detected from the blob name if not explicitly set. This option is on the client side only, it + * does not appear in a RPC call. + */ + public static BlobTargetOption detectContentType() { + return new BlobTargetOption(StorageRpc.Option.DETECT_CONTENT_TYPE, true); + } + /** * Returns an option to set a customer-supplied AES256 key for server-side encryption of the * blob. @@ -593,6 +602,7 @@ enum Option { CUSTOMER_SUPPLIED_KEY, KMS_KEY_NAME, USER_PROJECT, + DETECT_CONTENT_TYPE, IF_DISABLE_GZIP_CONTENT; StorageRpc.Option toRpcOption() { @@ -733,6 +743,15 @@ public static BlobWriteOption userProject(String userProject) { public static BlobWriteOption disableGzipContent() { return new BlobWriteOption(Option.IF_DISABLE_GZIP_CONTENT, true); } + + /** + * Returns an option for detecting content type. If this option is used, the content type is + * detected from the blob name if not explicitly set. This option is on the client side only, it + * does not appear in a RPC call. + */ + public static BlobWriteOption detectContentType() { + return new BlobWriteOption(Option.DETECT_CONTENT_TYPE, true); + } } /** Class for specifying blob source options. */ @@ -1032,6 +1051,28 @@ public static BlobListOption delimiter(String delimiter) { return new BlobListOption(StorageRpc.Option.DELIMITER, delimiter); } + /** + * Returns an option to set a startOffset to filter results to objects whose names are + * lexicographically equal to or after startOffset. If endOffset is also set, the objects listed + * have names between startOffset (inclusive) and endOffset (exclusive). + * + * @param startOffset startOffset to filter the results + */ + public static BlobListOption startOffset(String startOffset) { + return new BlobListOption(StorageRpc.Option.START_OFF_SET, startOffset); + } + + /** + * Returns an option to set a endOffset to filter results to objects whose names are + * lexicographically before endOffset. If startOffset is also set, the objects listed have names + * between startOffset (inclusive) and endOffset (exclusive). + * + * @param endOffset endOffset to filter the results + */ + public static BlobListOption endOffset(String endOffset) { + return new BlobListOption(StorageRpc.Option.END_OFF_SET, endOffset); + } + /** * Returns an option to define the billing user project. This option is required by buckets with * `requester_pays` flag enabled to assign operation costs. @@ -1832,9 +1873,10 @@ public static Builder newBuilder() { * Creates a new blob. Direct upload is used to upload {@code content}. For large content, {@link * #writer} is recommended as it uses resumable upload. MD5 and CRC32C hashes of {@code content} * are computed and used for validating transferred data. Accepts an optional userProject {@link - * BlobGetOption} option which defines the project id to assign operational costs. + * BlobGetOption} option which defines the project id to assign operational costs. The content + * type is detected from the blob name if not explicitly set. * - *

Example of creating a blob from a byte array. + *

Example of creating a blob from a byte array: * *

{@code
    * String bucketName = "my-unique-bucket";
@@ -1857,7 +1899,7 @@ public static Builder newBuilder() {
    * Accepts a userProject {@link BlobGetOption} option, which defines the project id to assign
    * operational costs.
    *
-   * 

Example of creating a blob from a byte array. + *

Example of creating a blob from a byte array: * *

{@code
    * String bucketName = "my-unique-bucket";
@@ -1876,7 +1918,7 @@ Blob create(
 
   /**
    * Creates a new blob. Direct upload is used to upload {@code content}. For large content, {@link
-   * #writer} is recommended as it uses resumable upload. By default any md5 and crc32c values in
+   * #writer} is recommended as it uses resumable upload. By default any MD5 and CRC32C values in
    * the given {@code blobInfo} are ignored unless requested via the {@code
    * BlobWriteOption.md5Match} and {@code BlobWriteOption.crc32cMatch} options. The given input
    * stream is closed upon success.
@@ -2603,11 +2645,11 @@ Blob createFrom(
   ReadChannel reader(BlobId blob, BlobSourceOption... options);
 
   /**
-   * Creates a blob and return a channel for writing its content. By default any md5 and crc32c
+   * Creates a blob and returns a channel for writing its content. By default any MD5 and CRC32C
    * values in the given {@code blobInfo} are ignored unless requested via the {@code
    * BlobWriteOption.md5Match} and {@code BlobWriteOption.crc32cMatch} options.
    *
-   * 

Example of writing a blob's content through a writer. + *

Example of writing a blob's content through a writer: * *

{@code
    * String bucketName = "my-unique-bucket";
@@ -2801,6 +2843,8 @@ Blob createFrom(
    * }
* * @param blobInfo the blob uploaded in the form + * @param duration time before expiration + * @param unit duration time unit * @param fields the fields specified in the form * @param conditions which conditions every upload must satisfy * @param duration how long until the form expires, in milliseconds @@ -2819,9 +2863,8 @@ PostPolicyV4 generateSignedPostPolicyV4( /** * Generates a presigned post policy without any conditions. Automatically creates required - * conditions. See full documentation for generateSignedPostPolicyV4( BlobInfo blobInfo, long - * duration, TimeUnit unit, PostFieldsV4 fields, PostConditionsV4 conditions, - * PostPolicyV4Option... options) above. + * conditions. See full documentation for {@link #generateSignedPostPolicyV4(BlobInfo, long, + * TimeUnit, PostPolicyV4.PostFieldsV4, PostPolicyV4.PostConditionsV4, PostPolicyV4Option...)}. */ PostPolicyV4 generateSignedPostPolicyV4( BlobInfo blobInfo, @@ -2832,9 +2875,8 @@ PostPolicyV4 generateSignedPostPolicyV4( /** * Generates a presigned post policy without any fields. Automatically creates required fields. - * See full documentation for generateSignedPostPolicyV4( BlobInfo blobInfo, long duration, - * TimeUnit unit, PostFieldsV4 fields, PostConditionsV4 conditions, PostPolicyV4Option... options) - * above. + * See full documentation for {@link #generateSignedPostPolicyV4(BlobInfo, long, TimeUnit, + * PostPolicyV4.PostFieldsV4, PostPolicyV4.PostConditionsV4, PostPolicyV4Option...)}. */ PostPolicyV4 generateSignedPostPolicyV4( BlobInfo blobInfo, @@ -2845,9 +2887,9 @@ PostPolicyV4 generateSignedPostPolicyV4( /** * Generates a presigned post policy without any fields or conditions. Automatically creates - * required fields and conditions. See full documentation for generateSignedPostPolicyV4( BlobInfo - * blobInfo, long duration, TimeUnit unit, PostFieldsV4 fields, PostConditionsV4 conditions, - * PostPolicyV4Option... options) above. + * required fields and conditions. See full documentation for {@link + * #generateSignedPostPolicyV4(BlobInfo, long, TimeUnit, PostPolicyV4.PostFieldsV4, + * PostPolicyV4.PostConditionsV4, PostPolicyV4Option...)}. */ PostPolicyV4 generateSignedPostPolicyV4( BlobInfo blobInfo, long duration, TimeUnit unit, PostPolicyV4Option... options); @@ -3517,7 +3559,7 @@ HmacKeyMetadata updateHmacKeyState( * String bucketName = "my-unique-bucket"; * List response = * storage.testIamPermissions( - * bucket, + * bucketName, * ImmutableList.of("storage.buckets.get", "storage.buckets.getIamPolicy")); * for (boolean hasPermission : response) { * // Do something with permission test response diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java index 519a9e9fa..d7b7baa0b 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java @@ -77,9 +77,12 @@ import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; +import java.net.FileNameMap; +import java.net.URLConnection; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; public class HttpStorageRpc implements StorageRpc { @@ -98,6 +101,7 @@ public class HttpStorageRpc implements StorageRpc { private final HttpRequestInitializer batchRequestInitializer; private static final long MEGABYTE = 1024L * 1024L; + private static final FileNameMap FILE_NAME_MAP = URLConnection.getFileNameMap(); public HttpStorageRpc(StorageOptions options) { HttpTransportOptions transportOptions = (HttpTransportOptions) options.getTransportOptions(); @@ -286,7 +290,7 @@ public StorageObject create( .insert( storageObject.getBucket(), storageObject, - new InputStreamContent(storageObject.getContentType(), content)); + new InputStreamContent(detectContentType(storageObject, options), content)); insert.getMediaHttpUploader().setDirectUploadEnabled(true); Boolean disableGzipContent = Option.IF_DISABLE_GZIP_CONTENT.getBoolean(options); if (disableGzipContent != null) { @@ -350,6 +354,8 @@ public Tuple> list(final String bucket, Map> list(final String bucket, Map options) { + String contentType = object.getContentType(); + if (contentType != null) { + return contentType; + } + + if (Boolean.TRUE == Option.DETECT_CONTENT_TYPE.get(options)) { + contentType = FILE_NAME_MAP.getContentTypeFor(object.getName().toLowerCase(Locale.ENGLISH)); + } + + return firstNonNull(contentType, "application/octet-stream"); + } + private static Function objectFromPrefix(final String bucket) { return new Function() { @Override @@ -834,9 +853,7 @@ public String open(StorageObject object, Map options) { HttpRequest httpRequest = requestFactory.buildPostRequest(url, new JsonHttpContent(jsonFactory, object)); HttpHeaders requestHeaders = httpRequest.getHeaders(); - requestHeaders.set( - "X-Upload-Content-Type", - firstNonNull(object.getContentType(), "application/octet-stream")); + requestHeaders.set("X-Upload-Content-Type", detectContentType(object, options)); String key = Option.CUSTOMER_SUPPLIED_KEY.getString(options); if (key != null) { BaseEncoding base64 = BaseEncoding.base64(); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java index 7ae9c8ec1..2088c15be 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java @@ -58,6 +58,8 @@ enum Option { MAX_RESULTS("maxResults"), PAGE_TOKEN("pageToken"), DELIMITER("delimiter"), + START_OFF_SET("startOffset"), + END_OFF_SET("endOffset"), VERSIONS("versions"), FIELDS("fields"), CUSTOMER_SUPPLIED_KEY("customerSuppliedKey"), @@ -65,7 +67,8 @@ enum Option { KMS_KEY_NAME("kmsKeyName"), SERVICE_ACCOUNT_EMAIL("serviceAccount"), SHOW_DELETED_KEYS("showDeletedKeys"), - REQUESTED_POLICY_VERSION("optionsRequestedPolicyVersion"); + REQUESTED_POLICY_VERSION("optionsRequestedPolicyVersion"), + DETECT_CONTENT_TYPE("detectContentType"); private final String value; diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java index ea4f6dbda..d4119971f 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java @@ -67,6 +67,7 @@ public class BlobInfoTest { private static final Long SIZE = 1024L; private static final Long UPDATE_TIME = DELETE_TIME - 1L; private static final Long CREATE_TIME = UPDATE_TIME - 1L; + private static final Long CUSTOM_TIME = CREATE_TIME - 1L; private static final String ENCRYPTION_ALGORITHM = "AES256"; private static final String KEY_SHA256 = "keySha"; private static final CustomerEncryption CUSTOMER_ENCRYPTION = @@ -101,6 +102,7 @@ public class BlobInfoTest { .setSize(SIZE) .setUpdateTime(UPDATE_TIME) .setCreateTime(CREATE_TIME) + .setCustomTime(CUSTOM_TIME) .setStorageClass(STORAGE_CLASS) .setKmsKeyName(KMS_KEY_NAME) .setEventBasedHold(EVENT_BASED_HOLD) @@ -195,6 +197,7 @@ public void testBuilder() { assertEquals(SIZE, BLOB_INFO.getSize()); assertEquals(UPDATE_TIME, BLOB_INFO.getUpdateTime()); assertEquals(CREATE_TIME, BLOB_INFO.getCreateTime()); + assertEquals(CUSTOM_TIME, BLOB_INFO.getCustomTime()); assertEquals(STORAGE_CLASS, BLOB_INFO.getStorageClass()); assertEquals(KMS_KEY_NAME, BLOB_INFO.getKmsKeyName()); assertEquals(EVENT_BASED_HOLD, BLOB_INFO.getEventBasedHold()); @@ -214,6 +217,7 @@ public void testBuilder() { assertNull(DIRECTORY_INFO.getCrc32c()); assertNull(DIRECTORY_INFO.getCrc32cToHexString()); assertNull(DIRECTORY_INFO.getCreateTime()); + assertNull(DIRECTORY_INFO.getCustomTime()); assertNull(DIRECTORY_INFO.getDeleteTime()); assertNull(DIRECTORY_INFO.getEtag()); assertNull(DIRECTORY_INFO.getGeneration()); @@ -257,6 +261,7 @@ private void compareBlobs(BlobInfo expected, BlobInfo value) { assertEquals(expected.getOwner(), value.getOwner()); assertEquals(expected.getSelfLink(), value.getSelfLink()); assertEquals(expected.getSize(), value.getSize()); + assertEquals(expected.getCustomTime(), value.getCustomTime()); assertEquals(expected.getUpdateTime(), value.getUpdateTime()); assertEquals(expected.getStorageClass(), value.getStorageClass()); assertEquals(expected.getKmsKeyName(), value.getKmsKeyName()); @@ -299,6 +304,7 @@ public void testToPbAndFromPb() { assertNull(blobInfo.getCrc32c()); assertNull(blobInfo.getCrc32cToHexString()); assertNull(blobInfo.getCreateTime()); + assertNull(blobInfo.getCustomTime()); assertNull(blobInfo.getDeleteTime()); assertNull(blobInfo.getEtag()); assertNull(blobInfo.getGeneration()); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java index 7ff384fb9..2ff21b712 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java @@ -91,6 +91,7 @@ public class BlobTest { private static final Long SIZE = 1024L; private static final Long UPDATE_TIME = DELETE_TIME - 1L; private static final Long CREATE_TIME = UPDATE_TIME - 1L; + private static final Long CUSTOM_TIME = CREATE_TIME - 1L; private static final String ENCRYPTION_ALGORITHM = "AES256"; private static final String KEY_SHA256 = "keySha"; private static final BlobInfo.CustomerEncryption CUSTOMER_ENCRYPTION = @@ -122,6 +123,7 @@ public class BlobTest { .setSize(SIZE) .setUpdateTime(UPDATE_TIME) .setCreateTime(CREATE_TIME) + .setCustomTime(CUSTOM_TIME) .setCustomerEncryption(CUSTOMER_ENCRYPTION) .setKmsKeyName(KMS_KEY_NAME) .setEventBasedHold(EVENT_BASED_HOLD) @@ -510,6 +512,7 @@ public void testBuilder() { .setContentLanguage(CONTENT_LANGUAGE) .setCrc32c(CRC32) .setCreateTime(CREATE_TIME) + .setCustomTime(CUSTOM_TIME) .setCustomerEncryption(CUSTOMER_ENCRYPTION) .setKmsKeyName(KMS_KEY_NAME) .setEventBasedHold(EVENT_BASED_HOLD) @@ -539,6 +542,7 @@ public void testBuilder() { assertEquals(CRC32, blob.getCrc32c()); assertEquals(CRC32_HEX_STRING, blob.getCrc32cToHexString()); assertEquals(CREATE_TIME, blob.getCreateTime()); + assertEquals(CUSTOM_TIME, blob.getCustomTime()); assertEquals(CUSTOMER_ENCRYPTION, blob.getCustomerEncryption()); assertEquals(KMS_KEY_NAME, blob.getKmsKeyName()); assertEquals(EVENT_BASED_HOLD, blob.getEventBasedHold()); @@ -589,6 +593,7 @@ public void testBuilder() { assertNull(blob.getSelfLink()); assertEquals(0L, (long) blob.getSize()); assertNull(blob.getUpdateTime()); + assertNull(blob.getCustomTime()); assertTrue(blob.isDirectory()); } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketInfoTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketInfoTest.java index b10839426..8def7c306 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketInfoTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketInfoTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import com.google.api.client.util.DateTime; import com.google.api.services.storage.model.Bucket; import com.google.api.services.storage.model.Bucket.Lifecycle.Rule; import com.google.cloud.storage.Acl.Project; @@ -56,6 +57,7 @@ public class BucketInfoTest { private static final User OWNER = new User("user@gmail.com"); private static final String SELF_LINK = "http://storage/b/n"; private static final Long CREATE_TIME = System.currentTimeMillis(); + private static final Long UPDATE_TIME = CREATE_TIME; private static final List CORS = Collections.singletonList(Cors.newBuilder().build()); private static final List DEFAULT_ACL = Collections.singletonList(Acl.of(User.ofAllAuthenticatedUsers(), Role.WRITER)); @@ -117,6 +119,7 @@ public class BucketInfoTest { .setSelfLink(SELF_LINK) .setCors(CORS) .setCreateTime(CREATE_TIME) + .setUpdateTime(UPDATE_TIME) .setDefaultAcl(DEFAULT_ACL) .setDeleteRules(DELETE_RULES) .setLifecycleRules(LIFECYCLE_RULES) @@ -148,6 +151,7 @@ public class BucketInfoTest { .setSelfLink(SELF_LINK) .setCors(CORS) .setCreateTime(CREATE_TIME) + .setUpdateTime(UPDATE_TIME) .setDefaultAcl(DEFAULT_ACL) .setDeleteRules(DELETE_RULES) .setLifecycleRules(LIFECYCLE_RULES) @@ -202,6 +206,7 @@ public void testBuilder() { assertEquals(OWNER, BUCKET_INFO.getOwner()); assertEquals(SELF_LINK, BUCKET_INFO.getSelfLink()); assertEquals(CREATE_TIME, BUCKET_INFO.getCreateTime()); + assertEquals(UPDATE_TIME, BUCKET_INFO.getUpdateTime()); assertEquals(CORS, BUCKET_INFO.getCors()); assertEquals(DEFAULT_ACL, BUCKET_INFO.getDefaultAcl()); assertEquals(DELETE_RULES, BUCKET_INFO.getDeleteRules()); @@ -223,6 +228,7 @@ public void testBuilder() { } @Test + @SuppressWarnings({"unchecked", "deprecation"}) public void testToPbAndFromPb() { compareBuckets(BUCKET_INFO, BucketInfo.fromPb(BUCKET_INFO.toPb())); BucketInfo bucketInfo = @@ -245,6 +251,7 @@ private void compareBuckets(BucketInfo expected, BucketInfo value) { assertEquals(expected.getOwner(), value.getOwner()); assertEquals(expected.getSelfLink(), value.getSelfLink()); assertEquals(expected.getCreateTime(), value.getCreateTime()); + assertEquals(expected.getUpdateTime(), value.getUpdateTime()); assertEquals(expected.getCors(), value.getCors()); assertEquals(expected.getDefaultAcl(), value.getDefaultAcl()); assertEquals(expected.getDeleteRules(), value.getDeleteRules()); @@ -322,6 +329,27 @@ public void testLifecycleRules() { setStorageClassLifecycleRule.getAction().getStorageClass()); assertTrue(setStorageClassLifecycleRule.getCondition().getIsLive()); assertEquals(10, setStorageClassLifecycleRule.getCondition().getNumNewerVersions().intValue()); + + Rule lifecycleRule = + new LifecycleRule( + LifecycleAction.newSetStorageClassAction(StorageClass.COLDLINE), + LifecycleCondition.newBuilder() + .setIsLive(true) + .setNumberOfNewerVersions(10) + .setDaysSinceNoncurrentTime(30) + .setNoncurrentTimeBefore(new DateTime(System.currentTimeMillis())) + .setCustomTimeBefore(new DateTime(System.currentTimeMillis())) + .setDaysSinceCustomTime(30) + .build()) + .toPb(); + assertEquals(StorageClass.COLDLINE.toString(), lifecycleRule.getAction().getStorageClass()); + assertTrue(lifecycleRule.getCondition().getIsLive()); + assertEquals(10, lifecycleRule.getCondition().getNumNewerVersions().intValue()); + assertEquals(30, lifecycleRule.getCondition().getDaysSinceNoncurrentTime().intValue()); + assertNotNull(lifecycleRule.getCondition().getNoncurrentTimeBefore()); + assertEquals(StorageClass.COLDLINE.toString(), lifecycleRule.getAction().getStorageClass()); + assertEquals(30, lifecycleRule.getCondition().getDaysSinceCustomTime().intValue()); + assertNotNull(lifecycleRule.getCondition().getCustomTimeBefore()); } @Test diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketTest.java index 94050ffef..e8a8e3f46 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketTest.java @@ -69,6 +69,7 @@ public class BucketTest { private static final User OWNER = new User("user@gmail.com"); private static final String SELF_LINK = "http://storage/b/n"; private static final Long CREATE_TIME = System.currentTimeMillis(); + private static final Long UPDATE_TIME = CREATE_TIME - 1L; private static final List CORS = Collections.singletonList(Cors.newBuilder().build()); private static final List DEFAULT_ACL = Collections.singletonList(Acl.of(User.ofAllAuthenticatedUsers(), WRITER)); @@ -111,6 +112,7 @@ public class BucketTest { .setSelfLink(SELF_LINK) .setCors(CORS) .setCreateTime(CREATE_TIME) + .setUpdateTime(UPDATE_TIME) .setDefaultAcl(DEFAULT_ACL) .setDeleteRules(DELETE_RULES) .setLifecycleRules(LIFECYCLE_RULES) @@ -803,6 +805,7 @@ public void testBuilder() { .setSelfLink(SELF_LINK) .setCors(CORS) .setCreateTime(CREATE_TIME) + .setUpdateTime(UPDATE_TIME) .setDefaultAcl(DEFAULT_ACL) .setDeleteRules(DELETE_RULES) .setLifecycleRules(LIFECYCLE_RULES) @@ -828,6 +831,7 @@ public void testBuilder() { assertEquals(OWNER, bucket.getOwner()); assertEquals(SELF_LINK, bucket.getSelfLink()); assertEquals(CREATE_TIME, bucket.getCreateTime()); + assertEquals(UPDATE_TIME, bucket.getUpdateTime()); assertEquals(CORS, bucket.getCors()); assertEquals(DEFAULT_ACL, bucket.getDefaultAcl()); assertEquals(DELETE_RULES, bucket.getDeleteRules()); @@ -886,4 +890,36 @@ public void testUpdateBucketLogging() { assertThat(actualUpdatedBucket.getLogging().getLogBucket()).isNull(); assertThat(actualUpdatedBucket.getLogging().getLogObjectPrefix()).isNull(); } + + @Test + public void testRemoveBucketCORS() { + initializeExpectedBucket(6); + List origins = ImmutableList.of(Cors.Origin.of("http://cloud.google.com")); + List httpMethods = ImmutableList.of(HttpMethod.GET); + List responseHeaders = ImmutableList.of("Content-Type"); + Cors cors = + Cors.newBuilder() + .setOrigins(origins) + .setMethods(httpMethods) + .setResponseHeaders(responseHeaders) + .setMaxAgeSeconds(100) + .build(); + BucketInfo bucketInfo = BucketInfo.newBuilder("b").setCors(ImmutableList.of(cors)).build(); + Bucket bucket = new Bucket(serviceMockReturnsOptions, new BucketInfo.BuilderImpl(bucketInfo)); + assertThat(bucket.getCors()).isNotNull(); + assertThat(bucket.getCors().get(0).getMaxAgeSeconds()).isEqualTo(100); + assertThat(bucket.getCors().get(0).getMethods()).isEqualTo(httpMethods); + assertThat(bucket.getCors().get(0).getOrigins()).isEqualTo(origins); + assertThat(bucket.getCors().get(0).getResponseHeaders()).isEqualTo(responseHeaders); + + // Remove bucket CORS configuration. + Bucket expectedUpdatedBucket = bucket.toBuilder().setCors(null).build(); + expect(storage.getOptions()).andReturn(mockOptions).times(2); + expect(storage.update(expectedUpdatedBucket)).andReturn(expectedUpdatedBucket); + replay(storage); + initializeBucket(); + Bucket updatedBucket = new Bucket(storage, new BucketInfo.BuilderImpl(expectedUpdatedBucket)); + Bucket actualUpdatedBucket = updatedBucket.update(); + assertThat(actualUpdatedBucket.getCors()).isEmpty(); + } } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/PostPolicyV4Test.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/PostPolicyV4Test.java new file mode 100644 index 000000000..8213f6093 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/PostPolicyV4Test.java @@ -0,0 +1,592 @@ +/* + * Copyright 2020 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 + * + * 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.storage; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import org.junit.Test; + +public class PostPolicyV4Test { + private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + private void assertNotSameButEqual(Map expected, Map returned) { + assertNotSame(expected, returned); + assertEquals("map sizes", expected.size(), returned.size()); + for (String key : expected.keySet()) { + assertEquals("value of $" + key, expected.get(key), returned.get(key)); + } + } + + private static final String[] VALID_FIELDS = { + "acl", + "bucket", + "cache-control", + "content-disposition", + "content-encoding", + "content-type", + "expires", + "file", + "key", + "policy", + "success_action_redirect", + "success_action_status", + "x-goog-algorithm", + "x-goog-credential", + "x-goog-date", + "x-goog-signature", + }; + + private static final String CUSTOM_PREFIX = "x-goog-meta-"; + + private static Map initAllFields() { + Map fields = new HashMap<>(); + for (String key : VALID_FIELDS) { + fields.put(key, "value of " + key); + } + fields.put(CUSTOM_PREFIX + "custom", "value of custom field"); + return Collections.unmodifiableMap(fields); + } + + private static final Map ALL_FIELDS = initAllFields(); + + @Test + public void testPostPolicyV4_of() { + String url = "http://example.com"; + PostPolicyV4 policy = PostPolicyV4.of(url, ALL_FIELDS); + assertEquals(url, policy.getUrl()); + assertNotSameButEqual(ALL_FIELDS, policy.getFields()); + } + + @Test + public void testPostPolicyV4_ofMalformedURL() { + try { + PostPolicyV4.of("example.com", new HashMap()); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("example.com is not an absolute URL", e.getMessage()); + } + + try { + PostPolicyV4.of("Scio nescio", new HashMap()); + fail(); + } catch (IllegalArgumentException e) { + assertEquals( + "java.net.URISyntaxException: Illegal character in path at index 4: Scio nescio", + e.getMessage()); + } + } + + @Test + public void testPostPolicyV4_ofInvalidField() { + Map fields = new HashMap<>(ALL_FIELDS); + fields.put("$file", "file.txt"); + try { + PostPolicyV4.of("http://google.com", fields); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Invalid key: $file", e.getMessage()); + } + } + + @Test + public void testPostFieldsV4_of() { + PostPolicyV4.PostFieldsV4 fields = PostPolicyV4.PostFieldsV4.of(ALL_FIELDS); + assertNotSameButEqual(ALL_FIELDS, fields.getFieldsMap()); + } + + @Test + public void testPostFieldsV4_ofInvalidField() { + Map map = new HashMap<>(); + map.put("$file", "file.txt"); + try { + PostPolicyV4.PostFieldsV4.of(map); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Invalid key: $file", e.getMessage()); + } + } + + @Test + public void testPostPolicyV4_builder() { + PostPolicyV4.PostFieldsV4.Builder builder = PostPolicyV4.PostFieldsV4.newBuilder(); + builder.setAcl("acl"); + builder.setCacheControl("cache-control"); + builder.setContentDisposition("content-disposition"); + builder.setContentType("content-type"); + builder.setExpires("expires"); + builder.setSuccessActionRedirect("success_action_redirect"); + Map map = builder.build().getFieldsMap(); + assertEquals("map size", 6, map.size()); + for (String key : map.keySet()) { + assertEquals("value of $" + key, key, map.get(key)); + } + + Map expectedUpdated = new HashMap<>(map); + builder.setCustomMetadataField("xxx", "XXX"); + builder.setCustomMetadataField(CUSTOM_PREFIX + "yyy", "YYY"); + builder.setAcl(null); + builder.setContentType("new-content-type"); + builder.setSuccessActionStatus(42); + expectedUpdated.put(CUSTOM_PREFIX + "xxx", "XXX"); + expectedUpdated.put(CUSTOM_PREFIX + "yyy", "YYY"); + expectedUpdated.put("acl", null); + expectedUpdated.put("content-type", "new-content-type"); + expectedUpdated.put("success_action_status", "42"); + Map updated = builder.build().getFieldsMap(); + assertNotSameButEqual(expectedUpdated, updated); + } + + @Test + public void testPostPolicyV4_setContentLength() { + PostPolicyV4.PostFieldsV4.Builder builder = PostPolicyV4.PostFieldsV4.newBuilder(); + builder.setContentLength(12345); + assertTrue(builder.build().getFieldsMap().isEmpty()); + } + + @Test + public void testPostConditionsV4_builder() { + PostPolicyV4.PostConditionsV4.Builder builder = PostPolicyV4.PostConditionsV4.newBuilder(); + assertTrue(builder.build().getConditions().isEmpty()); + + builder.addAclCondition(PostPolicyV4.ConditionV4Type.STARTS_WITH, "public"); + builder.addBucketCondition(PostPolicyV4.ConditionV4Type.MATCHES, "travel-maps"); + builder.addContentLengthRangeCondition(0, 100000); + + PostPolicyV4.PostConditionsV4 postConditionsV4 = builder.build(); + Set conditions = postConditionsV4.getConditions(); + assertEquals(3, conditions.size()); + + try { + conditions.clear(); + fail(); + } catch (UnsupportedOperationException e) { + // expected + } + + PostPolicyV4.PostConditionsV4 postConditionsV4Extended = + postConditionsV4 + .toBuilder() + .addCustomCondition(PostPolicyV4.ConditionV4Type.STARTS_WITH, "key", "") + .build(); + assertEquals(4, postConditionsV4Extended.getConditions().size()); + } + + interface ConditionTest { + /** + * Calls one of addCondition method on the given builder and returns expected ConditionV4 + * object. + */ + PostPolicyV4.ConditionV4 addCondition(PostPolicyV4.PostConditionsV4.Builder builder); + } + + @Test + public void testPostConditionsV4_addCondition() { + // shortcuts + final PostPolicyV4.ConditionV4Type eq = PostPolicyV4.ConditionV4Type.MATCHES; + final PostPolicyV4.ConditionV4Type startsWith = PostPolicyV4.ConditionV4Type.STARTS_WITH; + final PostPolicyV4.ConditionV4Type range = PostPolicyV4.ConditionV4Type.CONTENT_LENGTH_RANGE; + + ConditionTest[] cases = { + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addContentLengthRangeCondition(123, 456); + return new PostPolicyV4.ConditionV4(range, "123", "456"); + } + + @Override + public String toString() { + return "addContentLengthRangeCondition()"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + long date = 2000000000000L; + builder.addExpiresCondition(date); + return new PostPolicyV4.ConditionV4(eq, "expires", dateFormat.format(date)); + } + + @Override + public String toString() { + return "addExpiresCondition(long)"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addExpiresCondition("2030-Dec-31"); + return new PostPolicyV4.ConditionV4(eq, "expires", "2030-Dec-31"); + } + + @Override + public String toString() { + return "addExpiresCondition(String)"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addExpiresCondition(range, 0); + return new PostPolicyV4.ConditionV4(eq, "expires", dateFormat.format(0)); + } + + @Override + public String toString() { + return "@deprecated addExpiresCondition(type,long)"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addExpiresCondition(startsWith, "2030-Dec-31"); + return new PostPolicyV4.ConditionV4(eq, "expires", "2030-Dec-31"); + } + + @Override + public String toString() { + return "@deprecated addExpiresCondition(type,String)"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addSuccessActionStatusCondition(202); + return new PostPolicyV4.ConditionV4(eq, "success_action_status", "202"); + } + + @Override + public String toString() { + return "addSuccessActionStatusCondition(int)"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addSuccessActionStatusCondition(startsWith, 202); + return new PostPolicyV4.ConditionV4(eq, "success_action_status", "202"); + } + + @Override + public String toString() { + return "@deprecated addSuccessActionStatusCondition(type,int)"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addAclCondition(startsWith, "read"); + return new PostPolicyV4.ConditionV4(startsWith, "acl", "read"); + } + + @Override + public String toString() { + return "addAclCondition()"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addBucketCondition(eq, "my-bucket"); + return new PostPolicyV4.ConditionV4(eq, "bucket", "my-bucket"); + } + + @Override + public String toString() { + return "addBucketCondition()"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addCacheControlCondition(eq, "false"); + return new PostPolicyV4.ConditionV4(eq, "cache-control", "false"); + } + + @Override + public String toString() { + return "addCacheControlCondition()"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addContentDispositionCondition(startsWith, "gzip"); + return new PostPolicyV4.ConditionV4(startsWith, "content-disposition", "gzip"); + } + + @Override + public String toString() { + return "addContentDispositionCondition()"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addContentEncodingCondition(eq, "koi8"); + return new PostPolicyV4.ConditionV4(eq, "content-encoding", "koi8"); + } + + @Override + public String toString() { + return "addContentEncodingCondition()"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addContentTypeCondition(startsWith, "application/"); + return new PostPolicyV4.ConditionV4(startsWith, "content-type", "application/"); + } + + @Override + public String toString() { + return "addContentTypeCondition()"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addKeyCondition(startsWith, ""); + return new PostPolicyV4.ConditionV4(startsWith, "key", ""); + } + + @Override + public String toString() { + return "addKeyCondition()"; + } + }, + new ConditionTest() { + @Override + public PostPolicyV4.ConditionV4 addCondition( + PostPolicyV4.PostConditionsV4.Builder builder) { + builder.addSuccessActionRedirectUrlCondition(eq, "fail"); + return new PostPolicyV4.ConditionV4(eq, "success_action_redirect", "fail"); + } + + @Override + public String toString() { + return "addSuccessActionRedirectUrlCondition()"; + } + }, + }; + + for (ConditionTest testCase : cases) { + PostPolicyV4.PostConditionsV4.Builder builder = PostPolicyV4.PostConditionsV4.newBuilder(); + PostPolicyV4.ConditionV4 expected = testCase.addCondition(builder); + Set conditions = builder.build().getConditions(); + assertEquals("size", 1, conditions.size()); + PostPolicyV4.ConditionV4 actual = conditions.toArray(new PostPolicyV4.ConditionV4[1])[0]; + assertEquals(testCase.toString(), expected, actual); + } + } + + @Test + public void testPostConditionsV4_addConditionFail() { + final PostPolicyV4.PostConditionsV4.Builder builder = + PostPolicyV4.PostConditionsV4.newBuilder(); + final PostPolicyV4.ConditionV4Type range = PostPolicyV4.ConditionV4Type.CONTENT_LENGTH_RANGE; + + Callable[] cases = { + new Callable() { + @Override + public Void call() { + builder.addAclCondition(range, ""); + return null; + } + + @Override + public String toString() { + return "acl"; + } + }, + new Callable() { + @Override + public Void call() { + builder.addBucketCondition(range, ""); + return null; + } + + @Override + public String toString() { + return "bucket"; + } + }, + new Callable() { + @Override + public Void call() { + builder.addCacheControlCondition(range, ""); + return null; + } + + @Override + public String toString() { + return "cache-control"; + } + }, + new Callable() { + @Override + public Void call() { + builder.addContentDispositionCondition(range, ""); + return null; + } + + @Override + public String toString() { + return "content-disposition"; + } + }, + new Callable() { + @Override + public Void call() { + builder.addContentEncodingCondition(range, ""); + return null; + } + + @Override + public String toString() { + return "content-encoding"; + } + }, + new Callable() { + @Override + public Void call() { + builder.addContentTypeCondition(range, ""); + return null; + } + + @Override + public String toString() { + return "content-type"; + } + }, + new Callable() { + @Override + public Void call() { + builder.addKeyCondition(range, ""); + return null; + } + + @Override + public String toString() { + return "key"; + } + }, + new Callable() { + @Override + public Void call() { + builder.addSuccessActionRedirectUrlCondition(range, ""); + return null; + } + + @Override + public String toString() { + return "success_action_redirect"; + } + }, + }; + + for (Callable testCase : cases) { + try { + testCase.call(); + fail(); + } catch (Exception e) { + String expected = + "java.lang.IllegalArgumentException: Field " + + testCase + + " can't use content-length-range"; + assertEquals(expected, e.toString()); + } + } + assertTrue(builder.build().getConditions().isEmpty()); + } + + @Test + public void testPostConditionsV4_toString() { + PostPolicyV4.PostConditionsV4.Builder builder = PostPolicyV4.PostConditionsV4.newBuilder(); + builder.addKeyCondition(PostPolicyV4.ConditionV4Type.MATCHES, "test-object"); + builder.addAclCondition(PostPolicyV4.ConditionV4Type.STARTS_WITH, "public"); + builder.addContentLengthRangeCondition(246, 266); + + Set toStringSet = new HashSet<>(); + for (PostPolicyV4.ConditionV4 conditionV4 : builder.build().getConditions()) { + toStringSet.add(conditionV4.toString()); + } + assertEquals(3, toStringSet.size()); + + String[] expectedStrings = { + "[\"eq\", \"$key\", \"test-object\"]", + "[\"starts-with\", \"$acl\", \"public\"]", + "[\"content-length-range\", 246, 266]" + }; + + for (String expected : expectedStrings) { + assertTrue(expected + "/" + toStringSet, toStringSet.contains(expected)); + } + } + + @Test + public void testPostPolicyV4Document_of_toJson() { + PostPolicyV4.PostConditionsV4 emptyConditions = + PostPolicyV4.PostConditionsV4.newBuilder().build(); + PostPolicyV4.PostPolicyV4Document emptyDocument = + PostPolicyV4.PostPolicyV4Document.of("", emptyConditions); + String emptyJson = emptyDocument.toJson(); + assertEquals(emptyJson, "{\"conditions\":[],\"expiration\":\"\"}"); + + PostPolicyV4.PostConditionsV4 postConditionsV4 = + PostPolicyV4.PostConditionsV4.newBuilder() + .addBucketCondition(PostPolicyV4.ConditionV4Type.MATCHES, "my-bucket") + .addKeyCondition(PostPolicyV4.ConditionV4Type.STARTS_WITH, "") + .addContentLengthRangeCondition(1, 1000) + .build(); + + String expiration = dateFormat.format(System.currentTimeMillis()); + PostPolicyV4.PostPolicyV4Document document = + PostPolicyV4.PostPolicyV4Document.of(expiration, postConditionsV4); + String json = document.toJson(); + assertEquals( + json, + "{\"conditions\":[{\"bucket\":\"my-bucket\"},[\"starts-with\",\"$key\",\"\"],[\"content-length-range\",1,1000]],\"expiration\":\"" + + expiration + + "\"}"); + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java index 9eaefc66f..4efdda4d4 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -27,6 +28,7 @@ import static org.mockito.Mockito.mock; import com.google.api.core.ApiClock; +import com.google.api.gax.paging.Page; import com.google.api.services.storage.model.StorageObject; import com.google.cloud.Identity; import com.google.cloud.Policy; @@ -38,6 +40,7 @@ import com.google.cloud.storage.spi.v1.StorageRpc; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import com.google.common.io.BaseEncoding; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -938,8 +941,6 @@ public void testCreateBlobFromStreamWithEncryptionKey() throws IOException { @Test @SuppressWarnings({"unchecked", "deprecation"}) public void testCreateBlobFromStreamRetryableException() throws IOException { - ArgumentCaptor capturedStream = - ArgumentCaptor.forClass(ByteArrayInputStream.class); ByteArrayInputStream fileStream = new ByteArrayInputStream(BLOB_CONTENT); @@ -1144,6 +1145,330 @@ public void testCreateFromMultipleParts() throws Exception { assertEquals(Blob.fromPb(storage, storageObject), blob); } + @Test + public void testListBuckets() { + String cursor = "cursor"; + ImmutableList bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2); + Tuple> result = + Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION)); + + doReturn(result) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(EMPTY_RPC_OPTIONS); + + initializeService(); + ImmutableList bucketList = ImmutableList.of(expectedBucket1, expectedBucket2); + Page page = storage.list(); + assertEquals(cursor, page.getNextPageToken()); + assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class)); + } + + @Test + public void testListBucketsEmpty() { + doReturn(Tuple.>of(null, null)) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(EMPTY_RPC_OPTIONS); + + initializeService(); + Page page = storage.list(); + assertNull(page.getNextPageToken()); + assertArrayEquals( + ImmutableList.of().toArray(), Iterables.toArray(page.getValues(), Bucket.class)); + } + + @Test + public void testListBucketsWithOptions() { + String cursor = "cursor"; + ImmutableList bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2); + Tuple> result = + Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION)); + + doReturn(result) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(BUCKET_LIST_OPTIONS); + + initializeService(); + ImmutableList bucketList = ImmutableList.of(expectedBucket1, expectedBucket2); + Page page = storage.list(BUCKET_LIST_PAGE_SIZE, BUCKET_LIST_PREFIX); + assertEquals(cursor, page.getNextPageToken()); + assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class)); + } + + @Test + public void testListBucketsWithSelectedFields() { + String cursor = "cursor"; + ArgumentCaptor> capturedOptions = + ArgumentCaptor.forClass(Map.class); + + ImmutableList bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2); + Tuple> result = + Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION)); + + doReturn(result) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(capturedOptions.capture()); + initializeService(); + ImmutableList bucketList = ImmutableList.of(expectedBucket1, expectedBucket2); + Page page = storage.list(BUCKET_LIST_FIELDS); + String selector = (String) capturedOptions.getValue().get(BUCKET_LIST_FIELDS.getRpcOption()); + assertTrue(selector.contains("items(")); + assertTrue(selector.contains("name")); + assertTrue(selector.contains("acl")); + assertTrue(selector.contains("location")); + assertTrue(selector.contains("nextPageToken")); + assertTrue(selector.endsWith(")")); + assertEquals(38, selector.length()); + assertEquals(cursor, page.getNextPageToken()); + assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class)); + } + + @Test + public void testListBucketsWithEmptyFields() { + String cursor = "cursor"; + ArgumentCaptor> capturedOptions = + ArgumentCaptor.forClass(Map.class); + ImmutableList bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2); + Tuple> result = + Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION)); + + doReturn(result) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(capturedOptions.capture()); + initializeService(); + ImmutableList bucketList = ImmutableList.of(expectedBucket1, expectedBucket2); + Page page = storage.list(BUCKET_LIST_EMPTY_FIELDS); + String selector = + (String) capturedOptions.getValue().get(BUCKET_LIST_EMPTY_FIELDS.getRpcOption()); + assertTrue(selector.contains("items(")); + assertTrue(selector.contains("name")); + assertTrue(selector.contains("nextPageToken")); + assertTrue(selector.endsWith(")")); + assertEquals(25, selector.length()); + assertEquals(cursor, page.getNextPageToken()); + assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class)); + } + + @Test + public void testListBucketsWithException() { + doThrow(STORAGE_FAILURE).when(storageRpcMock).list(EMPTY_RPC_OPTIONS); + initializeService(); + try { + storage.list(); + fail(); + } catch (StorageException e) { + assertEquals(STORAGE_FAILURE.toString(), e.getMessage()); + } + } + + @Test + public void testListBlobs() { + String cursor = "cursor"; + ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); + Tuple> result = + Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); + + doReturn(result) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(BUCKET_NAME1, EMPTY_RPC_OPTIONS); + + initializeService(); + ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); + Page page = storage.list(BUCKET_NAME1); + assertEquals(cursor, page.getNextPageToken()); + assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); + } + + @Test + public void testListBlobsEmpty() { + doReturn( + Tuple.>of( + null, null)) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(BUCKET_NAME1, EMPTY_RPC_OPTIONS); + + initializeService(); + Page page = storage.list(BUCKET_NAME1); + assertNull(page.getNextPageToken()); + assertArrayEquals( + ImmutableList.of().toArray(), Iterables.toArray(page.getValues(), Blob.class)); + } + + @Test + public void testListBlobsWithOptions() { + String cursor = "cursor"; + ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); + Tuple> result = + Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); + doReturn(result) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(BUCKET_NAME1, BLOB_LIST_OPTIONS); + initializeService(); + ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); + Page page = + storage.list(BUCKET_NAME1, BLOB_LIST_PAGE_SIZE, BLOB_LIST_PREFIX, BLOB_LIST_VERSIONS); + assertEquals(cursor, page.getNextPageToken()); + assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); + } + + @Test + public void testListBlobsWithSelectedFields() { + String cursor = "cursor"; + ArgumentCaptor> capturedOptions = + ArgumentCaptor.forClass(Map.class); + ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); + Tuple> result = + Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); + doReturn(result) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(Mockito.eq(BUCKET_NAME1), capturedOptions.capture()); + + initializeService(); + ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); + Page page = + storage.list(BUCKET_NAME1, BLOB_LIST_PAGE_SIZE, BLOB_LIST_PREFIX, BLOB_LIST_FIELDS); + assertEquals( + BLOB_LIST_PAGE_SIZE.getValue(), + capturedOptions.getValue().get(BLOB_LIST_PAGE_SIZE.getRpcOption())); + assertEquals( + BLOB_LIST_PREFIX.getValue(), + capturedOptions.getValue().get(BLOB_LIST_PREFIX.getRpcOption())); + String selector = (String) capturedOptions.getValue().get(BLOB_LIST_FIELDS.getRpcOption()); + assertTrue(selector.contains("prefixes")); + assertTrue(selector.contains("items(")); + assertTrue(selector.contains("bucket")); + assertTrue(selector.contains("name")); + assertTrue(selector.contains("contentType")); + assertTrue(selector.contains("md5Hash")); + assertTrue(selector.contains("nextPageToken")); + assertTrue(selector.endsWith(")")); + assertEquals(61, selector.length()); + assertEquals(cursor, page.getNextPageToken()); + assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); + } + + @Test + public void testListBlobsWithEmptyFields() { + String cursor = "cursor"; + ArgumentCaptor> capturedOptions = + ArgumentCaptor.forClass(Map.class); + ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); + Tuple> result = + Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); + doReturn(result) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(Mockito.eq(BUCKET_NAME1), capturedOptions.capture()); + + initializeService(); + ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); + Page page = + storage.list(BUCKET_NAME1, BLOB_LIST_PAGE_SIZE, BLOB_LIST_PREFIX, BLOB_LIST_EMPTY_FIELDS); + assertEquals( + BLOB_LIST_PAGE_SIZE.getValue(), + capturedOptions.getValue().get(BLOB_LIST_PAGE_SIZE.getRpcOption())); + assertEquals( + BLOB_LIST_PREFIX.getValue(), + capturedOptions.getValue().get(BLOB_LIST_PREFIX.getRpcOption())); + String selector = + (String) capturedOptions.getValue().get(BLOB_LIST_EMPTY_FIELDS.getRpcOption()); + assertTrue(selector.contains("prefixes")); + assertTrue(selector.contains("items(")); + assertTrue(selector.contains("bucket")); + assertTrue(selector.contains("name")); + assertTrue(selector.contains("nextPageToken")); + assertTrue(selector.endsWith(")")); + assertEquals(41, selector.length()); + assertEquals(cursor, page.getNextPageToken()); + assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); + } + + @Test + public void testListBlobsCurrentDirectory() { + String cursor = "cursor"; + Map options = ImmutableMap.of(StorageRpc.Option.DELIMITER, "/"); + ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); + Tuple> result = + Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); + doReturn(result) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(BUCKET_NAME1, options); + + initializeService(); + ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); + Page page = storage.list(BUCKET_NAME1, Storage.BlobListOption.currentDirectory()); + assertEquals(cursor, page.getNextPageToken()); + assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); + } + + @Test + public void testListBlobsDelimiter() { + String cursor = "cursor"; + String delimiter = "/"; + Map options = ImmutableMap.of(StorageRpc.Option.DELIMITER, delimiter); + ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); + Tuple> result = + Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); + doReturn(result) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(BUCKET_NAME1, options); + + initializeService(); + ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); + Page page = storage.list(BUCKET_NAME1, Storage.BlobListOption.delimiter(delimiter)); + assertEquals(cursor, page.getNextPageToken()); + assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); + } + + @Test + public void testListBlobsWithOffset() { + String cursor = "cursor"; + String startOffset = "startOffset"; + String endOffset = "endOffset"; + Map options = + ImmutableMap.of( + StorageRpc.Option.START_OFF_SET, startOffset, StorageRpc.Option.END_OFF_SET, endOffset); + ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); + Tuple> result = + Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); + doReturn(result) + .doThrow(UNEXPECTED_CALL_EXCEPTION) + .when(storageRpcMock) + .list(BUCKET_NAME1, options); + + initializeService(); + ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); + Page page = + storage.list( + BUCKET_NAME1, + Storage.BlobListOption.startOffset(startOffset), + Storage.BlobListOption.endOffset(endOffset)); + assertEquals(cursor, page.getNextPageToken()); + assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); + } + + @Test + public void testListBlobsWithException() { + doThrow(STORAGE_FAILURE).when(storageRpcMock).list(BUCKET_NAME1, EMPTY_RPC_OPTIONS); + initializeService(); + try { + storage.list(BUCKET_NAME1); + fail(); + } catch (StorageException e) { + assertEquals(STORAGE_FAILURE.toString(), e.getMessage()); + } + } + private void verifyChannelRead(ReadChannel channel, byte[] bytes) throws IOException { assertNotNull(channel); assertTrue(channel.isOpen()); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java index 6aa76a03d..590fe37af 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java @@ -29,8 +29,8 @@ import static org.junit.Assert.assertTrue; import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.client.util.DateTime; import com.google.api.core.ApiClock; -import com.google.api.gax.paging.Page; import com.google.api.services.storage.model.Policy.Bindings; import com.google.api.services.storage.model.StorageObject; import com.google.api.services.storage.model.TestIamPermissionsResponse; @@ -38,7 +38,6 @@ import com.google.cloud.Identity; import com.google.cloud.Policy; import com.google.cloud.ServiceOptions; -import com.google.cloud.Tuple; import com.google.cloud.WriteChannel; import com.google.cloud.storage.Acl.Project; import com.google.cloud.storage.Acl.Project.ProjectRole; @@ -54,7 +53,6 @@ import com.google.cloud.storage.spi.v1.StorageRpc; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; import com.google.common.io.BaseEncoding; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; @@ -428,243 +426,6 @@ private void initializeServiceDependentObjects() { expectedBucket3 = new Bucket(storage, new BucketInfo.BuilderImpl(BUCKET_INFO3)); } - @Test - public void testListBuckets() { - String cursor = "cursor"; - ImmutableList bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2); - Tuple> result = - Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION)); - EasyMock.expect(storageRpcMock.list(EMPTY_RPC_OPTIONS)).andReturn(result); - EasyMock.replay(storageRpcMock); - initializeService(); - ImmutableList bucketList = ImmutableList.of(expectedBucket1, expectedBucket2); - Page page = storage.list(); - assertEquals(cursor, page.getNextPageToken()); - assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class)); - } - - @Test - public void testListBucketsEmpty() { - EasyMock.expect(storageRpcMock.list(EMPTY_RPC_OPTIONS)) - .andReturn( - Tuple.>of(null, null)); - EasyMock.replay(storageRpcMock); - initializeService(); - Page page = storage.list(); - assertNull(page.getNextPageToken()); - assertArrayEquals( - ImmutableList.of().toArray(), Iterables.toArray(page.getValues(), Bucket.class)); - } - - @Test - public void testListBucketsWithOptions() { - String cursor = "cursor"; - ImmutableList bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2); - Tuple> result = - Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION)); - EasyMock.expect(storageRpcMock.list(BUCKET_LIST_OPTIONS)).andReturn(result); - EasyMock.replay(storageRpcMock); - initializeService(); - ImmutableList bucketList = ImmutableList.of(expectedBucket1, expectedBucket2); - Page page = storage.list(BUCKET_LIST_PAGE_SIZE, BUCKET_LIST_PREFIX); - assertEquals(cursor, page.getNextPageToken()); - assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class)); - } - - @Test - public void testListBucketsWithSelectedFields() { - String cursor = "cursor"; - Capture> capturedOptions = Capture.newInstance(); - ImmutableList bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2); - Tuple> result = - Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION)); - EasyMock.expect(storageRpcMock.list(EasyMock.capture(capturedOptions))).andReturn(result); - EasyMock.replay(storageRpcMock); - initializeService(); - ImmutableList bucketList = ImmutableList.of(expectedBucket1, expectedBucket2); - Page page = storage.list(BUCKET_LIST_FIELDS); - String selector = (String) capturedOptions.getValue().get(BUCKET_LIST_FIELDS.getRpcOption()); - assertTrue(selector.contains("items(")); - assertTrue(selector.contains("name")); - assertTrue(selector.contains("acl")); - assertTrue(selector.contains("location")); - assertTrue(selector.contains("nextPageToken")); - assertTrue(selector.endsWith(")")); - assertEquals(38, selector.length()); - assertEquals(cursor, page.getNextPageToken()); - assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class)); - } - - @Test - public void testListBucketsWithEmptyFields() { - String cursor = "cursor"; - Capture> capturedOptions = Capture.newInstance(); - ImmutableList bucketInfoList = ImmutableList.of(BUCKET_INFO1, BUCKET_INFO2); - Tuple> result = - Tuple.of(cursor, Iterables.transform(bucketInfoList, BucketInfo.TO_PB_FUNCTION)); - EasyMock.expect(storageRpcMock.list(EasyMock.capture(capturedOptions))).andReturn(result); - EasyMock.replay(storageRpcMock); - initializeService(); - ImmutableList bucketList = ImmutableList.of(expectedBucket1, expectedBucket2); - Page page = storage.list(BUCKET_LIST_EMPTY_FIELDS); - String selector = - (String) capturedOptions.getValue().get(BUCKET_LIST_EMPTY_FIELDS.getRpcOption()); - assertTrue(selector.contains("items(")); - assertTrue(selector.contains("name")); - assertTrue(selector.contains("nextPageToken")); - assertTrue(selector.endsWith(")")); - assertEquals(25, selector.length()); - assertEquals(cursor, page.getNextPageToken()); - assertArrayEquals(bucketList.toArray(), Iterables.toArray(page.getValues(), Bucket.class)); - } - - @Test - public void testListBlobs() { - String cursor = "cursor"; - ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); - Tuple> result = - Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); - EasyMock.expect(storageRpcMock.list(BUCKET_NAME1, EMPTY_RPC_OPTIONS)).andReturn(result); - EasyMock.replay(storageRpcMock); - initializeService(); - ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); - Page page = storage.list(BUCKET_NAME1); - assertEquals(cursor, page.getNextPageToken()); - assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); - } - - @Test - public void testListBlobsEmpty() { - EasyMock.expect(storageRpcMock.list(BUCKET_NAME1, EMPTY_RPC_OPTIONS)) - .andReturn( - Tuple.>of( - null, null)); - EasyMock.replay(storageRpcMock); - initializeService(); - Page page = storage.list(BUCKET_NAME1); - assertNull(page.getNextPageToken()); - assertArrayEquals( - ImmutableList.of().toArray(), Iterables.toArray(page.getValues(), Blob.class)); - } - - @Test - public void testListBlobsWithOptions() { - String cursor = "cursor"; - ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); - Tuple> result = - Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); - EasyMock.expect(storageRpcMock.list(BUCKET_NAME1, BLOB_LIST_OPTIONS)).andReturn(result); - EasyMock.replay(storageRpcMock); - initializeService(); - ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); - Page page = - storage.list(BUCKET_NAME1, BLOB_LIST_PAGE_SIZE, BLOB_LIST_PREFIX, BLOB_LIST_VERSIONS); - assertEquals(cursor, page.getNextPageToken()); - assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); - } - - @Test - public void testListBlobsWithSelectedFields() { - String cursor = "cursor"; - Capture> capturedOptions = Capture.newInstance(); - ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); - Tuple> result = - Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); - EasyMock.expect( - storageRpcMock.list(EasyMock.eq(BUCKET_NAME1), EasyMock.capture(capturedOptions))) - .andReturn(result); - EasyMock.replay(storageRpcMock); - initializeService(); - ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); - Page page = - storage.list(BUCKET_NAME1, BLOB_LIST_PAGE_SIZE, BLOB_LIST_PREFIX, BLOB_LIST_FIELDS); - assertEquals( - BLOB_LIST_PAGE_SIZE.getValue(), - capturedOptions.getValue().get(BLOB_LIST_PAGE_SIZE.getRpcOption())); - assertEquals( - BLOB_LIST_PREFIX.getValue(), - capturedOptions.getValue().get(BLOB_LIST_PREFIX.getRpcOption())); - String selector = (String) capturedOptions.getValue().get(BLOB_LIST_FIELDS.getRpcOption()); - assertTrue(selector.contains("prefixes")); - assertTrue(selector.contains("items(")); - assertTrue(selector.contains("bucket")); - assertTrue(selector.contains("name")); - assertTrue(selector.contains("contentType")); - assertTrue(selector.contains("md5Hash")); - assertTrue(selector.contains("nextPageToken")); - assertTrue(selector.endsWith(")")); - assertEquals(61, selector.length()); - assertEquals(cursor, page.getNextPageToken()); - assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); - } - - @Test - public void testListBlobsWithEmptyFields() { - String cursor = "cursor"; - Capture> capturedOptions = Capture.newInstance(); - ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); - Tuple> result = - Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); - EasyMock.expect( - storageRpcMock.list(EasyMock.eq(BUCKET_NAME1), EasyMock.capture(capturedOptions))) - .andReturn(result); - EasyMock.replay(storageRpcMock); - initializeService(); - ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); - Page page = - storage.list(BUCKET_NAME1, BLOB_LIST_PAGE_SIZE, BLOB_LIST_PREFIX, BLOB_LIST_EMPTY_FIELDS); - assertEquals( - BLOB_LIST_PAGE_SIZE.getValue(), - capturedOptions.getValue().get(BLOB_LIST_PAGE_SIZE.getRpcOption())); - assertEquals( - BLOB_LIST_PREFIX.getValue(), - capturedOptions.getValue().get(BLOB_LIST_PREFIX.getRpcOption())); - String selector = - (String) capturedOptions.getValue().get(BLOB_LIST_EMPTY_FIELDS.getRpcOption()); - assertTrue(selector.contains("prefixes")); - assertTrue(selector.contains("items(")); - assertTrue(selector.contains("bucket")); - assertTrue(selector.contains("name")); - assertTrue(selector.contains("nextPageToken")); - assertTrue(selector.endsWith(")")); - assertEquals(41, selector.length()); - assertEquals(cursor, page.getNextPageToken()); - assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); - } - - @Test - public void testListBlobsCurrentDirectory() { - String cursor = "cursor"; - Map options = ImmutableMap.of(StorageRpc.Option.DELIMITER, "/"); - ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); - Tuple> result = - Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); - EasyMock.expect(storageRpcMock.list(BUCKET_NAME1, options)).andReturn(result); - EasyMock.replay(storageRpcMock); - initializeService(); - ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); - Page page = storage.list(BUCKET_NAME1, Storage.BlobListOption.currentDirectory()); - assertEquals(cursor, page.getNextPageToken()); - assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); - } - - @Test - public void testListBlobsDelimiter() { - String cursor = "cursor"; - String delimiter = "/"; - Map options = ImmutableMap.of(StorageRpc.Option.DELIMITER, delimiter); - ImmutableList blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2); - Tuple> result = - Tuple.of(cursor, Iterables.transform(blobInfoList, BlobInfo.INFO_TO_PB_FUNCTION)); - EasyMock.expect(storageRpcMock.list(BUCKET_NAME1, options)).andReturn(result); - EasyMock.replay(storageRpcMock); - initializeService(); - ImmutableList blobList = ImmutableList.of(expectedBlob1, expectedBlob2); - Page page = storage.list(BUCKET_NAME1, Storage.BlobListOption.delimiter(delimiter)); - assertEquals(cursor, page.getNextPageToken()); - assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class)); - } - @Test public void testUpdateBucket() { BucketInfo updatedBucketInfo = BUCKET_INFO1.toBuilder().setIndexPage("some-page").build(); @@ -2472,4 +2233,44 @@ public void testV4PostPolicy() { assertEquals(outputFields.get("key"), "my-object"); assertEquals("https://storage.googleapis.com/my-bucket/", policy.getUrl()); } + + @Test + public void testBucketLifecycleRules() { + BucketInfo bucketInfo = + BucketInfo.newBuilder("b") + .setLocation("us") + .setLifecycleRules( + ImmutableList.of( + new BucketInfo.LifecycleRule( + BucketInfo.LifecycleRule.LifecycleAction.newSetStorageClassAction( + StorageClass.COLDLINE), + BucketInfo.LifecycleRule.LifecycleCondition.newBuilder() + .setAge(1) + .setNumberOfNewerVersions(3) + .setIsLive(false) + .setCreatedBefore(new DateTime(System.currentTimeMillis())) + .setMatchesStorageClass(ImmutableList.of(StorageClass.COLDLINE)) + .setDaysSinceNoncurrentTime(30) + .setNoncurrentTimeBefore(new DateTime(System.currentTimeMillis())) + .setCustomTimeBefore(new DateTime(System.currentTimeMillis())) + .setDaysSinceCustomTime(30) + .build()))) + .build(); + EasyMock.expect( + storageRpcMock.create(bucketInfo.toPb(), new HashMap())) + .andReturn(bucketInfo.toPb()); + EasyMock.replay(storageRpcMock); + initializeService(); + Bucket bucket = storage.create(bucketInfo); + BucketInfo.LifecycleRule lifecycleRule = bucket.getLifecycleRules().get(0); + assertEquals(3, lifecycleRule.getCondition().getNumberOfNewerVersions().intValue()); + assertNotNull(lifecycleRule.getCondition().getCreatedBefore()); + assertFalse(lifecycleRule.getCondition().getIsLive()); + assertEquals(1, lifecycleRule.getCondition().getAge().intValue()); + assertEquals(1, lifecycleRule.getCondition().getMatchesStorageClass().size()); + assertEquals(30, lifecycleRule.getCondition().getDaysSinceNoncurrentTime().intValue()); + assertNotNull(lifecycleRule.getCondition().getNoncurrentTimeBefore()); + assertEquals(30, lifecycleRule.getCondition().getDaysSinceCustomTime().intValue()); + assertNotNull(lifecycleRule.getCondition().getCustomTimeBefore()); + } } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java index ba4b3623f..925e3d6d1 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java @@ -66,6 +66,7 @@ import com.google.cloud.storage.BucketInfo.LifecycleRule.LifecycleAction; import com.google.cloud.storage.BucketInfo.LifecycleRule.LifecycleCondition; import com.google.cloud.storage.CopyWriter; +import com.google.cloud.storage.Cors; import com.google.cloud.storage.HmacKey; import com.google.cloud.storage.HttpMethod; import com.google.cloud.storage.PostPolicyV4; @@ -135,7 +136,6 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.junit.AfterClass; -import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -150,6 +150,7 @@ public class ITStorageTest { Metadata.Key.of("x-goog-request-params", Metadata.ASCII_STRING_MARSHALLER); private static final Logger log = Logger.getLogger(ITStorageTest.class.getName()); private static final String BUCKET = RemoteStorageHelper.generateBucketName(); + private static final String BUCKET_REQUESTER_PAYS = RemoteStorageHelper.generateBucketName(); private static final String CONTENT_TYPE = "text/plain"; private static final byte[] BLOB_BYTE_CONTENT = {0xD, 0xE, 0xA, 0xD}; private static final String BLOB_STRING_CONTENT = "Hello Google Cloud Storage!"; @@ -212,15 +213,16 @@ public static void beforeClass() throws IOException { LifecycleCondition.newBuilder().setAge(1).build()))) .build()); + storage.create(BucketInfo.newBuilder(BUCKET_REQUESTER_PAYS).build()); + // Prepare KMS KeyRing for CMEK tests prepareKmsKeys(); } - @Before - public void beforeEach() { + private static void unsetRequesterPays() { Bucket remoteBucket = storage.get( - BUCKET, + BUCKET_REQUESTER_PAYS, Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING), Storage.BucketGetOption.userProject(storage.getOptions().getProjectId())); // Disable requester pays in case a test fails to clean up. @@ -246,6 +248,8 @@ public static void afterClass() throws ExecutionException, InterruptedException if (!wasDeleted && log.isLoggable(Level.WARNING)) { log.log(Level.WARNING, "Deletion of bucket {0} timed out, bucket is not empty", BUCKET); } + unsetRequesterPays(); + RemoteStorageHelper.forceDelete(storage, BUCKET_REQUESTER_PAYS, 5, TimeUnit.SECONDS); } } @@ -260,6 +264,7 @@ public HttpTransport create() { } private static void prepareKmsKeys() throws IOException { + // https://cloud.google.com/storage/docs/encryption/using-customer-managed-keys String projectId = remoteStorageHelper.getOptions().getProjectId(); GoogleCredentials credentials = GoogleCredentials.getApplicationDefault(); ManagedChannel kmsChannel = @@ -339,7 +344,13 @@ private static void ensureKmsKeyRingIamPermissionsForTests( .build(); requestParamsHeader.put(requestParamsKey, "parent=" + kmsKeyRingResourcePath); iamStub = MetadataUtils.attachHeaders(iamStub, requestParamsHeader); - iamStub.setIamPolicy(setIamPolicyRequest); + try { + iamStub.setIamPolicy(setIamPolicyRequest); + } catch (StatusRuntimeException e) { + if (log.isLoggable(Level.WARNING)) { + log.log(Level.WARNING, "Unable to set IAM policy: {0}", e.getMessage()); + } + } } private static String ensureKmsKeyExistsForTests( @@ -445,6 +456,10 @@ public void testGetBucketLifecycleRules() { .setIsLive(false) .setCreatedBefore(new DateTime(System.currentTimeMillis())) .setMatchesStorageClass(ImmutableList.of(StorageClass.COLDLINE)) + .setDaysSinceNoncurrentTime(30) + .setNoncurrentTimeBefore(new DateTime(System.currentTimeMillis())) + .setCustomTimeBefore(new DateTime(System.currentTimeMillis())) + .setDaysSinceCustomTime(30) .build()))) .build()); Bucket remoteBucket = @@ -461,6 +476,10 @@ public void testGetBucketLifecycleRules() { assertFalse(lifecycleRule.getCondition().getIsLive()); assertEquals(1, lifecycleRule.getCondition().getAge().intValue()); assertEquals(1, lifecycleRule.getCondition().getMatchesStorageClass().size()); + assertEquals(30, lifecycleRule.getCondition().getDaysSinceNoncurrentTime().intValue()); + assertNotNull(lifecycleRule.getCondition().getNoncurrentTimeBefore()); + assertEquals(30, lifecycleRule.getCondition().getDaysSinceCustomTime().intValue()); + assertNotNull(lifecycleRule.getCondition().getCustomTimeBefore()); } finally { storage.delete(lifecycleTestBucketName); } @@ -508,9 +527,11 @@ public void testUpdateBucketDefaultKmsKeyName() throws ExecutionException, Inter @Test public void testCreateBlob() { String blobName = "test-create-blob"; - BlobInfo blob = BlobInfo.newBuilder(BUCKET, blobName).build(); + BlobInfo blob = + BlobInfo.newBuilder(BUCKET, blobName).setCustomTime(System.currentTimeMillis()).build(); Blob remoteBlob = storage.create(blob, BLOB_BYTE_CONTENT); assertNotNull(remoteBlob); + assertNotNull(remoteBlob.getCustomTime()); assertEquals(blob.getBucket(), remoteBlob.getBucket()); assertEquals(blob.getName(), remoteBlob.getName()); byte[] readBytes = storage.readAllBytes(BUCKET, blobName); @@ -905,22 +926,26 @@ public void testListBlobsEmptySelectedFields() throws InterruptedException { @Test(timeout = 7500) public void testListBlobRequesterPays() throws InterruptedException { + unsetRequesterPays(); BlobInfo blob1 = - BlobInfo.newBuilder(BUCKET, "test-list-blobs-empty-selected-fields-blob1") + BlobInfo.newBuilder(BUCKET_REQUESTER_PAYS, "test-list-blobs-empty-selected-fields-blob1") .setContentType(CONTENT_TYPE) .build(); assertNotNull(storage.create(blob1)); // Test listing a Requester Pays bucket. Bucket remoteBucket = - storage.get(BUCKET, Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING)); - assertFalse(remoteBucket.requesterPays()); + storage.get( + BUCKET_REQUESTER_PAYS, + Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING)); + + assertTrue(remoteBucket.requesterPays() == null || !remoteBucket.requesterPays()); remoteBucket = remoteBucket.toBuilder().setRequesterPays(true).build(); Bucket updatedBucket = storage.update(remoteBucket); assertTrue(updatedBucket.requesterPays()); try { storage.list( - BUCKET, + BUCKET_REQUESTER_PAYS, Storage.BlobListOption.prefix("test-list-blobs-empty-selected-fields-blob"), Storage.BlobListOption.fields(), Storage.BlobListOption.userProject("fakeBillingProjectId")); @@ -933,7 +958,7 @@ public void testListBlobRequesterPays() throws InterruptedException { while (true) { Page page = storage.list( - BUCKET, + BUCKET_REQUESTER_PAYS, Storage.BlobListOption.prefix("test-list-blobs-empty-selected-fields-blob"), Storage.BlobListOption.fields(), Storage.BlobListOption.userProject(projectId)); @@ -993,6 +1018,53 @@ public void testListBlobsVersioned() throws ExecutionException, InterruptedExcep } } + @Test + public void testListBlobsWithOffset() throws ExecutionException, InterruptedException { + String bucketName = RemoteStorageHelper.generateBucketName(); + Bucket bucket = + storage.create(BucketInfo.newBuilder(bucketName).setVersioningEnabled(true).build()); + try { + List blobNames = + ImmutableList.of("startOffset_blob1", "startOffset_blob2", "blob3_endOffset"); + BlobInfo blob1 = + BlobInfo.newBuilder(bucket, blobNames.get(0)).setContentType(CONTENT_TYPE).build(); + BlobInfo blob2 = + BlobInfo.newBuilder(bucket, blobNames.get(1)).setContentType(CONTENT_TYPE).build(); + BlobInfo blob3 = + BlobInfo.newBuilder(bucket, blobNames.get(2)).setContentType(CONTENT_TYPE).build(); + + Blob remoteBlob1 = storage.create(blob1); + Blob remoteBlob2 = storage.create(blob2); + Blob remoteBlob3 = storage.create(blob3); + assertNotNull(remoteBlob1); + assertNotNull(remoteBlob2); + assertNotNull(remoteBlob3); + + // Listing blobs without BlobListOptions. + Page page1 = storage.list(bucketName); + assertEquals(3, Iterators.size(page1.iterateAll().iterator())); + + // Listing blobs with startOffset. + Page page2 = + storage.list(bucketName, Storage.BlobListOption.startOffset("startOffset")); + assertEquals(2, Iterators.size(page2.iterateAll().iterator())); + + // Listing blobs with endOffset. + Page page3 = storage.list(bucketName, Storage.BlobListOption.endOffset("endOffset")); + assertEquals(1, Iterators.size(page3.iterateAll().iterator())); + + // Listing blobs with startOffset and endOffset. + Page page4 = + storage.list( + bucketName, + Storage.BlobListOption.startOffset("startOffset"), + Storage.BlobListOption.endOffset("endOffset")); + assertEquals(0, Iterators.size(page4.iterateAll().iterator())); + } finally { + RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS); + } + } + @Test(timeout = 5000) public void testListBlobsCurrentDirectory() throws InterruptedException { String directoryName = "test-list-blobs-current-directory/"; @@ -2178,6 +2250,7 @@ public void testUpdateBlobsFail() { @Test public void testBucketAcl() { + unsetRequesterPays(); testBucketAclRequesterPays(true); testBucketAclRequesterPays(false); } @@ -2185,8 +2258,10 @@ public void testBucketAcl() { private void testBucketAclRequesterPays(boolean requesterPays) { if (requesterPays) { Bucket remoteBucket = - storage.get(BUCKET, Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING)); - assertNull(remoteBucket.requesterPays()); + storage.get( + BUCKET_REQUESTER_PAYS, + Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING)); + assertTrue(remoteBucket.requesterPays() == null || !remoteBucket.requesterPays()); remoteBucket = remoteBucket.toBuilder().setRequesterPays(true).build(); Bucket updatedBucket = storage.update(remoteBucket); assertTrue(updatedBucket.requesterPays()); @@ -2199,22 +2274,27 @@ private void testBucketAclRequesterPays(boolean requesterPays) { ? new Storage.BucketSourceOption[] {Storage.BucketSourceOption.userProject(projectId)} : new Storage.BucketSourceOption[] {}; - assertNull(storage.getAcl(BUCKET, User.ofAllAuthenticatedUsers(), bucketOptions)); - assertFalse(storage.deleteAcl(BUCKET, User.ofAllAuthenticatedUsers(), bucketOptions)); + assertNull( + storage.getAcl(BUCKET_REQUESTER_PAYS, User.ofAllAuthenticatedUsers(), bucketOptions)); + assertFalse( + storage.deleteAcl(BUCKET_REQUESTER_PAYS, User.ofAllAuthenticatedUsers(), bucketOptions)); Acl acl = Acl.of(User.ofAllAuthenticatedUsers(), Role.READER); - assertNotNull(storage.createAcl(BUCKET, acl, bucketOptions)); + assertNotNull(storage.createAcl(BUCKET_REQUESTER_PAYS, acl, bucketOptions)); Acl updatedAcl = - storage.updateAcl(BUCKET, acl.toBuilder().setRole(Role.WRITER).build(), bucketOptions); + storage.updateAcl( + BUCKET_REQUESTER_PAYS, acl.toBuilder().setRole(Role.WRITER).build(), bucketOptions); assertEquals(Role.WRITER, updatedAcl.getRole()); Set acls = new HashSet<>(); - acls.addAll(storage.listAcls(BUCKET, bucketOptions)); + acls.addAll(storage.listAcls(BUCKET_REQUESTER_PAYS, bucketOptions)); assertTrue(acls.contains(updatedAcl)); - assertTrue(storage.deleteAcl(BUCKET, User.ofAllAuthenticatedUsers(), bucketOptions)); - assertNull(storage.getAcl(BUCKET, User.ofAllAuthenticatedUsers(), bucketOptions)); + assertTrue( + storage.deleteAcl(BUCKET_REQUESTER_PAYS, User.ofAllAuthenticatedUsers(), bucketOptions)); + assertNull( + storage.getAcl(BUCKET_REQUESTER_PAYS, User.ofAllAuthenticatedUsers(), bucketOptions)); if (requesterPays) { Bucket remoteBucket = storage.get( - BUCKET, + BUCKET_REQUESTER_PAYS, Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING), Storage.BucketGetOption.userProject(projectId)); assertTrue(remoteBucket.requesterPays()); @@ -2406,80 +2486,75 @@ public void testReadCompressedBlob() throws IOException { @Test public void testBucketPolicyV1RequesterPays() throws ExecutionException, InterruptedException { - String bucketName = RemoteStorageHelper.generateBucketName(); - storage.create(BucketInfo.newBuilder(bucketName).build()); + unsetRequesterPays(); + Bucket bucketDefault = + storage.get( + BUCKET_REQUESTER_PAYS, + Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING)); + assertTrue(bucketDefault.requesterPays() == null || !bucketDefault.requesterPays()); - try { - Bucket bucketDefault = - storage.get( - bucketName, Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING)); - assertNull(bucketDefault.requesterPays()); - - Bucket bucketTrue = storage.update(bucketDefault.toBuilder().setRequesterPays(true).build()); - assertTrue(bucketTrue.requesterPays()); - - String projectId = remoteStorageHelper.getOptions().getProjectId(); - - Storage.BucketSourceOption[] bucketOptions = - new Storage.BucketSourceOption[] {Storage.BucketSourceOption.userProject(projectId)}; - Identity projectOwner = Identity.projectOwner(projectId); - Identity projectEditor = Identity.projectEditor(projectId); - Identity projectViewer = Identity.projectViewer(projectId); - Map> bindingsWithoutPublicRead = - ImmutableMap.of( - StorageRoles.legacyBucketOwner(), - new HashSet<>(Arrays.asList(projectOwner, projectEditor)), - StorageRoles.legacyBucketReader(), - (Set) new HashSet<>(Collections.singleton(projectViewer))); - Map> bindingsWithPublicRead = - ImmutableMap.of( - StorageRoles.legacyBucketOwner(), - new HashSet<>(Arrays.asList(projectOwner, projectEditor)), - StorageRoles.legacyBucketReader(), - new HashSet<>(Collections.singleton(projectViewer)), - StorageRoles.legacyObjectReader(), - (Set) new HashSet<>(Collections.singleton(Identity.allUsers()))); - - // Validate getting policy. - Policy currentPolicy = storage.getIamPolicy(bucketName, bucketOptions); - assertEquals(bindingsWithoutPublicRead, currentPolicy.getBindings()); - - // Validate updating policy. - Policy updatedPolicy = - storage.setIamPolicy( - bucketName, - currentPolicy - .toBuilder() - .addIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers()) - .build(), - bucketOptions); - assertEquals(bindingsWithPublicRead, updatedPolicy.getBindings()); - Policy revertedPolicy = - storage.setIamPolicy( - bucketName, - updatedPolicy - .toBuilder() - .removeIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers()) - .build(), - bucketOptions); - assertEquals(bindingsWithoutPublicRead, revertedPolicy.getBindings()); - - // Validate testing permissions. - List expectedPermissions = ImmutableList.of(true, true); - assertEquals( - expectedPermissions, - storage.testIamPermissions( - bucketName, - ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"), - bucketOptions)); - Bucket bucketFalse = - storage.update( - bucketTrue.toBuilder().setRequesterPays(false).build(), - Storage.BucketTargetOption.userProject(projectId)); - assertFalse(bucketFalse.requesterPays()); - } finally { - RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS); - } + Bucket bucketTrue = storage.update(bucketDefault.toBuilder().setRequesterPays(true).build()); + assertTrue(bucketTrue.requesterPays()); + + String projectId = remoteStorageHelper.getOptions().getProjectId(); + + Storage.BucketSourceOption[] bucketOptions = + new Storage.BucketSourceOption[] {Storage.BucketSourceOption.userProject(projectId)}; + Identity projectOwner = Identity.projectOwner(projectId); + Identity projectEditor = Identity.projectEditor(projectId); + Identity projectViewer = Identity.projectViewer(projectId); + Map> bindingsWithoutPublicRead = + ImmutableMap.of( + StorageRoles.legacyBucketOwner(), + new HashSet<>(Arrays.asList(projectOwner, projectEditor)), + StorageRoles.legacyBucketReader(), + (Set) new HashSet<>(Collections.singleton(projectViewer))); + Map> bindingsWithPublicRead = + ImmutableMap.of( + StorageRoles.legacyBucketOwner(), + new HashSet<>(Arrays.asList(projectOwner, projectEditor)), + StorageRoles.legacyBucketReader(), + new HashSet<>(Collections.singleton(projectViewer)), + StorageRoles.legacyObjectReader(), + (Set) new HashSet<>(Collections.singleton(Identity.allUsers()))); + + // Validate getting policy. + Policy currentPolicy = storage.getIamPolicy(BUCKET_REQUESTER_PAYS, bucketOptions); + assertEquals(bindingsWithoutPublicRead, currentPolicy.getBindings()); + + // Validate updating policy. + Policy updatedPolicy = + storage.setIamPolicy( + BUCKET_REQUESTER_PAYS, + currentPolicy + .toBuilder() + .addIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers()) + .build(), + bucketOptions); + assertEquals(bindingsWithPublicRead, updatedPolicy.getBindings()); + Policy revertedPolicy = + storage.setIamPolicy( + BUCKET_REQUESTER_PAYS, + updatedPolicy + .toBuilder() + .removeIdentity(StorageRoles.legacyObjectReader(), Identity.allUsers()) + .build(), + bucketOptions); + assertEquals(bindingsWithoutPublicRead, revertedPolicy.getBindings()); + + // Validate testing permissions. + List expectedPermissions = ImmutableList.of(true, true); + assertEquals( + expectedPermissions, + storage.testIamPermissions( + BUCKET_REQUESTER_PAYS, + ImmutableList.of("storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"), + bucketOptions)); + Bucket bucketFalse = + storage.update( + bucketTrue.toBuilder().setRequesterPays(false).build(), + Storage.BucketTargetOption.userProject(projectId)); + assertFalse(bucketFalse.requesterPays()); } @Test @@ -2709,9 +2784,12 @@ public void testUpdateBucketLabel() { @Test public void testUpdateBucketRequesterPays() { + unsetRequesterPays(); Bucket remoteBucket = - storage.get(BUCKET, Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING)); - assertFalse(remoteBucket.requesterPays()); + storage.get( + BUCKET_REQUESTER_PAYS, + Storage.BucketGetOption.fields(BucketField.ID, BucketField.BILLING)); + assertTrue(remoteBucket.requesterPays() == null || !remoteBucket.requesterPays()); remoteBucket = remoteBucket.toBuilder().setRequesterPays(true).build(); Bucket updatedBucket = storage.update(remoteBucket); assertTrue(updatedBucket.requesterPays()); @@ -2722,7 +2800,8 @@ public void testUpdateBucketRequesterPays() { Blob remoteBlob = updatedBucket.create(blobName, BLOB_BYTE_CONTENT, option); assertNotNull(remoteBlob); byte[] readBytes = - storage.readAllBytes(BUCKET, blobName, Storage.BlobSourceOption.userProject(projectId)); + storage.readAllBytes( + BUCKET_REQUESTER_PAYS, blobName, Storage.BlobSourceOption.userProject(projectId)); assertArrayEquals(BLOB_BYTE_CONTENT, readBytes); remoteBucket = remoteBucket.toBuilder().setRequesterPays(false).build(); updatedBucket = storage.update(remoteBucket, Storage.BucketTargetOption.userProject(projectId)); @@ -2968,7 +3047,7 @@ public void testAttemptDeletionObjectTemporaryHold() { } catch (StorageException ex) { // expected } finally { - remoteBlob.toBuilder().setEventBasedHold(false).build().update(); + remoteBlob.toBuilder().setTemporaryHold(false).build().update(); } } @@ -3363,4 +3442,129 @@ public void testUploadWithEncryption() throws Exception { byte[] readBytes = blob.getContent(Blob.BlobSourceOption.decryptionKey(KEY)); assertArrayEquals(BLOB_BYTE_CONTENT, readBytes); } + + private Blob createBlob(String method, BlobInfo blobInfo, boolean detectType) throws IOException { + switch (method) { + case "create": + return detectType + ? storage.create(blobInfo, Storage.BlobTargetOption.detectContentType()) + : storage.create(blobInfo); + case "createFrom": + InputStream inputStream = new ByteArrayInputStream(BLOB_BYTE_CONTENT); + return detectType + ? storage.createFrom(blobInfo, inputStream, Storage.BlobWriteOption.detectContentType()) + : storage.createFrom(blobInfo, inputStream); + case "writer": + if (detectType) { + storage.writer(blobInfo, Storage.BlobWriteOption.detectContentType()).close(); + } else { + storage.writer(blobInfo).close(); + } + return storage.get(BlobId.of(blobInfo.getBucket(), blobInfo.getName())); + default: + throw new IllegalArgumentException("Unknown method " + method); + } + } + + private void testAutoContentType(String method) throws IOException { + String[] names = {"file1.txt", "dir with spaces/Pic.Jpg", "no_extension"}; + String[] types = {"text/plain", "image/jpeg", "application/octet-stream"}; + for (int i = 0; i < names.length; i++) { + BlobId blobId = BlobId.of(BUCKET, names[i]); + BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build(); + Blob blob_true = createBlob(method, blobInfo, true); + assertEquals(types[i], blob_true.getContentType()); + + Blob blob_false = createBlob(method, blobInfo, false); + assertEquals("application/octet-stream", blob_false.getContentType()); + } + String customType = "custom/type"; + BlobId blobId = BlobId.of(BUCKET, names[0]); + BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType(customType).build(); + Blob blob = createBlob(method, blobInfo, true); + assertEquals(customType, blob.getContentType()); + } + + @Test + public void testAutoContentTypeCreate() throws IOException { + testAutoContentType("create"); + } + + @Test + public void testAutoContentTypeCreateFrom() throws IOException { + testAutoContentType("createFrom"); + } + + @Test + public void testAutoContentTypeWriter() throws IOException { + testAutoContentType("writer"); + } + + @Test + public void testRemoveBucketCORS() throws ExecutionException, InterruptedException { + String bucketName = RemoteStorageHelper.generateBucketName(); + List origins = ImmutableList.of(Cors.Origin.of("http://cloud.google.com")); + List httpMethods = ImmutableList.of(HttpMethod.GET); + List responseHeaders = ImmutableList.of("Content-Type"); + try { + Cors cors = + Cors.newBuilder() + .setOrigins(origins) + .setMethods(httpMethods) + .setResponseHeaders(responseHeaders) + .setMaxAgeSeconds(100) + .build(); + storage.create(BucketInfo.newBuilder(bucketName).setCors(ImmutableList.of(cors)).build()); + + // case-1 : Cors are set and field selector is selected then returns not-null. + Bucket remoteBucket = + storage.get(bucketName, Storage.BucketGetOption.fields(BucketField.CORS)); + assertThat(remoteBucket.getCors()).isNotNull(); + assertThat(remoteBucket.getCors().get(0).getMaxAgeSeconds()).isEqualTo(100); + assertThat(remoteBucket.getCors().get(0).getMethods()).isEqualTo(httpMethods); + assertThat(remoteBucket.getCors().get(0).getOrigins()).isEqualTo(origins); + assertThat(remoteBucket.getCors().get(0).getResponseHeaders()).isEqualTo(responseHeaders); + + // case-2 : Cors are set but field selector isn't selected then returns not-null. + remoteBucket = storage.get(bucketName); + assertThat(remoteBucket.getCors()).isNotNull(); + + // Remove CORS configuration from the bucket. + Bucket updatedBucket = remoteBucket.toBuilder().setCors(null).build().update(); + assertThat(updatedBucket.getCors()).isNull(); + + // case-3 : Cors are not set and field selector is selected then returns null. + updatedBucket = storage.get(bucketName, Storage.BucketGetOption.fields(BucketField.CORS)); + assertThat(updatedBucket.getCors()).isNull(); + + // case-4 : Cors are not set and field selector isn't selected then returns null. + updatedBucket = storage.get(bucketName); + assertThat(updatedBucket.getCors()).isNull(); + + } finally { + RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS); + } + } + + @Test + public void testBucketUpdateTime() throws ExecutionException, InterruptedException { + String bucketName = RemoteStorageHelper.generateBucketName(); + BucketInfo bucketInfo = + BucketInfo.newBuilder(bucketName).setLocation("us").setVersioningEnabled(true).build(); + try { + Bucket bucket = storage.create(bucketInfo); + assertThat(bucket).isNotNull(); + assertThat(bucket.versioningEnabled()).isTrue(); + assertThat(bucket.getCreateTime()).isNotNull(); + assertThat(bucket.getUpdateTime()).isEqualTo(bucket.getCreateTime()); + + Bucket updatedBucket = bucket.toBuilder().setVersioningEnabled(false).build().update(); + assertThat(updatedBucket.versioningEnabled()).isFalse(); + assertThat(updatedBucket.getUpdateTime()).isNotNull(); + assertThat(updatedBucket.getCreateTime()).isEqualTo(bucket.getCreateTime()); + assertThat(updatedBucket.getUpdateTime()).isGreaterThan(bucket.getCreateTime()); + } finally { + RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS); + } + } } diff --git a/pom.xml b/pom.xml index 40d8e2968..80c13fb10 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-storage-parent pom - 1.111.2 + 1.112.0 Storage Parent https://github.com/googleapis/java-storage @@ -14,7 +14,7 @@ com.google.cloud google-cloud-shared-config - 0.9.0 + 0.9.2 @@ -70,7 +70,7 @@ com.google.cloud google-cloud-shared-dependencies - 0.8.3 + 0.8.6 pom import @@ -78,7 +78,7 @@ com.google.apis google-api-services-storage - v1-rev20200611-1.30.9 + v1-rev20200727-1.30.10 org.easymock diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index cbe71fe42..4dad481d1 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -29,7 +29,7 @@ com.google.cloud google-cloud-storage - 1.111.1 + 1.111.2 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index cad01e090..f662ee168 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -28,7 +28,7 @@ com.google.cloud google-cloud-storage - 1.111.1 + 1.111.2 diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 320dca505..a2e44bb9c 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -30,7 +30,7 @@ com.google.cloud libraries-bom - 8.0.0 + 9.1.0 pom import diff --git a/synth.metadata b/synth.metadata index ed7f3e53e..c283453cd 100644 --- a/synth.metadata +++ b/synth.metadata @@ -4,15 +4,80 @@ "git": { "name": ".", "remote": "https://github.com/googleapis/java-storage.git", - "sha": "a5773d4c43127e808ee56b7185e58af6c3cdb8a2" + "sha": "8602b81eae95868e184fd4ab290396707bd21a8e" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "4f2c9f752a94042472fc03c5bd9e06e89817d2bd" + "sha": "968465a1cad496e1292ef4584a054a35f756ff94" } } + ], + "generatedFiles": [ + ".github/CODEOWNERS", + ".github/ISSUE_TEMPLATE/bug_report.md", + ".github/ISSUE_TEMPLATE/feature_request.md", + ".github/ISSUE_TEMPLATE/support_request.md", + ".github/PULL_REQUEST_TEMPLATE.md", + ".github/release-please.yml", + ".github/trusted-contribution.yml", + ".github/workflows/ci.yaml", + ".github/workflows/samples.yaml", + ".kokoro/build.bat", + ".kokoro/build.sh", + ".kokoro/coerce_logs.sh", + ".kokoro/common.cfg", + ".kokoro/common.sh", + ".kokoro/continuous/common.cfg", + ".kokoro/continuous/java8.cfg", + ".kokoro/dependencies.sh", + ".kokoro/linkage-monitor.sh", + ".kokoro/nightly/common.cfg", + ".kokoro/nightly/integration.cfg", + ".kokoro/nightly/java11.cfg", + ".kokoro/nightly/java7.cfg", + ".kokoro/nightly/java8-osx.cfg", + ".kokoro/nightly/java8-win.cfg", + ".kokoro/nightly/java8.cfg", + ".kokoro/nightly/samples.cfg", + ".kokoro/populate-secrets.sh", + ".kokoro/presubmit/clirr.cfg", + ".kokoro/presubmit/common.cfg", + ".kokoro/presubmit/dependencies.cfg", + ".kokoro/presubmit/java11.cfg", + ".kokoro/presubmit/java7.cfg", + ".kokoro/presubmit/java8-osx.cfg", + ".kokoro/presubmit/java8-win.cfg", + ".kokoro/presubmit/java8.cfg", + ".kokoro/presubmit/linkage-monitor.cfg", + ".kokoro/presubmit/lint.cfg", + ".kokoro/presubmit/samples.cfg", + ".kokoro/release/bump_snapshot.cfg", + ".kokoro/release/common.cfg", + ".kokoro/release/common.sh", + ".kokoro/release/drop.cfg", + ".kokoro/release/drop.sh", + ".kokoro/release/promote.cfg", + ".kokoro/release/promote.sh", + ".kokoro/release/publish_javadoc.cfg", + ".kokoro/release/publish_javadoc.sh", + ".kokoro/release/snapshot.cfg", + ".kokoro/release/snapshot.sh", + ".kokoro/release/stage.cfg", + ".kokoro/release/stage.sh", + ".kokoro/trampoline.sh", + "CODE_OF_CONDUCT.md", + "LICENSE", + "README.md", + "codecov.yaml", + "java.header", + "license-checks.xml", + "renovate.json", + "samples/install-without-bom/pom.xml", + "samples/pom.xml", + "samples/snapshot/pom.xml", + "samples/snippets/pom.xml" ] } \ No newline at end of file diff --git a/synth.py b/synth.py index 7e8a9a3dc..23c93c7c2 100644 --- a/synth.py +++ b/synth.py @@ -19,6 +19,7 @@ AUTOSYNTH_MULTIPLE_COMMITS = True java.common_templates(excludes=[ - '.kokoro/presubmit/integration.cfg' + '.kokoro/presubmit/integration.cfg', + 'CONTRIBUTING.md' ]) diff --git a/versions.txt b/versions.txt index 2eee41669..4e7fd1db9 100644 --- a/versions.txt +++ b/versions.txt @@ -1,4 +1,4 @@ # Format: # module:released-version:current-version -google-cloud-storage:1.111.2:1.111.2 \ No newline at end of file +google-cloud-storage:1.112.0:1.112.0 \ No newline at end of file