diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6de08b5..6906351 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build +name: Main Branch Build on: push: branches: diff --git a/local-s3-core/src/main/java/com/robothy/s3/core/service/manager/InMemoryLocalS3Manager.java b/local-s3-core/src/main/java/com/robothy/s3/core/service/manager/InMemoryLocalS3Manager.java index 406f7d2..9159845 100644 --- a/local-s3-core/src/main/java/com/robothy/s3/core/service/manager/InMemoryLocalS3Manager.java +++ b/local-s3-core/src/main/java/com/robothy/s3/core/service/manager/InMemoryLocalS3Manager.java @@ -33,7 +33,7 @@ class InMemoryLocalS3Manager implements LocalS3Manager { * @param initialDataPath initial data path. */ InMemoryLocalS3Manager(Path initialDataPath, boolean enableInitialDataCache) { - if (Objects.isNull(initialDataPath)) { + if (Objects.isNull(initialDataPath) || !Files.exists(initialDataPath)) { this.storage = Storage.createInMemory(); this.s3Metadata = new LocalS3Metadata(); } else { diff --git a/local-s3-core/src/main/resources/META-INF/native-image/io.github.robothy/local-s3-core/proxy-config.json b/local-s3-core/src/main/resources/META-INF/native-image/io.github.robothy/local-s3-core/proxy-config.json new file mode 100644 index 0000000..803aaa2 --- /dev/null +++ b/local-s3-core/src/main/resources/META-INF/native-image/io.github.robothy/local-s3-core/proxy-config.json @@ -0,0 +1,8 @@ +[ + { + "interfaces":["com.robothy.s3.core.service.BucketService"] + }, + { + "interfaces":["com.robothy.s3.core.service.ObjectService"] + } +] diff --git a/local-s3-core/src/test/java/com/robothy/s3/core/service/DeleteObjectsServiceTest.java b/local-s3-core/src/test/java/com/robothy/s3/core/service/DeleteObjectsServiceTest.java index c860bb7..9ed35bf 100644 --- a/local-s3-core/src/test/java/com/robothy/s3/core/service/DeleteObjectsServiceTest.java +++ b/local-s3-core/src/test/java/com/robothy/s3/core/service/DeleteObjectsServiceTest.java @@ -81,8 +81,8 @@ void testDeleteObjectsFromUnVersionedBucket(BucketService bucketService, ObjectS objectService.putObject(bucketName, "a.txt", PutObjectOptions.builder() .content(new ByteArrayInputStream("Hello".getBytes())) - .contentType("plain.text") - .size(5) + .contentType("plain.text") + .size(5) .build()); List results1 = objectService.deleteObjects(bucketName, new DeleteObjectsRequest(List.of( new ObjectIdentifier("a.txt", null) diff --git a/local-s3-docker/GraalVMNativeImage.dockerfile b/local-s3-docker/GraalVMNativeImage.dockerfile new file mode 100644 index 0000000..2d07898 --- /dev/null +++ b/local-s3-docker/GraalVMNativeImage.dockerfile @@ -0,0 +1,11 @@ +FROM frolvlad/alpine-glibc:glibc-2.34 + +MAINTAINER Fuxiang Luo + +WORKDIR /app + +COPY build/bin/s3 /app/s3 + +EXPOSE 80 + +CMD exec ./s3 \ No newline at end of file diff --git a/local-s3-docker/README.md b/local-s3-docker/README.md new file mode 100644 index 0000000..c6bc003 --- /dev/null +++ b/local-s3-docker/README.md @@ -0,0 +1,54 @@ +## LocalS3 Docker Image + +LocalS3 provides two types of Docker images: `local-s3` and `local-s3-native`. The executable in `local-s3` is a Java application that runs on Java 17, +while the executable in `local-s3-native` is build using GraalVM. The `local-s3-native` image is much smaller than the `local-s3` image. + +### Gradle tasks + +This module contains several Gradle tasks to build and publish the Docker images. + +### Collect reachability metadata + ++ `buildCollectReachabilityMetadataImage` - Builds the Docker image for collecting reachability metadata. ++ `collectReachabilityMetadata` - Collects reachability metadata for building the GraalVM native image. + +Above tasks are not integrated in the CI pipeline. We need to manually run the `collectReachabilityMetadata` task. +Collected reachability metadata files in `build/graalvm-native-image/reachability-metadata` are used to build the GraalVM native image. + +```mermaid +graph BT; + + collectReachabilityMetadata --> jar; + buildJava17BasedDockerImage --> jar; + + subgraph build Docker images + buildGraalVMNativeImage --> collectReachabilityMetadata; + buildDockerImages --> buildJava17BasedDockerImage + buildGraalVMNativeBasedDockerImage --> buildGraalVMNativeImage + buildDockerImages --> buildGraalVMNativeBasedDockerImage + end + + test --> buildDockerImages + pushJava17BasedDockerImage --> test + pushGraalVMNativeBasedDockerImage --> test + + subgraph test and publish Docker images + + pushLatestDockerImage --> pushJava17BasedDockerImage + pushDockerImages --> pushLatestDockerImage + pushDockerImages --> pushGraalVMNativeBasedDockerImage + end + + release --> pushDockerImages + +``` + + +`collectReachbilityMetadata` - Collects reachability metadata for building the GraalVM native image. + +```mermaid +graph TD; + + + +``` \ No newline at end of file diff --git a/local-s3-docker/build.gradle b/local-s3-docker/build.gradle index b3cce82..7d7cb4d 100644 --- a/local-s3-docker/build.gradle +++ b/local-s3-docker/build.gradle @@ -26,7 +26,74 @@ jar { def os = DefaultNativePlatform.getCurrentOperatingSystem() def executor = os.isWindows() ? Arrays.asList("cmd", "/c") : Arrays.asList("sh", "-c") -task dockerBuild(type: Exec, dependsOn: jar) { +tasks.register('collectReachabilityMetadata', JavaExec) { + group('native-image') + dependsOn('jar') + + outputs.cacheIf { true } + + inputs.file("${buildDir}/libs/s3.jar") + .withPropertyName("jar") + .withPathSensitivity(PathSensitivity.RELATIVE) + + outputs.dir(file("${buildDir}/reachability-metadata")) + .withPropertyName("reachability-metadata") + + mainClass.set('com.robothy.s3.docker.ReachabilityMetadataGenerator') + classpath = sourceSets.test.runtimeClasspath + sourceSets.main.runtimeClasspath +} + +tasks.register('buildGraalVMNativeImage', Exec) { + group('native-image') + dependsOn('collectReachabilityMetadata') + + outputs.cacheIf { true } + inputs.dir(file("${buildDir}/reachability-metadata")) + .withPropertyName("reachability-metadata") + .withPathSensitivity(PathSensitivity.RELATIVE) + inputs.file("${buildDir}/libs/s3.jar") + .withPropertyName("jar") + .withPathSensitivity(PathSensitivity.RELATIVE) + outputs.file("${buildDir}/bin/s3") + .withPropertyName("s3-native-image") + + doFirst { + var cmd = new ArrayList(executor) + var buildCmd = """docker run --rm \ +-v ${project.projectDir}:/project \ +-v ${project.buildDir}/bin:/app \ +ghcr.io/graalvm/native-image:ol9-java17-22.3.0 \ +-jar /project/build/libs/s3.jar \ +--initialize-at-build-time=ch.qos.logback,org.slf4j \ +--install-exit-handlers \ +--class-path /project/build/reachability-metadata +""" + cmd.add(buildCmd) + commandLine(cmd) + } +} + +tasks.register('buildGraalVMNativeBasedDockerImage', Exec) { + group("docker-image") + dependsOn('buildGraalVMNativeImage') + + outputs.cacheIf { true } + + inputs.file("${buildDir}/bin/s3") + .withPropertyName("s3-native-image") + .withPathSensitivity(PathSensitivity.RELATIVE) + + doFirst { + var cmd = new ArrayList(executor) + cmd.add("docker build -t luofuxiang/local-s3:native-${project.version} -f GraalVMNativeImage.dockerfile .") + commandLine(cmd) + } +} + +tasks.register('buildJava17BasedDockerImage', Exec) { + group("docker-image") + dependsOn('jar') + doFirst { var cmd = new ArrayList(executor) cmd.add("docker build -t luofuxiang/local-s3:latest -t luofuxiang/local-s3:${project.version} .") @@ -34,9 +101,18 @@ task dockerBuild(type: Exec, dependsOn: jar) { } } -tasks.test.dependsOn(dockerBuild) -task dockerPush(type: Exec, dependsOn: 'test') { +tasks.register('buildDockerImages') { + group("docker-image") + dependsOn ('buildGraalVMNativeBasedDockerImage', 'buildJava17BasedDockerImage') + description = "Builds the docker images" +} + +tasks.test.dependsOn(buildDockerImages) + +tasks.register('pushJava17BasedDockerImage', Exec) { + dependsOn 'test' + group("docker-image") doFirst { var cmd = new ArrayList(executor) cmd.add("docker push luofuxiang/local-s3:${project.version}") @@ -44,7 +120,19 @@ task dockerPush(type: Exec, dependsOn: 'test') { } } -task dockerPushLatest(type: Exec, dependsOn: 'test') { +tasks.register('pushGraalVMNativeBasedDockerImage', Exec) { + dependsOn 'test' + group("docker-image") + doFirst { + var cmd = new ArrayList(executor) + cmd.add("docker push luofuxiang/local-s3:native-${project.version}") + commandLine(cmd) + } +} + +tasks.register('pushLatestImage', Exec) { + dependsOn 'test' + group("docker-image") doFirst { var cmd = new ArrayList(executor) cmd.add("docker push luofuxiang/local-s3:latest") @@ -53,4 +141,4 @@ task dockerPushLatest(type: Exec, dependsOn: 'test') { } -rootProject.tasks.release.dependsOn(dockerPush, dockerPushLatest) \ No newline at end of file +rootProject.tasks.release.dependsOn(pushLatestImage, pushJava17BasedDockerImage, pushGraalVMNativeBasedDockerImage) \ No newline at end of file diff --git a/local-s3-docker/src/main/java/com/robothy/s3/docker/App.java b/local-s3-docker/src/main/java/com/robothy/s3/docker/App.java index 3adc6f1..ff087b0 100644 --- a/local-s3-docker/src/main/java/com/robothy/s3/docker/App.java +++ b/local-s3-docker/src/main/java/com/robothy/s3/docker/App.java @@ -2,7 +2,6 @@ import com.robothy.s3.rest.LocalS3; import com.robothy.s3.rest.bootstrap.LocalS3Mode; -import java.nio.file.Paths; import java.util.Arrays; import java.util.Optional; import lombok.extern.slf4j.Slf4j; @@ -13,11 +12,11 @@ public class App { private static final String MODE = "MODE"; public static void main(String[] args) { - if (System.getenv(MODE) == null) { + if (getProperty(MODE) == null) { log.info("\"MODE\" is not specified; use the default value \"PERSISTENCE\""); } - final String mode = Optional.ofNullable(System.getenv(MODE)).orElse(LocalS3Mode.PERSISTENCE.name()); + final String mode = Optional.ofNullable(getProperty(MODE)).orElse(LocalS3Mode.PERSISTENCE.name()); if (Arrays.stream(LocalS3Mode.values()).noneMatch(m -> m.name().equalsIgnoreCase(mode))) { log.error("\"{}\" is not a valid mode. Valid values are {}", mode, LocalS3Mode.values()); System.exit(1); @@ -33,4 +32,8 @@ public static void main(String[] args) { .start(); } + private static String getProperty(String name) { + return Optional.ofNullable(System.getenv(name)).orElse(System.getProperty(name)); + } + } diff --git a/local-s3-docker/src/test/java/com/robothy/s3/docker/InMemoryModeTest.java b/local-s3-docker/src/test/java/com/robothy/s3/docker/InMemoryModeTest.java index d7c7630..ec99f82 100644 --- a/local-s3-docker/src/test/java/com/robothy/s3/docker/InMemoryModeTest.java +++ b/local-s3-docker/src/test/java/com/robothy/s3/docker/InMemoryModeTest.java @@ -20,9 +20,11 @@ public class InMemoryModeTest { .withMode(LocalS3Container.Mode.IN_MEMORY) .withRandomHttpPort(); + @Test void testInMemoryMode() { assertTrue(container.isRunning()); + // assertTrue(container.isHealthy()); int port = container.getPort(); AmazonS3 s3 = AmazonS3ClientBuilder.standard() .enablePathStyleAccess() diff --git a/local-s3-docker/src/test/java/com/robothy/s3/docker/ReachabilityMetadataGenerator.java b/local-s3-docker/src/test/java/com/robothy/s3/docker/ReachabilityMetadataGenerator.java new file mode 100644 index 0000000..147b72a --- /dev/null +++ b/local-s3-docker/src/test/java/com/robothy/s3/docker/ReachabilityMetadataGenerator.java @@ -0,0 +1,200 @@ +package com.robothy.s3.docker; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import com.amazonaws.ClientConfiguration; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.AmazonS3Exception; +import com.amazonaws.services.s3.model.BucketReplicationConfiguration; +import com.amazonaws.services.s3.model.BucketTaggingConfiguration; +import com.amazonaws.services.s3.model.BucketVersioningConfiguration; +import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest; +import com.amazonaws.services.s3.model.DeleteMarkerReplication; +import com.amazonaws.services.s3.model.DeleteObjectsRequest; +import com.amazonaws.services.s3.model.HeadBucketRequest; +import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest; +import com.amazonaws.services.s3.model.InitiateMultipartUploadResult; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PartETag; +import com.amazonaws.services.s3.model.ReplicationDestinationConfig; +import com.amazonaws.services.s3.model.ReplicationRule; +import com.amazonaws.services.s3.model.ServerSideEncryptionByDefault; +import com.amazonaws.services.s3.model.ServerSideEncryptionConfiguration; +import com.amazonaws.services.s3.model.ServerSideEncryptionRule; +import com.amazonaws.services.s3.model.SetBucketEncryptionRequest; +import com.amazonaws.services.s3.model.SetBucketVersioningConfigurationRequest; +import com.amazonaws.services.s3.model.TagSet; +import com.amazonaws.services.s3.model.UploadPartRequest; +import com.github.dockerjava.api.command.StopContainerCmd; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +public class ReachabilityMetadataGenerator { + + public static void main(String[] args) throws IOException { + int port = 38080; + File dataPath = Files.createTempDirectory("local-s3-data").toFile(); + dataPath.deleteOnExit(); + + try (CollectReachabilityMetadataContainer container = new CollectReachabilityMetadataContainer("ol9-java17-22.3.0")) { + + + + container.port(port) + .withFileSystemBind("build/reachability-metadata/META-INF/native-image", "/metadata", BindMode.READ_WRITE) + .withFileSystemBind("build/libs", "/app", BindMode.READ_WRITE) + .withFileSystemBind(dataPath.getAbsolutePath(), "/data", BindMode.READ_WRITE) + .withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("")) + .withCommand( "java -DMODE=PERSISTENCE -agentlib:native-image-agent=config-output-dir=/metadata -jar /app/s3.jar") + .start(); + + AmazonS3 s3 = AmazonS3ClientBuilder.standard() + .withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration("http://localhost:" + port, "local")) + .enablePathStyleAccess() + .withClientConfiguration(new ClientConfiguration() + .withConnectionTimeout(5000) + .withSocketTimeout(5000)) + .build(); + + // Hit all LocalS3 APIs, cover as many classes as possible to generate reachability metadata. + run(s3); + + // Stop the container. + try(StopContainerCmd cmd = container.getDockerClient().stopContainerCmd(container.getContainerId())) { + cmd.exec(); + } + + } + + /*======== Load data from data path. ========*/ + try (CollectReachabilityMetadataContainer container = new CollectReachabilityMetadataContainer("ol9-java17-22.3.0")) { + container.port(port) + .withFileSystemBind("build/reachability-metadata/META-INF/native-image", "/metadata", BindMode.READ_WRITE) + .withFileSystemBind("build/libs", "/app", BindMode.READ_WRITE) + .withFileSystemBind(dataPath.getAbsolutePath(), "/data", BindMode.READ_WRITE) + .withCreateContainerCmdModifier(cmd -> cmd.withEntrypoint("")) + .withCommand( "java -DMODE=IN_MEMORY -agentlib:native-image-agent=config-merge-dir=/metadata -jar /app/s3.jar") + .start(); + + // Stop the container. + try(StopContainerCmd cmd = container.getDockerClient().stopContainerCmd(container.getContainerId())) { + cmd.exec(); + } + } + + } + + /** + * Hit all LocalS3 APIs, cover as many classes as possible. + */ + static void run(AmazonS3 s3) { + String bucketName = "my-bucket"; + s3.createBucket(bucketName); + s3.listBuckets(); + s3.putObject(bucketName, "my-object", "Hello World!"); + s3.getObject(bucketName, "my-object"); + s3.listObjects(bucketName); + s3.deleteObject(bucketName, "my-object"); + s3.setBucketVersioningConfiguration(new SetBucketVersioningConfigurationRequest(bucketName, new BucketVersioningConfiguration("Enabled"))); + + s3.putObject(bucketName, "my-object", "Hello World!"); + s3.deleteObject(bucketName, "my-object"); + s3.putObject(bucketName, "my-object", "Hello World!"); + s3.listVersions(bucketName, "my-object"); + s3.copyObject(bucketName, "my-object", bucketName, "my-object-copy"); + s3.initiateMultipartUpload(new InitiateMultipartUploadRequest(bucketName, "my-object")); + + ObjectMetadata objectMetadata1 = new ObjectMetadata(); + objectMetadata1.setContentType("plain/text"); + InitiateMultipartUploadResult initResult = + s3.initiateMultipartUpload(new InitiateMultipartUploadRequest(bucketName, "my-object", objectMetadata1)); + + UploadPartRequest part1 = new UploadPartRequest() + .withBucketName(bucketName) + .withKey("my-object") + .withUploadId(initResult.getUploadId()) + .withPartNumber(1) + .withInputStream(new ByteArrayInputStream("Hello".getBytes())) + .withPartSize(5L) + .withLastPart(true); + + UploadPartRequest part2 = new UploadPartRequest() + .withBucketName(bucketName) + .withKey("my-object") + .withUploadId(initResult.getUploadId()) + .withPartNumber(2) + .withInputStream(new ByteArrayInputStream("World".getBytes())) + .withPartSize(5L) + .withLastPart(true); + + s3.uploadPart(part1); + s3.uploadPart(part2); + + s3.completeMultipartUpload(new CompleteMultipartUploadRequest(bucketName, "my-object", initResult.getUploadId(), List.of( + new PartETag(1, ""), + new PartETag(2, "") + ))); + + SetBucketEncryptionRequest setBucketEncryptionRequest = new SetBucketEncryptionRequest(); + setBucketEncryptionRequest.setBucketName(bucketName); + setBucketEncryptionRequest.setServerSideEncryptionConfiguration(new ServerSideEncryptionConfiguration() + .withRules(new ServerSideEncryptionRule().withBucketKeyEnabled(true) + .withApplyServerSideEncryptionByDefault(new ServerSideEncryptionByDefault() + .withSSEAlgorithm("AES256").withKMSMasterKeyID("arn:aws:kms:us-east-1:1234/5678example")))); + + assertDoesNotThrow(() -> s3.setBucketEncryption(setBucketEncryptionRequest)); + s3.setBucketEncryption(setBucketEncryptionRequest); + s3.getBucketEncryption(bucketName); + s3.deleteBucketEncryption(bucketName); + + s3.setBucketPolicy(bucketName, "policy"); + s3.getBucketPolicy(bucketName); + s3.deleteBucketPolicy(bucketName); + + ReplicationDestinationConfig destinationConfig = + new ReplicationDestinationConfig().withBucketARN("arn:aws:s3:::exampletargetbucket"); + s3.setBucketReplicationConfiguration(bucketName, new BucketReplicationConfiguration() + .addRule("1", new ReplicationRule().withDestinationConfig(destinationConfig) + .withPriority(1).withDeleteMarkerReplication(new DeleteMarkerReplication().withStatus("Disabled")))); + s3.getBucketReplicationConfiguration(bucketName); + s3.deleteBucketReplicationConfiguration(bucketName); + + s3.setBucketTaggingConfiguration(bucketName, new BucketTaggingConfiguration(List.of(new TagSet(Map.of("key", "value"))))); + s3.getBucketTaggingConfiguration(bucketName); + s3.deleteBucketTaggingConfiguration(bucketName); + + s3.headBucket(new HeadBucketRequest(bucketName)); + s3.getObjectMetadata(bucketName, "my-object"); + + s3.deleteObjects(new DeleteObjectsRequest(bucketName).withKeys("my-object")); + + assertThrows(AmazonS3Exception.class, () -> s3.deleteBucket("not-exist-bucket")); + } + + /** + * Collect reachability metadata container. + */ + static class CollectReachabilityMetadataContainer extends GenericContainer { + CollectReachabilityMetadataContainer(String tag) { + super(DockerImageName.parse("ghcr.io/graalvm/native-image").withTag(tag)); + this.waitingFor(Wait.forLogMessage("^.{1,}LocalS3 started.\n$", 1)); + } + + CollectReachabilityMetadataContainer port(int port) { + super.addFixedExposedPort(port, 80); + return this; + } + + } + +} diff --git a/local-s3-interationtest/src/test/java/com/robothy/s3/test/BucketIntegrationTest.java b/local-s3-interationtest/src/test/java/com/robothy/s3/test/BucketIntegrationTest.java index 8c724e5..aab7496 100644 --- a/local-s3-interationtest/src/test/java/com/robothy/s3/test/BucketIntegrationTest.java +++ b/local-s3-interationtest/src/test/java/com/robothy/s3/test/BucketIntegrationTest.java @@ -211,19 +211,22 @@ void testBucketEncryption(AmazonS3 s3) { @Test @LocalS3 void testListBuckets(AmazonS3 s3) { + List buckets = s3.listBuckets(); + assertEquals(0, buckets.size()); + s3.createBucket("test-bucket1"); s3.createBucket("test-bucket2"); - List buckets = s3.listBuckets(); - assertEquals(2, buckets.size()); - assertEquals("test-bucket1", buckets.get(0).getName()); - assertTrue(buckets.get(0).getCreationDate().before(new Date())); - assertEquals("test-bucket2", buckets.get(1).getName()); - assertTrue(buckets.get(1).getCreationDate().before(new Date())); + List buckets1 = s3.listBuckets(); + assertEquals(2, buckets1.size()); + assertEquals("test-bucket1", buckets1.get(0).getName()); + assertTrue(buckets1.get(0).getCreationDate().before(new Date())); + assertEquals("test-bucket2", buckets1.get(1).getName()); + assertTrue(buckets1.get(1).getCreationDate().before(new Date())); s3.deleteBucket("test-bucket1"); - List buckets1 = s3.listBuckets(); - assertEquals(1, buckets1.size()); - assertEquals("test-bucket2", buckets1.get(0).getName()); + List buckets2 = s3.listBuckets(); + assertEquals(1, buckets2.size()); + assertEquals("test-bucket2", buckets2.get(0).getName()); } } diff --git a/local-s3-interationtest/src/test/java/com/robothy/s3/test/ObjectIntegrationTest.java b/local-s3-interationtest/src/test/java/com/robothy/s3/test/ObjectIntegrationTest.java index 9fcf88c..c15e781 100644 --- a/local-s3-interationtest/src/test/java/com/robothy/s3/test/ObjectIntegrationTest.java +++ b/local-s3-interationtest/src/test/java/com/robothy/s3/test/ObjectIntegrationTest.java @@ -289,7 +289,7 @@ void testCopyObject(AmazonS3 s3) throws IOException { PutObjectResult putObjectResult1 = s3.putObject(bucket2, key2, text2); CopyObjectResult copyObjectResult3 = s3.copyObject(bucket2, key2, bucket1, key1); assertEquals("null", copyObjectResult3.getVersionId()); - assertTrue(copyObjectResult3.getLastModifiedDate().before(new Date())); + assertTrue(copyObjectResult3.getLastModifiedDate().compareTo(new Date()) <= 0); S3Object object3 = s3.getObject(bucket1, key1); assertEquals(text2.length(), object3.getObjectMetadata().getContentLength()); assertEquals(text2, new String(object3.getObjectContent().readAllBytes())); diff --git a/settings.gradle b/settings.gradle index f96e669..e3ee7e3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,7 @@ pluginManagement { repositories { mavenLocal() + mavenCentral() // maven { // url = 'https://maven.aliyun.com/repository/gradle-plugin'