Convert storage-aws tests to Java (#484)
* storage-aws.ListerTest: convert to Java * storage-aws: convert to Java
This commit is contained in:
parent
5661c9e7da
commit
78b1bcf6ac
26 changed files with 510 additions and 602 deletions
Binary file not shown.
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 193 KiB |
|
@ -1,6 +1,7 @@
|
||||||
package net.kemitix.thorp.domain;
|
package net.kemitix.thorp.domain;
|
||||||
|
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ public class StorageEvent {
|
||||||
public static class DoNothingEvent extends StorageEvent {
|
public static class DoNothingEvent extends StorageEvent {
|
||||||
public final RemoteKey remoteKey;
|
public final RemoteKey remoteKey;
|
||||||
}
|
}
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public static class CopyEvent extends StorageEvent {
|
public static class CopyEvent extends StorageEvent {
|
||||||
public final RemoteKey sourceKey;
|
public final RemoteKey sourceKey;
|
||||||
|
@ -37,6 +39,7 @@ public class StorageEvent {
|
||||||
public final RemoteKey remoteKey;
|
public final RemoteKey remoteKey;
|
||||||
public final MD5Hash md5Hash;
|
public final MD5Hash md5Hash;
|
||||||
}
|
}
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public static class DeleteEvent extends StorageEvent {
|
public static class DeleteEvent extends StorageEvent {
|
||||||
public final RemoteKey remoteKey;
|
public final RemoteKey remoteKey;
|
||||||
|
|
|
@ -13,7 +13,7 @@ import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
public class MD5HashGenerator implements HashGenerator {
|
public class MD5HashGenerator implements HashGenerator {
|
||||||
private static final int maxBufferSize = 8048;
|
private static final int maxBufferSize = 8192;
|
||||||
private static final byte[] defaultBuffer = new byte[maxBufferSize];
|
private static final byte[] defaultBuffer = new byte[maxBufferSize];
|
||||||
public static String hex(byte[] in) throws NoSuchAlgorithmException {
|
public static String hex(byte[] in) throws NoSuchAlgorithmException {
|
||||||
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
||||||
|
|
|
@ -2,6 +2,7 @@ package net.kemitix.thorp.lib
|
||||||
|
|
||||||
import net.kemitix.thorp.config.Configuration
|
import net.kemitix.thorp.config.Configuration
|
||||||
import net.kemitix.thorp.domain.Action.{ToCopy, ToDelete, ToUpload}
|
import net.kemitix.thorp.domain.Action.{ToCopy, ToDelete, ToUpload}
|
||||||
|
import net.kemitix.thorp.domain.StorageEvent.ActionSummary
|
||||||
import net.kemitix.thorp.domain._
|
import net.kemitix.thorp.domain._
|
||||||
import net.kemitix.thorp.storage.Storage
|
import net.kemitix.thorp.storage.Storage
|
||||||
import net.kemitix.thorp.uishell.{UIEvent, UploadEventListener}
|
import net.kemitix.thorp.uishell.{UIEvent, UploadEventListener}
|
||||||
|
@ -35,7 +36,16 @@ trait UnversionedMirrorArchive extends ThorpArchive {
|
||||||
.copy(bucket, sourceKey, hash, targetKey)
|
.copy(bucket, sourceKey, hash, targetKey)
|
||||||
case toDelete: ToDelete =>
|
case toDelete: ToDelete =>
|
||||||
val remoteKey = toDelete.remoteKey
|
val remoteKey = toDelete.remoteKey
|
||||||
Storage.getInstance().delete(bucket, remoteKey)
|
try {
|
||||||
|
Storage.getInstance().delete(bucket, remoteKey)
|
||||||
|
} catch {
|
||||||
|
case e: Throwable =>
|
||||||
|
StorageEvent.errorEvent(
|
||||||
|
ActionSummary.delete(remoteKey.key()),
|
||||||
|
remoteKey,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
case doNothing: Action.DoNothing =>
|
case doNothing: Action.DoNothing =>
|
||||||
val remoteKey = doNothing.remoteKey
|
val remoteKey = doNothing.remoteKey
|
||||||
StorageEvent.doNothingEvent(remoteKey)
|
StorageEvent.doNothingEvent(remoteKey)
|
||||||
|
|
|
@ -48,11 +48,10 @@
|
||||||
<artifactId>assertj-core</artifactId>
|
<artifactId>assertj-core</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- scala -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.scala-lang</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
<artifactId>scala-library</artifactId>
|
<artifactId>mockito-junit-jupiter</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- aws-sdk -->
|
<!-- aws-sdk -->
|
||||||
|
@ -95,27 +94,5 @@
|
||||||
<groupId>commons-logging</groupId>
|
<groupId>commons-logging</groupId>
|
||||||
<version>1.2</version>
|
<version>1.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- scala - testing -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.scalatest</groupId>
|
|
||||||
<artifactId>scalatest_2.13</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.scalamock</groupId>
|
|
||||||
<artifactId>scalamock_2.13</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>net.alchim31.maven</groupId>
|
|
||||||
<artifactId>scala-maven-plugin</artifactId>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package net.kemitix.thorp.storage.aws;
|
package net.kemitix.thorp.storage.aws;
|
||||||
|
|
||||||
|
import com.amazonaws.SdkClientException;
|
||||||
import com.amazonaws.services.s3.model.CopyObjectRequest;
|
import com.amazonaws.services.s3.model.CopyObjectRequest;
|
||||||
import net.kemitix.thorp.domain.Bucket;
|
import net.kemitix.thorp.domain.Bucket;
|
||||||
import net.kemitix.thorp.domain.MD5Hash;
|
import net.kemitix.thorp.domain.MD5Hash;
|
||||||
|
@ -24,16 +25,19 @@ public interface S3Copier {
|
||||||
return request -> {
|
return request -> {
|
||||||
RemoteKey sourceKey = RemoteKey.create(request.getSourceKey());
|
RemoteKey sourceKey = RemoteKey.create(request.getSourceKey());
|
||||||
RemoteKey targetKey = RemoteKey.create(request.getDestinationKey());
|
RemoteKey targetKey = RemoteKey.create(request.getDestinationKey());
|
||||||
return client.copyObject(request)
|
try {
|
||||||
.map(success -> StorageEvent.copyEvent(sourceKey, targetKey))
|
return client.copyObject(request)
|
||||||
.orElseGet(() -> errorEvent(sourceKey, targetKey));
|
.map(success -> StorageEvent.copyEvent(sourceKey, targetKey))
|
||||||
|
.orElseGet(() -> StorageEvent.errorEvent(
|
||||||
|
actionSummary(sourceKey, targetKey),
|
||||||
|
targetKey, S3Exception.hashError()));
|
||||||
|
} catch (SdkClientException e) {
|
||||||
|
return StorageEvent.errorEvent(
|
||||||
|
actionSummary(sourceKey, targetKey), targetKey, e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static StorageEvent.ErrorEvent errorEvent(RemoteKey sourceKey, RemoteKey targetKey) {
|
|
||||||
return StorageEvent.errorEvent(actionSummary(sourceKey, targetKey), targetKey, S3Exception.hashError());
|
|
||||||
}
|
|
||||||
|
|
||||||
static StorageEvent.ActionSummary.Copy actionSummary(RemoteKey sourceKey, RemoteKey targetKey) {
|
static StorageEvent.ActionSummary.Copy actionSummary(RemoteKey sourceKey, RemoteKey targetKey) {
|
||||||
return StorageEvent.ActionSummary.copy(
|
return StorageEvent.ActionSummary.copy(
|
||||||
String.format("%s => %s", sourceKey.key(), targetKey.key()));
|
String.format("%s => %s", sourceKey.key(), targetKey.key()));
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package net.kemitix.thorp.storage.aws;
|
package net.kemitix.thorp.storage.aws;
|
||||||
|
|
||||||
|
import com.amazonaws.SdkClientException;
|
||||||
import com.amazonaws.services.s3.model.DeleteObjectRequest;
|
import com.amazonaws.services.s3.model.DeleteObjectRequest;
|
||||||
import net.kemitix.thorp.domain.Bucket;
|
import net.kemitix.thorp.domain.Bucket;
|
||||||
import net.kemitix.thorp.domain.RemoteKey;
|
import net.kemitix.thorp.domain.RemoteKey;
|
||||||
|
@ -13,9 +14,15 @@ public interface S3Deleter {
|
||||||
}
|
}
|
||||||
static Function<DeleteObjectRequest, StorageEvent> deleter(AmazonS3Client client) {
|
static Function<DeleteObjectRequest, StorageEvent> deleter(AmazonS3Client client) {
|
||||||
return request -> {
|
return request -> {
|
||||||
client.deleteObject(request);
|
|
||||||
RemoteKey remoteKey = RemoteKey.create(request.getKey());
|
RemoteKey remoteKey = RemoteKey.create(request.getKey());
|
||||||
return StorageEvent.deleteEvent(remoteKey);
|
try {
|
||||||
|
client.deleteObject(request);
|
||||||
|
return StorageEvent.deleteEvent(remoteKey);
|
||||||
|
} catch (SdkClientException e) {
|
||||||
|
return StorageEvent.errorEvent(
|
||||||
|
StorageEvent.ActionSummary.delete(remoteKey.key()),
|
||||||
|
remoteKey, e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,15 +7,18 @@ import net.kemitix.thorp.domain.HashGenerator;
|
||||||
import net.kemitix.thorp.domain.HashType;
|
import net.kemitix.thorp.domain.HashType;
|
||||||
import net.kemitix.thorp.domain.Hashes;
|
import net.kemitix.thorp.domain.Hashes;
|
||||||
import net.kemitix.thorp.domain.MD5Hash;
|
import net.kemitix.thorp.domain.MD5Hash;
|
||||||
|
import net.kemitix.thorp.filesystem.MD5HashGenerator;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.LongStream;
|
import java.util.stream.LongStream;
|
||||||
|
|
||||||
|
import static net.kemitix.thorp.filesystem.MD5HashGenerator.md5FileChunk;
|
||||||
|
|
||||||
public class S3ETagGenerator implements HashGenerator {
|
public class S3ETagGenerator implements HashGenerator {
|
||||||
@Deprecated // Use hashFile
|
@Deprecated // Use hashFile
|
||||||
public String eTag(Path path) throws IOException, NoSuchAlgorithmException {
|
public String eTag(Path path) throws IOException, NoSuchAlgorithmException {
|
||||||
|
@ -43,7 +46,7 @@ public class S3ETagGenerator implements HashGenerator {
|
||||||
|
|
||||||
public List<Long> offsets(long totalFileSizeBytes, long optimalPartSize) {
|
public List<Long> offsets(long totalFileSizeBytes, long optimalPartSize) {
|
||||||
return LongStream
|
return LongStream
|
||||||
.range(0, totalFileSizeBytes / optimalPartSize)
|
.rangeClosed(0, totalFileSizeBytes / optimalPartSize)
|
||||||
.mapToObj(part -> part * optimalPartSize)
|
.mapToObj(part -> part * optimalPartSize)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
@ -63,12 +66,11 @@ public class S3ETagGenerator implements HashGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String eTagHex(Path path, long partSize, long parts) throws IOException, NoSuchAlgorithmException {
|
private String eTagHex(Path path, long partSize, long parts) throws IOException, NoSuchAlgorithmException {
|
||||||
HashGenerator hashGenerator = HashGenerator.generatorFor("MD5");
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
|
||||||
for (long i = 0; i < parts ; i++ ){
|
for (long i = 0; i < parts ; i++ ){
|
||||||
md5.update(hashGenerator.hashChunk(path, i, partSize).digest());
|
bos.write(md5FileChunk(path, i * partSize, partSize).digest());
|
||||||
}
|
}
|
||||||
return MD5Hash.digestAsString(md5.digest());
|
return MD5HashGenerator.hex(bos.toByteArray());
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public HashType hashType() {
|
public HashType hashType() {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package net.kemitix.thorp.storage.aws;
|
package net.kemitix.thorp.storage.aws;
|
||||||
|
|
||||||
|
import com.amazonaws.AmazonServiceException;
|
||||||
|
import com.amazonaws.SdkClientException;
|
||||||
import com.amazonaws.services.s3.model.ListObjectsV2Request;
|
import com.amazonaws.services.s3.model.ListObjectsV2Request;
|
||||||
import com.amazonaws.services.s3.model.ListObjectsV2Result;
|
import com.amazonaws.services.s3.model.ListObjectsV2Result;
|
||||||
import com.amazonaws.services.s3.model.S3ObjectSummary;
|
import com.amazonaws.services.s3.model.S3ObjectSummary;
|
||||||
|
@ -71,10 +73,14 @@ public interface S3Lister {
|
||||||
AmazonS3Client client,
|
AmazonS3Client client,
|
||||||
ListObjectsV2Request request
|
ListObjectsV2Request request
|
||||||
) {
|
) {
|
||||||
Batch batch = fetchBatch(client, request);
|
try {
|
||||||
List<S3ObjectSummary> more = fetchMore(client, request, batch.more);
|
Batch batch = fetchBatch(client, request);
|
||||||
batch.summaries.addAll(more);
|
List<S3ObjectSummary> more = fetchMore(client, request, batch.more);
|
||||||
return batch.summaries;
|
batch.summaries.addAll(more);
|
||||||
|
return batch.summaries;
|
||||||
|
} catch (SdkClientException e) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static Optional<String> moreToken(ListObjectsV2Result result) {
|
static Optional<String> moreToken(ListObjectsV2Result result) {
|
||||||
|
|
|
@ -2,7 +2,6 @@ package net.kemitix.thorp.storage.aws;
|
||||||
|
|
||||||
import com.amazonaws.services.s3.model.PutObjectRequest;
|
import com.amazonaws.services.s3.model.PutObjectRequest;
|
||||||
import com.amazonaws.services.s3.transfer.TransferManager;
|
import com.amazonaws.services.s3.transfer.TransferManager;
|
||||||
import com.amazonaws.services.s3.transfer.Upload;
|
|
||||||
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
@ -17,14 +16,9 @@ public interface S3TransferManager {
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public Function<PutObjectRequest, S3Upload> uploader() {
|
public Function<PutObjectRequest, S3Upload> uploader() {
|
||||||
return request -> {
|
return request ->
|
||||||
Upload upload = transferManager.upload(request);
|
S3Upload.inProgress(
|
||||||
try {
|
transferManager.upload(request));
|
||||||
return S3Upload.inProgress(upload);
|
|
||||||
} catch (S3Exception.UploadError error) {
|
|
||||||
return S3Upload.errored(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package net.kemitix.thorp.storage.aws;
|
package net.kemitix.thorp.storage.aws;
|
||||||
|
|
||||||
|
import com.amazonaws.SdkClientException;
|
||||||
import com.amazonaws.services.s3.model.ObjectMetadata;
|
import com.amazonaws.services.s3.model.ObjectMetadata;
|
||||||
import com.amazonaws.services.s3.model.PutObjectRequest;
|
import com.amazonaws.services.s3.model.PutObjectRequest;
|
||||||
import com.amazonaws.services.s3.transfer.model.UploadResult;
|
import com.amazonaws.services.s3.transfer.model.UploadResult;
|
||||||
|
@ -29,13 +30,20 @@ public interface S3Uploader {
|
||||||
S3TransferManager transferManager
|
S3TransferManager transferManager
|
||||||
) {
|
) {
|
||||||
return request -> {
|
return request -> {
|
||||||
UploadResult uploadResult =
|
try {
|
||||||
transferManager.uploader()
|
UploadResult uploadResult =
|
||||||
.apply(request)
|
transferManager.uploader()
|
||||||
.waitForUploadResult();
|
.apply(request)
|
||||||
return StorageEvent.uploadEvent(
|
.waitForUploadResult();
|
||||||
RemoteKey.create(uploadResult.getKey()),
|
return StorageEvent.uploadEvent(
|
||||||
MD5Hash.create(uploadResult.getETag()));
|
RemoteKey.create(uploadResult.getKey()),
|
||||||
|
MD5Hash.create(uploadResult.getETag()));
|
||||||
|
} catch (SdkClientException e) {
|
||||||
|
String key = request.getKey();
|
||||||
|
return StorageEvent.errorEvent(
|
||||||
|
StorageEvent.ActionSummary.upload(key),
|
||||||
|
RemoteKey.create(key), e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package net.kemitix.thorp.storage.aws;
|
||||||
|
|
||||||
|
import com.amazonaws.SdkClientException;
|
||||||
|
import com.amazonaws.services.s3.model.CopyObjectRequest;
|
||||||
|
import com.amazonaws.services.s3.model.CopyObjectResult;
|
||||||
|
import net.kemitix.thorp.domain.Bucket;
|
||||||
|
import net.kemitix.thorp.domain.MD5Hash;
|
||||||
|
import net.kemitix.thorp.domain.RemoteKey;
|
||||||
|
import net.kemitix.thorp.domain.StorageEvent;
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class CopierTest
|
||||||
|
implements WithAssertions {
|
||||||
|
|
||||||
|
private final Bucket bucket = Bucket.named("aBucket");
|
||||||
|
private final RemoteKey sourceKey = RemoteKey.create("sourceKey");
|
||||||
|
private final MD5Hash hash = MD5Hash.create("aHash");
|
||||||
|
private final RemoteKey targetKey = RemoteKey.create("targetKey");
|
||||||
|
private final CopyObjectRequest request = S3Copier.request(bucket, sourceKey, hash, targetKey);
|
||||||
|
|
||||||
|
private final AmazonS3Client amazonS3Client;
|
||||||
|
|
||||||
|
public CopierTest(@Mock AmazonS3Client amazonS3Client) {
|
||||||
|
this.amazonS3Client = amazonS3Client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("copy produces copy event")
|
||||||
|
public void copy_thenCopyEvent() {
|
||||||
|
//given
|
||||||
|
StorageEvent event = StorageEvent.copyEvent(sourceKey, targetKey);
|
||||||
|
given(amazonS3Client.copyObject(request))
|
||||||
|
.willReturn(Optional.of(new CopyObjectResult()));
|
||||||
|
//when
|
||||||
|
StorageEvent result = S3Copier.copier(amazonS3Client).apply(request);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when error copying then return an error storage event")
|
||||||
|
public void whenCopyErrors_thenErrorEvent() {
|
||||||
|
//given
|
||||||
|
doThrow(SdkClientException.class)
|
||||||
|
.when(amazonS3Client)
|
||||||
|
.copyObject(request);
|
||||||
|
//when
|
||||||
|
StorageEvent result = S3Copier.copier(amazonS3Client).apply(request);
|
||||||
|
//then
|
||||||
|
assertThat(result).isInstanceOf(StorageEvent.ErrorEvent.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package net.kemitix.thorp.storage.aws;
|
||||||
|
|
||||||
|
import com.amazonaws.SdkClientException;
|
||||||
|
import com.amazonaws.services.s3.model.DeleteObjectRequest;
|
||||||
|
import net.kemitix.thorp.domain.Bucket;
|
||||||
|
import net.kemitix.thorp.domain.RemoteKey;
|
||||||
|
import net.kemitix.thorp.domain.StorageEvent;
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class DeleterTest
|
||||||
|
implements WithAssertions {
|
||||||
|
|
||||||
|
private final Bucket bucket = Bucket.named("aBucket");
|
||||||
|
private final RemoteKey remoteKey = RemoteKey.create("aRemoteKey");
|
||||||
|
private final DeleteObjectRequest request = S3Deleter.request(bucket, remoteKey);
|
||||||
|
|
||||||
|
private final AmazonS3Client amazonS3Client;
|
||||||
|
|
||||||
|
public DeleterTest(@Mock AmazonS3Client amazonS3Client) {
|
||||||
|
this.amazonS3Client = amazonS3Client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when delete then return delete storage event")
|
||||||
|
public void whenDelete_thenDeleteEvent() {
|
||||||
|
//when
|
||||||
|
StorageEvent result = S3Deleter.deleter(amazonS3Client).apply(request);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(StorageEvent.deleteEvent(remoteKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when error deleting then return an error storage event")
|
||||||
|
public void whenDeleteErrors_thenErrorEvent() {
|
||||||
|
//given
|
||||||
|
doThrow(SdkClientException.class)
|
||||||
|
.when(amazonS3Client)
|
||||||
|
.deleteObject(request);
|
||||||
|
//when
|
||||||
|
StorageEvent result = S3Deleter.deleter(amazonS3Client).apply(request);
|
||||||
|
//then
|
||||||
|
assertThat(result).isInstanceOf(StorageEvent.ErrorEvent.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package net.kemitix.thorp.storage.aws;
|
||||||
|
|
||||||
|
import net.kemitix.thorp.domain.MD5Hash;
|
||||||
|
import net.kemitix.thorp.domain.Tuple;
|
||||||
|
import net.kemitix.thorp.filesystem.Resource;
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ETagGeneratorTest
|
||||||
|
implements WithAssertions {
|
||||||
|
|
||||||
|
public static final String BIG_FILE_ETAG = "f14327c90ad105244c446c498bfe9a7d-2";
|
||||||
|
private final Resource bigFile = Resource.select(this, "big-file");
|
||||||
|
private final long chunkSize = 1200000;
|
||||||
|
private final S3ETagGenerator generator = new S3ETagGenerator();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("creates offsets")
|
||||||
|
public void createsOffsets() {
|
||||||
|
List<Long> offsets = generator.offsets(bigFile.length(), chunkSize);
|
||||||
|
assertThat(offsets).containsExactly(
|
||||||
|
0L, chunkSize, chunkSize * 2, chunkSize * 3, chunkSize * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("generate valid hashes")
|
||||||
|
public void generatesValidHashes() throws IOException, NoSuchAlgorithmException {
|
||||||
|
List<Tuple<Integer, String>> md5Hashes = Arrays.asList(
|
||||||
|
Tuple.create(0, "68b7d37e6578297621e06f01800204f1"),
|
||||||
|
Tuple.create(1, "973475b14a7bda6ad8864a7f9913a947"),
|
||||||
|
Tuple.create(2, "b9adcfc5b103fe2dd5924a5e5e6817f0"),
|
||||||
|
Tuple.create(3, "5bd6e10a99fef100fe7bf5eaa0a42384"),
|
||||||
|
Tuple.create(4, "8a0c1d0778ac8fcf4ca2010eba4711eb"));
|
||||||
|
for (Tuple<Integer, String> t : md5Hashes) {
|
||||||
|
long offset = t.a * chunkSize;
|
||||||
|
MD5Hash md5Hash =
|
||||||
|
generator.hashChunk(bigFile.toPath(), offset, chunkSize);
|
||||||
|
assertThat(md5Hash.getValue()).isEqualTo(t.b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("create eTag for whole file")
|
||||||
|
public void createTagForWholeFile() throws IOException, NoSuchAlgorithmException {
|
||||||
|
String result = generator.hashFile(bigFile.toPath());
|
||||||
|
assertThat(result).isEqualTo(BIG_FILE_ETAG);
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,7 +42,8 @@ public class HashGeneratorTest
|
||||||
//when
|
//when
|
||||||
Hashes result = HashGenerator.hashObject(path);
|
Hashes result = HashGenerator.hashObject(path);
|
||||||
//then
|
//then
|
||||||
assertThat(result.get(HashType.MD5)).contains(MD5HashData.rootHash());
|
assertThat(result.get(HashType.MD5))
|
||||||
|
.contains(MD5Hash.create("a3a6ac11a0eb577b81b3bb5c95cc8a6e"));
|
||||||
}
|
}
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("leaf-file")
|
@DisplayName("leaf-file")
|
||||||
|
@ -52,7 +53,8 @@ public class HashGeneratorTest
|
||||||
//when
|
//when
|
||||||
Hashes result = HashGenerator.hashObject(path);
|
Hashes result = HashGenerator.hashObject(path);
|
||||||
//then
|
//then
|
||||||
assertThat(result.get(HashType.MD5)).contains(MD5HashData.leafHash());
|
assertThat(result.get(HashType.MD5))
|
||||||
|
.contains(MD5Hash.create("208386a650bdec61cfcd7bd8dcb6b542"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path getResource(String s) {
|
private Path getResource(String s) {
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
package net.kemitix.thorp.storage.aws;
|
||||||
|
|
||||||
|
import com.amazonaws.SdkClientException;
|
||||||
|
import com.amazonaws.services.s3.model.ListObjectsV2Request;
|
||||||
|
import com.amazonaws.services.s3.model.ListObjectsV2Result;
|
||||||
|
import com.amazonaws.services.s3.model.S3ObjectSummary;
|
||||||
|
import net.kemitix.thorp.domain.*;
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class ListerTest
|
||||||
|
implements WithAssertions {
|
||||||
|
|
||||||
|
private final Bucket bucket = Bucket.named("aBucket");
|
||||||
|
private final RemoteKey prefix = RemoteKey.create("aRemoteKey");
|
||||||
|
private final ListObjectsV2Request request = S3Lister.request(bucket, prefix);
|
||||||
|
|
||||||
|
private final AmazonS3Client amazonS3Client;
|
||||||
|
|
||||||
|
public ListerTest(@Mock AmazonS3Client amazonS3Client) {
|
||||||
|
this.amazonS3Client = amazonS3Client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("single fetch")
|
||||||
|
public void singleFetch() {
|
||||||
|
//given
|
||||||
|
Date nowDate = new Date();
|
||||||
|
String key = "key";
|
||||||
|
String etag = "etag";
|
||||||
|
Map<MD5Hash, RemoteKey> expectedHashMap =
|
||||||
|
Collections.singletonMap(
|
||||||
|
MD5Hash.create(etag),
|
||||||
|
RemoteKey.create(key)
|
||||||
|
);
|
||||||
|
Map<RemoteKey, MD5Hash> expectedKeyMap =
|
||||||
|
Collections.singletonMap(
|
||||||
|
RemoteKey.create(key),
|
||||||
|
MD5Hash.create(etag)
|
||||||
|
);
|
||||||
|
given(amazonS3Client.listObjects(any()))
|
||||||
|
.willReturn(objectResults(nowDate, key, etag, false));
|
||||||
|
//when
|
||||||
|
RemoteObjects result = S3Lister.lister(amazonS3Client).apply(request);
|
||||||
|
//then
|
||||||
|
assertThat(result.byHash.asMap()).isEqualTo(expectedHashMap);
|
||||||
|
assertThat(result.byKey.asMap()).isEqualTo(expectedKeyMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("two fetches")
|
||||||
|
public void twoFetches() {
|
||||||
|
//given
|
||||||
|
Date nowDate = new Date();
|
||||||
|
String key1 = "key1";
|
||||||
|
String etag1 = "etag1";
|
||||||
|
String key2 = "key2";
|
||||||
|
String etag2 = "etag2";
|
||||||
|
Map<MD5Hash, RemoteKey> expectedHashMap = new HashMap<>();
|
||||||
|
expectedHashMap.put(
|
||||||
|
MD5Hash.create(etag1),
|
||||||
|
RemoteKey.create(key1));
|
||||||
|
expectedHashMap.put(
|
||||||
|
MD5Hash.create(etag2),
|
||||||
|
RemoteKey.create(key2));
|
||||||
|
Map<RemoteKey, MD5Hash> expectedKeyMap = new HashMap<>();
|
||||||
|
expectedKeyMap.put(
|
||||||
|
RemoteKey.create(key1),
|
||||||
|
MD5Hash.create(etag1));
|
||||||
|
expectedKeyMap.put(
|
||||||
|
RemoteKey.create(key2),
|
||||||
|
MD5Hash.create(etag2));
|
||||||
|
given(amazonS3Client.listObjects(any()))
|
||||||
|
.willReturn(objectResults(nowDate, key1, etag1, true))
|
||||||
|
.willReturn(objectResults(nowDate, key2, etag2, false));
|
||||||
|
//when
|
||||||
|
RemoteObjects result = S3Lister.lister(amazonS3Client).apply(request);
|
||||||
|
//then
|
||||||
|
assertThat(result.byHash.asMap()).isEqualTo(expectedHashMap);
|
||||||
|
assertThat(result.byKey.asMap()).isEqualTo(expectedKeyMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when error listing then return an error storage event")
|
||||||
|
public void whenListErrors_thenErrorEvent() {
|
||||||
|
//given
|
||||||
|
doThrow(SdkClientException.class)
|
||||||
|
.when(amazonS3Client)
|
||||||
|
.listObjects(request);
|
||||||
|
//when
|
||||||
|
RemoteObjects result = S3Lister.lister(amazonS3Client).apply(request);
|
||||||
|
//then
|
||||||
|
assertThat(result.byKey.asMap()).isEmpty();
|
||||||
|
assertThat(result.byHash.asMap()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListObjectsV2Result objectResults(
|
||||||
|
Date nowDate,
|
||||||
|
String key,
|
||||||
|
String etag,
|
||||||
|
boolean truncated
|
||||||
|
) {
|
||||||
|
ListObjectsV2Result result = new ListObjectsV2Result();
|
||||||
|
result.getObjectSummaries().add(objectSummary(key, etag, nowDate));
|
||||||
|
result.setNextContinuationToken("next token");
|
||||||
|
result.setTruncated(truncated);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private S3ObjectSummary objectSummary(
|
||||||
|
String key,
|
||||||
|
String etag,
|
||||||
|
Date lastModified
|
||||||
|
) {
|
||||||
|
S3ObjectSummary summary = new S3ObjectSummary();
|
||||||
|
summary.setKey(key);
|
||||||
|
summary.setETag(etag);
|
||||||
|
summary.setLastModified(lastModified);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package net.kemitix.thorp.storage.aws;
|
||||||
|
|
||||||
|
import com.amazonaws.services.s3.model.S3ObjectSummary;
|
||||||
|
import lombok.val;
|
||||||
|
import net.kemitix.thorp.domain.MD5Hash;
|
||||||
|
import net.kemitix.thorp.domain.RemoteKey;
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ObjectsByHashTest
|
||||||
|
implements WithAssertions {
|
||||||
|
private S3ObjectSummary s3object(MD5Hash md5Hash,
|
||||||
|
RemoteKey remoteKey) {
|
||||||
|
S3ObjectSummary summary = new S3ObjectSummary();
|
||||||
|
summary.setETag(md5Hash.hash());
|
||||||
|
summary.setKey(remoteKey.key());
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("grouping s3 objects together by their hash value")
|
||||||
|
public void groupS3ObjectsByHashValue() {
|
||||||
|
//given
|
||||||
|
MD5Hash hash = MD5Hash.create("hash");
|
||||||
|
RemoteKey key1 = RemoteKey.create("key-1");
|
||||||
|
RemoteKey key2 = RemoteKey.create("key-2");
|
||||||
|
S3ObjectSummary o1 = s3object(hash, key1);
|
||||||
|
S3ObjectSummary o2 = s3object(hash, key2);
|
||||||
|
List<S3ObjectSummary> os = Arrays.asList(o1, o2);
|
||||||
|
Map<MD5Hash, RemoteKey> expected = Collections.singletonMap(
|
||||||
|
hash, key2);
|
||||||
|
//when
|
||||||
|
Map<MD5Hash, RemoteKey> result = S3Lister.byHash(os);
|
||||||
|
//then
|
||||||
|
assertThat(result).isEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package net.kemitix.thorp.storage.aws;
|
||||||
|
|
||||||
|
import com.amazonaws.SdkClientException;
|
||||||
|
import com.amazonaws.services.s3.model.PutObjectRequest;
|
||||||
|
import com.amazonaws.services.s3.transfer.TransferManager;
|
||||||
|
import com.amazonaws.services.s3.transfer.Upload;
|
||||||
|
import com.amazonaws.services.s3.transfer.model.UploadResult;
|
||||||
|
import net.kemitix.thorp.domain.HashType;
|
||||||
|
import net.kemitix.thorp.domain.*;
|
||||||
|
import net.kemitix.thorp.filesystem.Resource;
|
||||||
|
import org.assertj.core.api.WithAssertions;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class UploaderTest
|
||||||
|
implements WithAssertions {
|
||||||
|
|
||||||
|
private final File aSource = Resource.select(this, "").toFile();
|
||||||
|
private final File aFile = Resource.select(this, "small-file").toFile();
|
||||||
|
private final MD5Hash aHash = MD5Hash.create("aHash");
|
||||||
|
private final Hashes hashes = Hashes.create(HashType.MD5, aHash);
|
||||||
|
private final RemoteKey remoteKey = RemoteKey.create("aRemoteKey");
|
||||||
|
private final LocalFile localFile =
|
||||||
|
LocalFile.create(aFile, aSource, hashes, remoteKey, aFile.length());
|
||||||
|
private final Bucket bucket = Bucket.named("aBucket");
|
||||||
|
private final PutObjectRequest request =
|
||||||
|
S3Uploader.request(localFile, bucket);
|
||||||
|
|
||||||
|
private final TransferManager transferManager;
|
||||||
|
|
||||||
|
private final S3TransferManager s3TransferManager;
|
||||||
|
|
||||||
|
private final UploadResult uploadResult;
|
||||||
|
private final Upload upload;
|
||||||
|
|
||||||
|
public UploaderTest(@Mock TransferManager transferManager,
|
||||||
|
@Mock Upload upload) {
|
||||||
|
this.transferManager = transferManager;
|
||||||
|
this.upload = upload;
|
||||||
|
s3TransferManager = S3TransferManager.create(transferManager);
|
||||||
|
uploadResult = new UploadResult();
|
||||||
|
uploadResult.setKey(remoteKey.key());
|
||||||
|
uploadResult.setETag(aHash.hash());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when upload then return upload event")
|
||||||
|
public void whenUpload_thenUploadEvent() throws InterruptedException {
|
||||||
|
//given
|
||||||
|
given(transferManager.upload(request))
|
||||||
|
.willReturn(upload);
|
||||||
|
given(upload.waitForUploadResult()).willReturn(uploadResult);
|
||||||
|
//when
|
||||||
|
StorageEvent result = S3Uploader.uploader(s3TransferManager).apply(request);
|
||||||
|
//then
|
||||||
|
assertThat(result).isInstanceOf(StorageEvent.UploadEvent.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("when error uploading then return an error storage event")
|
||||||
|
public void whenUploadErrors_thenErrorEvent() {
|
||||||
|
//given
|
||||||
|
doThrow(SdkClientException.class)
|
||||||
|
.when(transferManager)
|
||||||
|
.upload(request);
|
||||||
|
//when
|
||||||
|
StorageEvent result = S3Uploader.uploader(s3TransferManager).apply(request);
|
||||||
|
//then
|
||||||
|
assertThat(result).isInstanceOf(StorageEvent.ErrorEvent.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,60 +0,0 @@
|
||||||
package net.kemitix.thorp.storage.aws
|
|
||||||
|
|
||||||
import net.kemitix.thorp.storage.Storage
|
|
||||||
import org.scalamock.scalatest.MockFactory
|
|
||||||
|
|
||||||
trait AmazonS3ClientTestFixture extends MockFactory {
|
|
||||||
|
|
||||||
@SuppressWarnings(Array("org.wartremover.warts.PublicInference"))
|
|
||||||
private val manager = stub[S3TransferManager]
|
|
||||||
@SuppressWarnings(Array("org.wartremover.warts.PublicInference"))
|
|
||||||
private val client = stub[AmazonS3Client]
|
|
||||||
val fixture: Fixture = Fixture(client, manager)
|
|
||||||
|
|
||||||
case class Fixture(amazonS3Client: AmazonS3Client,
|
|
||||||
amazonS3TransferManager: S3TransferManager,
|
|
||||||
) {
|
|
||||||
lazy val storageService: Storage = Storage.getInstance()
|
|
||||||
// new Storage.Service {
|
|
||||||
//
|
|
||||||
// private val client = amazonS3Client
|
|
||||||
// private val transferManager = amazonS3TransferManager
|
|
||||||
//
|
|
||||||
// override def listObjects(
|
|
||||||
// bucket: Bucket,
|
|
||||||
// prefix: RemoteKey
|
|
||||||
// ): RIO[Storage, RemoteObjects] =
|
|
||||||
// UIO {
|
|
||||||
// S3Lister.lister(client)(S3Lister.request(bucket, prefix))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override def upload(localFile: LocalFile,
|
|
||||||
// bucket: Bucket,
|
|
||||||
// listenerSettings: UploadEventListener.Settings,
|
|
||||||
// ): UIO[StorageEvent] =
|
|
||||||
// UIO(
|
|
||||||
// S3Uploader
|
|
||||||
// .uploader(transferManager)(S3Uploader.request(localFile, bucket))
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
// override def copy(bucket: Bucket,
|
|
||||||
// sourceKey: RemoteKey,
|
|
||||||
// hash: MD5Hash,
|
|
||||||
// targetKey: RemoteKey): UIO[StorageEvent] =
|
|
||||||
// UIO {
|
|
||||||
// val request = S3Copier.request(bucket, sourceKey, hash, targetKey)
|
|
||||||
// S3Copier.copier(client)(request)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override def delete(bucket: Bucket,
|
|
||||||
// remoteKey: RemoteKey): UIO[StorageEvent] =
|
|
||||||
// UIO(S3Deleter.deleter(client)(S3Deleter.request(bucket, remoteKey)))
|
|
||||||
//
|
|
||||||
// override def shutdown: UIO[StorageEvent] = {
|
|
||||||
// UIO(transferManager.shutdownNow(true)) *> UIO(client.shutdown())
|
|
||||||
// .map(_ => StorageEvent.shutdownEvent())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
package net.kemitix.thorp.storage.aws
|
|
||||||
|
|
||||||
import org.scalatest.FreeSpec
|
|
||||||
|
|
||||||
class CopierTest extends FreeSpec {
|
|
||||||
|
|
||||||
// private val runtime = Runtime(Console.Live, PlatformLive.Default)
|
|
||||||
//
|
|
||||||
// "copier" - {
|
|
||||||
// val bucket = Bucket.named("aBucket")
|
|
||||||
// val sourceKey = RemoteKey.create("sourceKey")
|
|
||||||
// val hash = MD5Hash.create("aHash")
|
|
||||||
// val targetKey = RemoteKey.create("targetKey")
|
|
||||||
// "when source exists" - {
|
|
||||||
// "when source hash matches" - {
|
|
||||||
// "copies from source to target" in {
|
|
||||||
// val event = StorageEvent.copyEvent(sourceKey, targetKey)
|
|
||||||
// val expected = Right(event)
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
// (() => fixture.amazonS3Client.copyObject)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ => Task.succeed(Some(new CopyObjectResult)))
|
|
||||||
// private val result =
|
|
||||||
// invoke(bucket, sourceKey, hash, targetKey, fixture.amazonS3Client)
|
|
||||||
// assertResult(expected)(result)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// "when source hash does not match" - {
|
|
||||||
// "skip the file with an error" in {
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
// (() => fixture.amazonS3Client.copyObject)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ => Task.succeed(None))
|
|
||||||
// private val result =
|
|
||||||
// invoke(bucket, sourceKey, hash, targetKey, fixture.amazonS3Client)
|
|
||||||
// result match {
|
|
||||||
// case right: Right[Throwable, StorageEvent] => {
|
|
||||||
// val e = right.value.asInstanceOf[ErrorEvent].e
|
|
||||||
// e match {
|
|
||||||
// case HashError => assert(true)
|
|
||||||
// case _ => fail(s"Not a HashError: ${e.getMessage}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// case e => fail(s"Not an ErrorQueueEvent: $e")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// "when client throws an exception" - {
|
|
||||||
// "skip the file with an error" in {
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
// private val expectedMessage = "The specified key does not exist"
|
|
||||||
// (() => fixture.amazonS3Client.copyObject)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ => Task.fail(new AmazonS3Exception(expectedMessage)))
|
|
||||||
// private val result =
|
|
||||||
// invoke(bucket, sourceKey, hash, targetKey, fixture.amazonS3Client)
|
|
||||||
// val key = RemoteKey.create("targetKey")
|
|
||||||
// result match {
|
|
||||||
// case right: Right[Throwable, StorageEvent] => {
|
|
||||||
// val e = right.value.asInstanceOf[ErrorEvent].e
|
|
||||||
// e match {
|
|
||||||
// case CopyError(cause) =>
|
|
||||||
// assert(cause.getMessage.startsWith(expectedMessage))
|
|
||||||
// case _ => fail(s"Not a CopyError: ${e.getMessage}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// case e => fail(s"Not an ErrorQueueEvent: ${e}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// def invoke(
|
|
||||||
// bucket: Bucket,
|
|
||||||
// sourceKey: RemoteKey,
|
|
||||||
// hash: MD5Hash,
|
|
||||||
// targetKey: RemoteKey,
|
|
||||||
// amazonS3Client: AmazonS3Client
|
|
||||||
// ) =
|
|
||||||
// runtime.unsafeRunSync {
|
|
||||||
// Copier.copy(amazonS3Client)(
|
|
||||||
// Copier.Request(bucket, sourceKey, hash, targetKey))
|
|
||||||
// }.toEither
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package net.kemitix.thorp.storage.aws
|
|
||||||
|
|
||||||
import org.scalatest.FreeSpec
|
|
||||||
|
|
||||||
class DeleterTest extends FreeSpec {
|
|
||||||
|
|
||||||
// private val runtime = Runtime(Console.Live, PlatformLive.Default)
|
|
||||||
//
|
|
||||||
// "delete" - {
|
|
||||||
// val bucket = Bucket.named("aBucket")
|
|
||||||
// val remoteKey = RemoteKey.create("aRemoteKey")
|
|
||||||
// "when no errors" in {
|
|
||||||
// val expected = Right(StorageEvent.deleteEvent(remoteKey))
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
// (() => fixture.amazonS3Client.deleteObject)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ => UIO.succeed(()))
|
|
||||||
// private val result = invoke(fixture.amazonS3Client)(bucket, remoteKey)
|
|
||||||
// assertResult(expected)(result)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// "when Amazon Service Exception" in {
|
|
||||||
// val exception = new AmazonS3Exception("message")
|
|
||||||
// val expected =
|
|
||||||
// Right(
|
|
||||||
// StorageEvent.errorEvent(ActionSummary.delete(remoteKey.key),
|
|
||||||
// remoteKey,
|
|
||||||
// exception))
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
// (() => fixture.amazonS3Client.deleteObject)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ => Task.fail(exception))
|
|
||||||
// private val result = invoke(fixture.amazonS3Client)(bucket, remoteKey)
|
|
||||||
// assertResult(expected)(result)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// "when Amazon SDK Client Exception" in {
|
|
||||||
// val exception = new SdkClientException("message")
|
|
||||||
// val expected =
|
|
||||||
// Right(
|
|
||||||
// StorageEvent.errorEvent(ActionSummary.delete(remoteKey.key),
|
|
||||||
// remoteKey,
|
|
||||||
// exception))
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
// (() => fixture.amazonS3Client.deleteObject)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ => Task.fail(exception))
|
|
||||||
// private val result = invoke(fixture.amazonS3Client)(bucket, remoteKey)
|
|
||||||
// assertResult(expected)(result)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// def invoke(amazonS3Client: AmazonS3Client.Client)(bucket: Bucket,
|
|
||||||
// remoteKey: RemoteKey) =
|
|
||||||
// runtime.unsafeRunSync {
|
|
||||||
// Deleter.delete(amazonS3Client)(bucket, remoteKey)
|
|
||||||
// }.toEither
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
package net.kemitix.thorp.storage.aws
|
|
||||||
|
|
||||||
import org.scalatest.FreeSpec
|
|
||||||
|
|
||||||
class ListerTest extends FreeSpec {
|
|
||||||
|
|
||||||
// "list" - {
|
|
||||||
// val bucket = Bucket.named("aBucket")
|
|
||||||
// val prefix = RemoteKey.create("aRemoteKey")
|
|
||||||
// "when no errors" - {
|
|
||||||
// "when single fetch required" in {
|
|
||||||
// val nowDate = new Date
|
|
||||||
// val key = "key"
|
|
||||||
// val etag = "etag"
|
|
||||||
// val expectedHashMap = Map(MD5Hash.create(etag) -> RemoteKey.create(key))
|
|
||||||
// val expectedKeyMap = Map(RemoteKey.create(key) -> MD5Hash.create(etag))
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
// (() => fixture.amazonS3Client.listObjectsV2)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ => {
|
|
||||||
// UIO.succeed(objectResults(nowDate, key, etag, truncated = false))
|
|
||||||
// })
|
|
||||||
// private val result = invoke(fixture.amazonS3Client)(bucket, prefix)
|
|
||||||
// private val hashMap =
|
|
||||||
// result.map(_.byHash).map(m => Map.from(m.asMap.asScala))
|
|
||||||
// private val keyMap =
|
|
||||||
// result.map(_.byKey).map(m => Map.from(m.asMap.asScala))
|
|
||||||
// hashMap should be(Right(expectedHashMap))
|
|
||||||
// keyMap should be(Right(expectedKeyMap))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// "when second fetch required" in {
|
|
||||||
// val nowDate = new Date
|
|
||||||
// val key1 = "key1"
|
|
||||||
// val etag1 = "etag1"
|
|
||||||
// val key2 = "key2"
|
|
||||||
// val etag2 = "etag2"
|
|
||||||
// val expectedHashMap = Map(
|
|
||||||
// MD5Hash.create(etag1) -> RemoteKey.create(key1),
|
|
||||||
// MD5Hash.create(etag2) -> RemoteKey.create(key2)
|
|
||||||
// )
|
|
||||||
// val expectedKeyMap = Map(
|
|
||||||
// RemoteKey.create(key1) -> MD5Hash.create(etag1),
|
|
||||||
// RemoteKey.create(key2) -> MD5Hash.create(etag2)
|
|
||||||
// )
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
//
|
|
||||||
// (() => fixture.amazonS3Client.listObjectsV2)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ =>
|
|
||||||
// UIO(objectResults(nowDate, key1, etag1, truncated = true)))
|
|
||||||
// .noMoreThanOnce()
|
|
||||||
//
|
|
||||||
// (() => fixture.amazonS3Client.listObjectsV2)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ =>
|
|
||||||
// UIO(objectResults(nowDate, key2, etag2, truncated = false)))
|
|
||||||
// private val result = invoke(fixture.amazonS3Client)(bucket, prefix)
|
|
||||||
// private val hashMap =
|
|
||||||
// result.map(_.byHash).map(m => Map.from(m.asMap.asScala))
|
|
||||||
// private val keyMap =
|
|
||||||
// result.map(_.byKey).map(m => Map.from(m.asMap.asScala))
|
|
||||||
// hashMap should be(Right(expectedHashMap))
|
|
||||||
// keyMap should be(Right(expectedKeyMap))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// def objectSummary(key: String, etag: String, lastModified: Date) = {
|
|
||||||
// val objectSummary = new S3ObjectSummary
|
|
||||||
// objectSummary.setKey(key)
|
|
||||||
// objectSummary.setETag(etag)
|
|
||||||
// objectSummary.setLastModified(lastModified)
|
|
||||||
// objectSummary
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// def objectResults(nowDate: Date,
|
|
||||||
// key: String,
|
|
||||||
// etag: String,
|
|
||||||
// truncated: Boolean) = {
|
|
||||||
// val result = new ListObjectsV2Result
|
|
||||||
// result.getObjectSummaries.add(objectSummary(key, etag, nowDate))
|
|
||||||
// result.setTruncated(truncated)
|
|
||||||
// result
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
// "when Amazon Service Exception" in {
|
|
||||||
// val exception = new AmazonS3Exception("message")
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
// (() => fixture.amazonS3Client.listObjectsV2)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ => Task.fail(exception))
|
|
||||||
// private val result = invoke(fixture.amazonS3Client)(bucket, prefix)
|
|
||||||
// assert(result.isLeft)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// "when Amazon SDK Client Exception" in {
|
|
||||||
// val exception = new SdkClientException("message")
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
// (() => fixture.amazonS3Client.listObjectsV2)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ => Task.fail(exception))
|
|
||||||
// private val result = invoke(fixture.amazonS3Client)(bucket, prefix)
|
|
||||||
// assert(result.isLeft)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// def invoke(amazonS3Client: AmazonS3Client.Client)(bucket: Bucket,
|
|
||||||
// prefix: RemoteKey) = {
|
|
||||||
// object TestEnv extends Storage.Test
|
|
||||||
// val program: RIO[Storage, RemoteObjects] = Lister
|
|
||||||
// .listObjects(amazonS3Client)(bucket, prefix)
|
|
||||||
// val runtime = new DefaultRuntime {}
|
|
||||||
// runtime.unsafeRunSync(program.provide(TestEnv)).toEither
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package net.kemitix.thorp.storage.aws
|
|
||||||
|
|
||||||
import net.kemitix.thorp.domain.MD5Hash
|
|
||||||
|
|
||||||
object MD5HashData {
|
|
||||||
|
|
||||||
val rootHash = MD5Hash.create("a3a6ac11a0eb577b81b3bb5c95cc8a6e")
|
|
||||||
|
|
||||||
val leafHash = MD5Hash.create("208386a650bdec61cfcd7bd8dcb6b542")
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package net.kemitix.thorp.storage.aws
|
|
||||||
|
|
||||||
import scala.jdk.CollectionConverters._
|
|
||||||
|
|
||||||
import com.amazonaws.services.s3.model.S3ObjectSummary
|
|
||||||
import net.kemitix.thorp.domain.{MD5Hash, RemoteKey}
|
|
||||||
import org.scalatest.FunSpec
|
|
||||||
|
|
||||||
class S3ObjectsByHashSuite extends FunSpec {
|
|
||||||
|
|
||||||
describe("grouping s3 object together by their hash values") {
|
|
||||||
val hash = MD5Hash.create("hash")
|
|
||||||
val key1 = RemoteKey.create("key-1")
|
|
||||||
val key2 = RemoteKey.create("key-2")
|
|
||||||
val o1 = s3object(hash, key1)
|
|
||||||
val o2 = s3object(hash, key2)
|
|
||||||
val os = List(o1, o2)
|
|
||||||
it("should group by the hash value") {
|
|
||||||
val expected: Map[MD5Hash, RemoteKey] = Map(
|
|
||||||
hash -> key2
|
|
||||||
)
|
|
||||||
val result = Map.from(S3Lister.byHash(os.asJava).asScala)
|
|
||||||
assertResult(expected)(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def s3object(md5Hash: MD5Hash,
|
|
||||||
remoteKey: RemoteKey): S3ObjectSummary = {
|
|
||||||
val summary = new S3ObjectSummary()
|
|
||||||
summary.setETag(md5Hash.hash())
|
|
||||||
summary.setKey(remoteKey.key)
|
|
||||||
summary
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
package net.kemitix.thorp.storage.aws
|
|
||||||
|
|
||||||
import org.scalamock.scalatest.MockFactory
|
|
||||||
import org.scalatest.FreeSpec
|
|
||||||
|
|
||||||
class UploaderTest extends FreeSpec with MockFactory {
|
|
||||||
|
|
||||||
// val uiChannel: UChannel[Any, UIEvent] = zioMessage => ()
|
|
||||||
//
|
|
||||||
// "upload" - {
|
|
||||||
// val aSource: File = Resource(this, "").toFile
|
|
||||||
// val aFile: File = Resource(this, "small-file").toFile
|
|
||||||
// val aHash = MD5Hash.create("aHash")
|
|
||||||
// val hashes = Hashes.create(MD5, aHash)
|
|
||||||
// val remoteKey = RemoteKey.create("aRemoteKey")
|
|
||||||
// val localFile =
|
|
||||||
// LocalFile.create(aFile, aSource, hashes, remoteKey, aFile.length)
|
|
||||||
// val bucket = Bucket.named("aBucket")
|
|
||||||
// val uploadResult = new UploadResult
|
|
||||||
// uploadResult.setKey(remoteKey.key)
|
|
||||||
// uploadResult.setETag(aHash.hash())
|
|
||||||
// val listenerSettings =
|
|
||||||
// UploadEventListener.Settings(uiChannel, localFile, 0, 0, batchMode = true)
|
|
||||||
// "when no error" in {
|
|
||||||
// val expected =
|
|
||||||
// Right(StorageEvent.uploadEvent(remoteKey, aHash))
|
|
||||||
// val inProgress = new AmazonUpload.InProgress {
|
|
||||||
// override def waitForUploadResult: Task[UploadResult] =
|
|
||||||
// Task(uploadResult)
|
|
||||||
// }
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
// (() => fixture.amazonS3TransferManager.uploader)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ => UIO.succeed(inProgress))
|
|
||||||
// private val result =
|
|
||||||
// invoke(fixture.amazonS3TransferManager)(
|
|
||||||
// localFile,
|
|
||||||
// bucket,
|
|
||||||
// listenerSettings
|
|
||||||
// )
|
|
||||||
// assertResult(expected)(result)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// "when Amazon Service Exception" in {
|
|
||||||
// val exception = new AmazonS3Exception("message")
|
|
||||||
// val expected =
|
|
||||||
// Right(
|
|
||||||
// StorageEvent.errorEvent(ActionSummary.upload(remoteKey.key),
|
|
||||||
// remoteKey,
|
|
||||||
// exception))
|
|
||||||
// val inProgress = new AmazonUpload.InProgress {
|
|
||||||
// override def waitForUploadResult: Task[UploadResult] =
|
|
||||||
// Task.fail(exception)
|
|
||||||
// }
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
// (() => fixture.amazonS3TransferManager.upload)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ => UIO.succeed(inProgress))
|
|
||||||
// private val result =
|
|
||||||
// invoke(fixture.amazonS3TransferManager)(
|
|
||||||
// localFile,
|
|
||||||
// bucket,
|
|
||||||
// listenerSettings
|
|
||||||
// )
|
|
||||||
// assertResult(expected)(result)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// "when Amazon SDK Client Exception" in {
|
|
||||||
// val exception = new SdkClientException("message")
|
|
||||||
// val expected =
|
|
||||||
// Right(
|
|
||||||
// StorageEvent.errorEvent(ActionSummary.upload(remoteKey.key),
|
|
||||||
// remoteKey,
|
|
||||||
// exception))
|
|
||||||
// val inProgress = new AmazonUpload.InProgress {
|
|
||||||
// override def waitForUploadResult: Task[UploadResult] =
|
|
||||||
// Task.fail(exception)
|
|
||||||
// }
|
|
||||||
// new AmazonS3ClientTestFixture {
|
|
||||||
// (() => fixture.amazonS3TransferManager.upload)
|
|
||||||
// .when()
|
|
||||||
// .returns(_ => UIO.succeed(inProgress))
|
|
||||||
// private val result =
|
|
||||||
// invoke(fixture.amazonS3TransferManager)(
|
|
||||||
// localFile,
|
|
||||||
// bucket,
|
|
||||||
// listenerSettings
|
|
||||||
// )
|
|
||||||
// assertResult(expected)(result)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// def invoke(transferManager: AmazonTransferManager)(
|
|
||||||
// localFile: LocalFile,
|
|
||||||
// bucket: Bucket,
|
|
||||||
// listenerSettings: UploadEventListener.Settings
|
|
||||||
// ) = {
|
|
||||||
// val program = Uploader
|
|
||||||
// .upload(transferManager)(
|
|
||||||
// Uploader.Request(localFile, bucket, listenerSettings))
|
|
||||||
// val runtime = new DefaultRuntime {}
|
|
||||||
// runtime
|
|
||||||
// .unsafeRunSync(
|
|
||||||
// program
|
|
||||||
// .provide(Config.Live))
|
|
||||||
// .toEither
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
package net.kemitix.thorp.storage.aws.hasher
|
|
||||||
|
|
||||||
import com.amazonaws.services.s3.transfer.TransferManagerConfiguration
|
|
||||||
import net.kemitix.thorp.filesystem.Resource
|
|
||||||
import org.scalatest.FreeSpec
|
|
||||||
|
|
||||||
class ETagGeneratorTest extends FreeSpec {
|
|
||||||
|
|
||||||
private val bigFile = Resource.select(this, "../big-file")
|
|
||||||
private val bigFilePath = bigFile.toPath
|
|
||||||
private val configuration = new TransferManagerConfiguration
|
|
||||||
private val chunkSize = 1200000
|
|
||||||
configuration.setMinimumUploadPartSize(chunkSize)
|
|
||||||
|
|
||||||
// "Create offsets" - {
|
|
||||||
// "should create offsets" in {
|
|
||||||
// val offsets = S3ETagGenerator
|
|
||||||
// .offsets(bigFile.length, chunkSize)
|
|
||||||
// .foldRight(List[Long]())((l: Long, a: List[Long]) => l :: a)
|
|
||||||
// assertResult(
|
|
||||||
// List(0, chunkSize, chunkSize * 2, chunkSize * 3, chunkSize * 4))(
|
|
||||||
// offsets)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// "create md5 hash for each chunk" - {
|
|
||||||
// "should create expected hash for chunks" in {
|
|
||||||
// val md5Hashes = List(
|
|
||||||
// "68b7d37e6578297621e06f01800204f1",
|
|
||||||
// "973475b14a7bda6ad8864a7f9913a947",
|
|
||||||
// "b9adcfc5b103fe2dd5924a5e5e6817f0",
|
|
||||||
// "5bd6e10a99fef100fe7bf5eaa0a42384",
|
|
||||||
// "8a0c1d0778ac8fcf4ca2010eba4711eb"
|
|
||||||
// ).zipWithIndex
|
|
||||||
// md5Hashes.foreach {
|
|
||||||
// case (hash, index) =>
|
|
||||||
// val program = Hasher.hashObjectChunk(bigFilePath, index, chunkSize)
|
|
||||||
// val result = runtime.unsafeRunSync(program.provide(TestEnv)).toEither
|
|
||||||
// assertResult(Right(hash))(
|
|
||||||
// result
|
|
||||||
// .map(hashes => hashes.get(MD5).get())
|
|
||||||
// .map(x => x.hash))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// "create etag for whole file" - {
|
|
||||||
// val expected = "f14327c90ad105244c446c498bfe9a7d-2"
|
|
||||||
// "should match aws etag for the file" in {
|
|
||||||
// val program = ETagGenerator.eTag(bigFilePath)
|
|
||||||
// val result = runtime.unsafeRunSync(program.provide(TestEnv)).toEither
|
|
||||||
// assertResult(Right(expected))(result)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in a new issue