├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.sbt ├── cloudwatch └── src │ ├── main │ └── scala │ │ └── cloudwatch.scala │ └── test │ └── scala │ └── CloudwatchSpec.scala ├── commons └── src │ └── main │ └── scala │ ├── AWSThreadFactory.scala │ └── FutureHelper.scala ├── project ├── Dependencies.scala ├── build.properties └── plugins.sbt ├── s3 └── src │ ├── main │ └── scala │ │ ├── AmazonS3Client.scala │ │ ├── AmazonS3ClientMaterialized.scala │ │ ├── AmazonS3Wrapper.scala │ │ └── futureTransfer.scala │ └── test │ ├── resources │ ├── big.txt │ ├── medium.txt │ └── small.txt │ └── scala │ └── S3Spec.scala ├── scaladoc.sbt └── sqs └── src ├── main └── scala │ ├── AmazonSQSClient.scala │ └── AmazonSQSWrapper.scala └── test └── scala └── SQSSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | **/.idea 9 | /*.iml 10 | /out 11 | /.idea_modules 12 | /.classpath 13 | /.project 14 | /RUNNING_PID 15 | /.settings -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file summarizes the main changes for each release. 4 | 5 | # Version 0.12.2 6 | 7 | - Add cross compilation to scala 2.12 ([#24](https://github.com/MfgLabs/common-aws/pull/24)) 8 | - In `uploadStreamAsFile` expose the `InitiateMultipartUploadRequest` as suggested in [#13](hhttps://github.com/MfgLabs/commons-aws/issues/13)) 9 | - Upgrade `akka-stream-extensions` version to `0.11.2` 10 | 11 | ## Version 0.12.1 12 | 13 | - Add the region as a mandatory parameter for all clients. 14 | - Add the `apply` and `from` constructor for `AmazonCloudwatchClient`. 15 | 16 | ### Version 0.12.0 17 | 18 | - Rework the class hierarchy, for `S3` and `SQS` 19 | - The `Wrapper` wrap the amazon Java sdk function with no additional logic. 20 | - The `Client` extend it 21 | - For `ClientMaterialized` extend the `Client` and need an additional `Materializer` to transform a stream to `Future`. 22 | - Move `deleteObjects(bucket, prefix)` out of `AmazonS3Wrapper` to be able to use `AmazonS3ClientMaterialized.listFiles` 23 | - Switch to `ForkJoinPool` with daemon thread as default. 24 | 25 | 26 | ### Version 0.11.0 27 | 28 | - Remove dependency to the unmaintained `dwhjames/aws-wrap` by including the necessary code 29 | - Split the project in three : `commons-aws-cloudwatch`, `commons-aws-s3` and `commons-aws-sqs` 30 | - Upgrade dependencies including `aws-java-sdk` and `akka-stream-extensions` 31 | - Remove `CloudwatchAkkaHeartbeat` 32 | - Replace `AmazonCloudwatchClient` and `AmazonS3AsyncClient` constructors by an `apply` with default values. 33 | - The followind duplicated methods from`AmazonS3AsyncClient` were deleted : 34 | - `uploadFile` use `putObject` with `new PutObjectRequest().withCannedAcl(CannedAccessControlList.PublicReadWrite)` 35 | - `deleteFile` use `deleteObject` 36 | - `deleteFiles` use `deleteObjects` 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streaming / asynchronous Scala client for common AWS services 2 | 3 | Streaming / asynchronous Scala client for common AWS services. 4 | When possible, clients expose methods that return Akka Stream's Sources / Flows / Sinks to provide streaming facilities. 5 | 6 | Clients use a pool of threads managed internally and optimized for blocking IO operations. 7 | 8 | This library makes heavy use of our extension library for Akka Stream 9 | [MfgLabs/akka-stream-extensions](https://github.com/MfgLabs/akka-stream-extensions). 10 | 11 | ## Resolver 12 | 13 | ```scala 14 | resolvers ++= Seq( 15 | Resolver.bintrayRepo("mfglabs", "maven") 16 | ) 17 | ``` 18 | 19 | ## Dependencies 20 | 21 | Three packages are available : 22 | ```scala 23 | libraryDependencies += "com.mfglabs" %% "commons-aws-cloudwatch" % "0.12.2" 24 | libraryDependencies += "com.mfglabs" %% "commons-aws-s3" % "0.12.2" 25 | libraryDependencies += "com.mfglabs" %% "commons-aws-sqs" % "0.12.2" 26 | ``` 27 | 28 | Changelog [here](CHANGELOG.md) 29 | 30 | ## Usage 31 | 32 | > Scaladoc is available [there](http://mfglabs.github.io/commons-aws/api/current/) 33 | 34 | ### Commons 35 | 36 | #### S3 37 | 38 | ```scala 39 | import com.mfglabs.commons.aws.s3._ 40 | 41 | val client = AmazonS3Client()()) // client with un-materialized composable Source / Flow / Sink 42 | 43 | val fileStream: Source[ByteString, Unit] = client.getFileAsStream(bucket, key) 44 | 45 | val multipartfileStream: Source[ByteString, Unit] = client.getMultipartFileAsStream(bucket, prefix) 46 | 47 | someBinaryStream.via( 48 | client.uploadStreamAsFile(bucket, key, chunkUploadConcurrency = 2) 49 | ) 50 | 51 | someBinaryStream.via( 52 | client.uploadStreamAsMultipartFile( 53 | bucket, 54 | prefix, 55 | nbChunkPerFile = 10000, 56 | chunkUploadConcurrency = 2 57 | ) 58 | ) 59 | 60 | val ops = client.materialized(flowMaterializer) // client with added materialized methods 61 | 62 | val file: Future[ByteString] = ops.getFile(bucket, key) 63 | 64 | // More methods, check the source code 65 | ``` 66 | 67 | Please remark that you don't need any implicit `scala.concurrent.ExecutionContext` as it's directly provided 68 | and managed by [[AmazonS3Client]] itself. 69 | 70 | There are also smart `AmazonS3Client` constructors that can be provided with custom. 71 | `java.util.concurrent.ExecutorService` if you want to manage your pools of threads. 72 | 73 | 74 | #### SQS 75 | 76 | ```scala 77 | import com.mfglabs.commons.aws.sqs._ 78 | 79 | val sqs = AmazonSQSClient()() 80 | 81 | val sender: Flow[String, SendMessageResult, Unit] = 82 | Flow[String].map { body => 83 | val req = new SendMessageRequest() 84 | req.setMessageBody(body) 85 | req.setQueueUrl(queueUrl) 86 | req 87 | } 88 | .via(sqs.sendMessageAsStream()) 89 | 90 | val receiver: Source[Message, Unit] = sqs.receiveMessageAsStream(queueUrl, autoAck = false) 91 | ``` 92 | 93 | Please remark that you don't need any implicit `scala.concurrent.ExecutionContext` as it's directly provided 94 | and managed by [[AmazonSQSClient]] itself. 95 | 96 | There are also smart `AmazonSQSClient` constructors that can be provided with custom. 97 | `java.util.concurrent.ExecutorService` if you want to manage your pools of threads. 98 | 99 | #### Cloudwatch 100 | 101 | In your code: 102 | 103 | ```scala 104 | import com.mfglabs.commons.aws.cloudwatch 105 | import cloudwatch._ // brings implicit extensions 106 | 107 | // Create the client 108 | val CW = cloudwatch.AmazonCloudwatchClient()() 109 | 110 | // Use it 111 | for { 112 | metrics <- CW.listMetrics() 113 | } yield (metrics) 114 | ``` 115 | 116 | Please remark that you don't need any implicit `scala.concurrent.ExecutionContext` as it's directly provided 117 | and managed by [[AmazonCloudwatchClient]] itself. 118 | 119 | There are also smart `AmazonCloudwatchClient` constructors that can be provided with custom. 120 | `java.util.concurrent.ExecutorService` if you want to manage your pools of threads. 121 | 122 | ## License 123 | 124 | This software is licensed under the Apache 2 license, quoted below. 125 | 126 | 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 127 | 128 | http://www.apache.org/licenses/LICENSE-2.0 129 | 130 | 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. 131 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import bintray.Plugin._ 2 | 3 | organization in ThisBuild := "com.mfglabs" 4 | 5 | scalaVersion in ThisBuild := "2.11.11" 6 | 7 | version in ThisBuild := "0.12.2" 8 | 9 | crossScalaVersions := Seq("2.11.11", "2.12.2") 10 | 11 | resolvers in ThisBuild ++= Seq( 12 | "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/", 13 | Resolver.sonatypeRepo("releases"), 14 | DefaultMavenRepository, 15 | Resolver.bintrayRepo("mfglabs", "maven") 16 | ) 17 | 18 | scalacOptions in ThisBuild ++= Seq( 19 | "-encoding", "UTF-8", 20 | "-target:jvm-1.8", 21 | "-Ydelambdafy:method", 22 | "-Yno-adapted-args", 23 | "-deprecation", 24 | "-feature", 25 | "-language:postfixOps", 26 | "-unchecked", 27 | "-Xfuture", 28 | "-Xlint", 29 | "-Xlint:-missing-interpolator", 30 | "-Xlint:private-shadow", 31 | "-Xlint:type-parameter-shadow", 32 | "-Ywarn-dead-code", 33 | "-Ywarn-unused", 34 | "-Ywarn-unused-import", 35 | "-Ywarn-numeric-widen", 36 | "-Ywarn-value-discard", 37 | "-Xcheckinit" 38 | ) 39 | 40 | publishMavenStyle in ThisBuild := true 41 | 42 | lazy val commonSettings = Seq( 43 | scmInfo := Some(ScmInfo( 44 | url("https://github.com/MfgLabs/commons-aws"), 45 | "git@github.com:MfgLabs/commons-aws.git" 46 | )) 47 | ) 48 | 49 | lazy val publishSettings = Seq( 50 | homepage := Some(url("https://github.com/MfgLabs/commons-aws")), 51 | licenses := Seq("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0.html")), 52 | autoAPIMappings := true, 53 | publishMavenStyle := true, 54 | publishArtifact in packageDoc := false, 55 | publishArtifact in Test := false, 56 | pomIncludeRepository := { _ => false } 57 | ) ++ bintrayPublishSettings 58 | 59 | lazy val noPublishSettings = Seq( 60 | publish := (), 61 | publishLocal := (), 62 | publishArtifact := false 63 | ) 64 | 65 | lazy val all = (project in file(".")) 66 | .aggregate(commons) 67 | .aggregate(cloudwatch) 68 | .aggregate(s3) 69 | .aggregate(sqs) 70 | .settings(name := "commons-aws-all") 71 | .enablePlugins(ScalaUnidocPlugin) 72 | .settings(name := "commons-aws-all") 73 | .settings(noPublishSettings) 74 | 75 | lazy val commons = project.in(file("commons")) 76 | .settings ( 77 | name := "commons-aws", 78 | libraryDependencies ++= Seq( 79 | Dependencies.Compile.awsJavaSDKcore 80 | ), 81 | commonSettings, 82 | publishSettings 83 | ) 84 | 85 | lazy val cloudwatch = project.in(file("cloudwatch")) 86 | .settings ( 87 | name := "commons-aws-cloudwatch", 88 | libraryDependencies ++= Seq( 89 | Dependencies.Compile.awsJavaSDKcw, 90 | Dependencies.Compile.akkaStreamExt, 91 | Dependencies.Compile.slf4j, 92 | Dependencies.Test.scalaTest 93 | ), 94 | commonSettings, 95 | publishSettings 96 | ).dependsOn(commons) 97 | 98 | lazy val s3 = project.in(file("s3")) 99 | .settings ( 100 | name := "commons-aws-s3", 101 | libraryDependencies ++= Seq( 102 | Dependencies.Compile.awsJavaSDKs3, 103 | Dependencies.Compile.akkaStreamExt, 104 | Dependencies.Compile.slf4j, 105 | Dependencies.Test.scalaTest 106 | ), 107 | commonSettings, 108 | publishSettings 109 | ).dependsOn(commons) 110 | 111 | lazy val sqs = project.in(file("sqs")) 112 | .settings ( 113 | name := "commons-aws-sqs", 114 | libraryDependencies ++= Seq( 115 | Dependencies.Compile.awsJavaSDKsqs, 116 | Dependencies.Compile.akkaStreamExt, 117 | Dependencies.Compile.slf4j, 118 | Dependencies.Test.scalaTest 119 | ), 120 | commonSettings, 121 | publishSettings 122 | ).dependsOn(commons) 123 | -------------------------------------------------------------------------------- /cloudwatch/src/main/scala/cloudwatch.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Pellucid Analytics 3 | * Copyright 2015 Daniel W. H. James 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.mfglabs.commons.aws 19 | package cloudwatch 20 | 21 | import java.util.concurrent.ExecutorService 22 | import scala.concurrent.Future 23 | 24 | import com.amazonaws.auth.{AWSCredentialsProvider, DefaultAWSCredentialsProviderChain} 25 | import com.amazonaws.ClientConfiguration 26 | import com.amazonaws.client.builder.ExecutorFactory 27 | 28 | import com.amazonaws.services.cloudwatch.{AmazonCloudWatchAsync, AmazonCloudWatchAsyncClientBuilder} 29 | import com.amazonaws.services.cloudwatch.model._ 30 | 31 | 32 | object AmazonCloudwatchClient { 33 | import com.amazonaws.auth.{AWSCredentials, AWSStaticCredentialsProvider} 34 | import FutureHelper.defaultExecutorService 35 | import com.amazonaws.regions.Regions 36 | 37 | def apply( 38 | region : Regions, 39 | awsCredentials : AWSCredentials, 40 | clientConfiguration : ClientConfiguration = new ClientConfiguration() 41 | )( 42 | executorService : ExecutorService = defaultExecutorService(clientConfiguration, "aws.wrap.cloudwatch") 43 | ): AmazonCloudwatchClient = { 44 | from(region, new AWSStaticCredentialsProvider(awsCredentials), clientConfiguration)(executorService) 45 | } 46 | 47 | def from( 48 | region : Regions, 49 | awsCredentialsProvider : AWSCredentialsProvider = new DefaultAWSCredentialsProviderChain, 50 | clientConfiguration : ClientConfiguration = new ClientConfiguration() 51 | )( 52 | executorService : ExecutorService = defaultExecutorService(clientConfiguration, "aws.wrap.cloudwatch") 53 | ): AmazonCloudwatchClient = new AmazonCloudwatchClient( 54 | AmazonCloudWatchAsyncClientBuilder 55 | .standard() 56 | .withRegion(region) 57 | .withCredentials(awsCredentialsProvider) 58 | .withClientConfiguration(clientConfiguration) 59 | .withExecutorFactory(new ExecutorFactory { def newExecutor() = executorService }) 60 | .build() 61 | ) 62 | 63 | } 64 | 65 | /** 66 | * A lightweight wrapper for [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/dynamodbv2/AmazonDynamoDBAsyncClient.html AmazonCloudWatchAsyncClient]]. 67 | * 68 | * @constructor construct a wrapper client from an Amazon async client. 69 | * @param client 70 | * the underlying [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/dynamodbv2/AmazonDynamoDBAsyncClient.html AmazonCloudWatchAsyncClient]]. 71 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/dynamodbv2/AmazonDynamoDBAsyncClient.html AmazonCloudWatchAsyncClient]] 72 | */ 73 | class AmazonCloudwatchClient(val client: AmazonCloudWatchAsync) { 74 | import FutureHelper._ 75 | 76 | /** 77 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#deleteAlarms(com.amazonaws.services.cloudwatch.model.DeleteAlarmsRequest) AWS Java SDK]] 78 | */ 79 | def deleteAlarms( 80 | deleteAlarmsRequest: DeleteAlarmsRequest 81 | ): Future[DeleteAlarmsResult] = 82 | wrapAsyncMethod(client.deleteAlarmsAsync, deleteAlarmsRequest) 83 | 84 | /** 85 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#deleteAlarms(com.amazonaws.services.cloudwatch.model.DeleteAlarmsRequest) AWS Java SDK]] 86 | */ 87 | def deleteAlarms(alarmNames: String*): Future[DeleteAlarmsResult] = 88 | deleteAlarms(new DeleteAlarmsRequest().withAlarmNames(alarmNames: _*)) 89 | 90 | /** 91 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#describeAlarmHistory(com.amazonaws.services.cloudwatch.model.DescribeAlarmHistoryRequest) AWS Java SDK]] 92 | */ 93 | def describeAlarmHistory( 94 | describeAlarmHistoryRequest: DescribeAlarmHistoryRequest 95 | ): Future[DescribeAlarmHistoryResult] = 96 | wrapAsyncMethod(client.describeAlarmHistoryAsync, describeAlarmHistoryRequest) 97 | 98 | /** 99 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#describeAlarmHistory() AWS Java SDK]] 100 | */ 101 | def describeAlarmHistory(): Future[DescribeAlarmHistoryResult] = 102 | describeAlarmHistory(new DescribeAlarmHistoryRequest()) 103 | 104 | /** 105 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#describeAlarms(com.amazonaws.services.cloudwatch.model.DescribeAlarmsRequest) AWS Java SDK]] 106 | */ 107 | def describeAlarms( 108 | describeAlarmRequest: DescribeAlarmsRequest 109 | ): Future[DescribeAlarmsResult] = 110 | wrapAsyncMethod(client.describeAlarmsAsync, describeAlarmRequest) 111 | 112 | /** 113 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#describeAlarms() AWS Java SDK]] 114 | */ 115 | def describeAlarms(): Future[DescribeAlarmsResult] = 116 | describeAlarms(new DescribeAlarmsRequest()) 117 | 118 | /** 119 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#describeAlarmsForMetric(com.amazonaws.services.cloudwatch.model.DescribeAlarmsForMetricRequest) AWS Java SDK]] 120 | */ 121 | def describeAlarmsForMetric( 122 | describeAlarmsForMetricRequest: DescribeAlarmsForMetricRequest 123 | ): Future[DescribeAlarmsForMetricResult] = 124 | wrapAsyncMethod(client.describeAlarmsForMetricAsync, describeAlarmsForMetricRequest) 125 | 126 | /** 127 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#disableAlarmActions(com.amazonaws.services.cloudwatch.model.DisableAlarmActionsRequest) AWS Java SDK]] 128 | */ 129 | def disableAlarmActions( 130 | disableAlarmActionsRequest: DisableAlarmActionsRequest 131 | ): Future[DisableAlarmActionsResult] = 132 | wrapAsyncMethod(client.disableAlarmActionsAsync, disableAlarmActionsRequest) 133 | 134 | /** 135 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#disableAlarmActions(com.amazonaws.services.cloudwatch.model.DisableAlarmActionsRequest) AWS Java SDK]] 136 | */ 137 | def disableAlarmActions(alarmNames: String*): Future[DisableAlarmActionsResult] = 138 | disableAlarmActions(new DisableAlarmActionsRequest().withAlarmNames(alarmNames: _*)) 139 | 140 | /** 141 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#enableAlarmActions(com.amazonaws.services.cloudwatch.model.EnableAlarmActionsRequest) AWS Java SDK]] 142 | */ 143 | def enableAlarmActions( 144 | enableAlarmActionsRequest: EnableAlarmActionsRequest 145 | ): Future[EnableAlarmActionsResult] = 146 | wrapAsyncMethod(client.enableAlarmActionsAsync, enableAlarmActionsRequest) 147 | 148 | /** 149 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#enableAlarmActions(com.amazonaws.services.cloudwatch.model.EnableAlarmActionsRequest) AWS Java SDK]] 150 | */ 151 | def enableAlarmActions(alarmNames: String*): Future[EnableAlarmActionsResult] = 152 | enableAlarmActions(new EnableAlarmActionsRequest().withAlarmNames(alarmNames: _*)) 153 | 154 | /** 155 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#getMetricStatistics(com.amazonaws.services.cloudwatch.model.GetMetricStatisticsRequest) AWS Java SDK]] 156 | */ 157 | def getMetricStatistics( 158 | getMetricStatisticsRequest: GetMetricStatisticsRequest 159 | ): Future[GetMetricStatisticsResult] = 160 | wrapAsyncMethod(client.getMetricStatisticsAsync, getMetricStatisticsRequest) 161 | 162 | /** 163 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#listMetrics(com.amazonaws.services.cloudwatch.model.ListMetricsRequest) AWS Java SDK]] 164 | */ 165 | def listMetrics( 166 | listMetricsRequest: ListMetricsRequest 167 | ): Future[ListMetricsResult] = 168 | wrapAsyncMethod(client.listMetricsAsync, listMetricsRequest) 169 | 170 | /** 171 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#listMetrics() AWS Java SDK]] 172 | */ 173 | def listMetrics(): Future[ListMetricsResult] = 174 | listMetrics(new ListMetricsRequest) 175 | 176 | /** 177 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#putMetricAlarm(com.amazonaws.services.cloudwatch.model.PutMetricAlarmRequest) AWS Java SDK]] 178 | */ 179 | def putMetricAlarm( 180 | putMetricAlarmRequest: PutMetricAlarmRequest 181 | ): Future[PutMetricAlarmResult] = 182 | wrapAsyncMethod(client.putMetricAlarmAsync, putMetricAlarmRequest) 183 | 184 | /** 185 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#putMetricData(com.amazonaws.services.cloudwatch.model.PutMetricDataRequest) AWS Java SDK]] 186 | */ 187 | def putMetricData( 188 | putMetricDataRequest: PutMetricDataRequest 189 | ): Future[PutMetricDataResult] = 190 | wrapAsyncMethod(client.putMetricDataAsync, putMetricDataRequest) 191 | 192 | /** 193 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#putMetricData(com.amazonaws.services.cloudwatch.model.PutMetricDataRequest) AWS Java SDK]] 194 | */ 195 | def putMetricData( 196 | namespace: String, 197 | metricData: Iterable[MetricDatum] 198 | ): Future[PutMetricDataResult] = { 199 | import scala.collection.JavaConverters.asJavaCollectionConverter 200 | 201 | putMetricData( 202 | new PutMetricDataRequest() 203 | .withNamespace(namespace) 204 | .withMetricData(metricData.asJavaCollection) 205 | ) 206 | } 207 | 208 | /** 209 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#setAlarmState(com.amazonaws.services.cloudwatch.model.SetAlarmStateRequest) AWS Java SDK]] 210 | */ 211 | def setAlarmState( 212 | setAlarmStateRequest: SetAlarmStateRequest 213 | ): Future[SetAlarmStateResult] = 214 | wrapAsyncMethod(client.setAlarmStateAsync, setAlarmStateRequest) 215 | 216 | /** 217 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#setAlarmState(com.amazonaws.services.cloudwatch.model.SetAlarmStateRequest) AWS Java SDK]] 218 | */ 219 | def setAlarmState( 220 | alarmName: String, 221 | stateReason: String, 222 | stateValue: StateValue, 223 | stateReasonData: String = "" 224 | ): Future[SetAlarmStateResult] = 225 | setAlarmState( 226 | new SetAlarmStateRequest() 227 | .withAlarmName(alarmName) 228 | .withStateReason(stateReason) 229 | .withStateValue(stateValue) 230 | .withStateReasonData(stateReasonData) 231 | ) 232 | 233 | /** 234 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/cloudwatch/AmazonCloudWatch.html#shutdown() AWS Java SDK]] 235 | */ 236 | def shutdown(): Unit = 237 | client.shutdown() 238 | 239 | } 240 | -------------------------------------------------------------------------------- /cloudwatch/src/test/scala/CloudwatchSpec.scala: -------------------------------------------------------------------------------- 1 | package com.mfglabs.commons.aws 2 | 3 | import com.amazonaws.regions.Regions 4 | import org.scalatest.{FlatSpec, Matchers} 5 | import org.scalatest.concurrent.ScalaFutures 6 | import org.scalatest.time.{Minutes, Millis, Span} 7 | 8 | class CloudwatchSpec extends FlatSpec with Matchers with ScalaFutures { 9 | implicit override val patienceConfig = 10 | PatienceConfig(timeout = Span(2, Minutes), interval = Span(5, Millis)) 11 | 12 | val CW = cloudwatch.AmazonCloudwatchClient.from(Regions.EU_WEST_1)() 13 | 14 | "Cloudwatch client" should "retrieve all metrics" in { 15 | whenReady(CW.listMetrics()) { s => s.getMetrics should not be 'empty } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /commons/src/main/scala/AWSThreadFactory.scala: -------------------------------------------------------------------------------- 1 | package com.mfglabs.commons.aws 2 | 3 | import java.util.concurrent._ 4 | 5 | class AWSThreadFactory(name: String) extends ForkJoinPool.ForkJoinWorkerThreadFactory { 6 | private val backingThreadFactory = ForkJoinPool.defaultForkJoinWorkerThreadFactory 7 | 8 | override def newThread(pool: ForkJoinPool): ForkJoinWorkerThread = { 9 | val thread = backingThreadFactory.newThread(pool) 10 | thread.setName(s"$name-${thread.getPoolIndex()}") 11 | thread 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /commons/src/main/scala/FutureHelper.scala: -------------------------------------------------------------------------------- 1 | package com.mfglabs.commons.aws 2 | 3 | import com.amazonaws.AmazonWebServiceRequest 4 | import com.amazonaws.ClientConfiguration 5 | import com.amazonaws.handlers.AsyncHandler 6 | 7 | import scala.concurrent.{Future, Promise} 8 | import scala.util.Try 9 | import java.util.concurrent.{Future => JFuture, ForkJoinPool} 10 | 11 | object FutureHelper { 12 | 13 | def promiseToAsyncHandler[Request <: AmazonWebServiceRequest, Result](p: Promise[Result]) = 14 | new AsyncHandler[Request, Result] { 15 | override def onError(exception: Exception): Unit = { p.failure(exception); () } 16 | override def onSuccess(request: Request, result: Result): Unit = { p.success(result); () } 17 | } 18 | 19 | @inline 20 | def wrapAsyncMethod[Request <: AmazonWebServiceRequest, Result]( 21 | f: (Request, AsyncHandler[Request, Result]) => JFuture[Result], 22 | request: Request 23 | ): Future[Result] = { 24 | val p = Promise[Result] 25 | f(request, promiseToAsyncHandler(p)) 26 | p.future 27 | } 28 | 29 | def defaultExecutorService(clientConfiguration: ClientConfiguration, factoryName: String) = { 30 | new ForkJoinPool( 31 | clientConfiguration.getMaxConnections + 1, 32 | new AWSThreadFactory(factoryName), 33 | null, // We do not override the default Thread.UncaughtExceptionHandler 34 | true // activate asyncMode 35 | ) 36 | } 37 | 38 | trait MethodWrapper { 39 | 40 | @inline 41 | def executorService: java.util.concurrent.ExecutorService 42 | 43 | @inline 44 | def wrapMethod[Request, Result](f: Request => Result, request: Request): Future[Result] = { 45 | val p = Promise[Result] 46 | executorService.execute(new Runnable { 47 | override def run(): Unit = { 48 | p.complete(Try { f(request) }) 49 | () 50 | } 51 | }) 52 | p.future 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | 5 | object V { 6 | val awsJavaSDK = "1.11.132" 7 | val akkaStreamExt = "0.11.2" 8 | val scalaTest = "3.0.3" 9 | val slf4j = "1.7.12" 10 | } 11 | 12 | object Compile { 13 | val awsJavaSDKcore = "com.amazonaws" % "aws-java-sdk-core" % V.awsJavaSDK 14 | val awsJavaSDKcw = "com.amazonaws" % "aws-java-sdk-cloudwatch" % V.awsJavaSDK 15 | val awsJavaSDKs3 = "com.amazonaws" % "aws-java-sdk-s3" % V.awsJavaSDK 16 | val awsJavaSDKsqs = "com.amazonaws" % "aws-java-sdk-sqs" % V.awsJavaSDK 17 | 18 | val akkaStreamExt = "com.mfglabs" %% "akka-stream-extensions" % V.akkaStreamExt 19 | 20 | val slf4j = "org.slf4j" % "slf4j-api" % V.slf4j 21 | } 22 | 23 | object Test { 24 | val scalaTest = "org.scalatest" %% "scalatest" % V.scalaTest % "test" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.15 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.0") 2 | 3 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.2.1") 4 | -------------------------------------------------------------------------------- /s3/src/main/scala/AmazonS3Client.scala: -------------------------------------------------------------------------------- 1 | package com.mfglabs.commons.aws 2 | package s3 3 | 4 | import akka.stream.ActorMaterializer 5 | import akka.stream.scaladsl._ 6 | import akka.util.ByteString 7 | 8 | import com.amazonaws.services.s3.AmazonS3 9 | import com.amazonaws.services.s3.model._ 10 | import com.mfglabs.stream.{ExecutionContextForBlockingOps, FlowExt, SourceExt} 11 | 12 | import java.io.{ByteArrayInputStream, InputStream} 13 | import java.util.concurrent.ExecutorService 14 | import java.util.zip.GZIPInputStream 15 | 16 | import scala.concurrent.Future 17 | 18 | object AmazonS3Client { 19 | import com.amazonaws.auth._ 20 | import com.amazonaws.ClientConfiguration 21 | import com.amazonaws.regions.Regions 22 | import com.amazonaws.services.s3.AmazonS3ClientBuilder 23 | import FutureHelper.defaultExecutorService 24 | 25 | def apply( 26 | region : Regions, 27 | awsCredentials : AWSCredentials, 28 | clientConfiguration : ClientConfiguration = new ClientConfiguration() 29 | )( 30 | executorService : ExecutorService = defaultExecutorService(clientConfiguration, "aws.wrap.s3") 31 | ): AmazonS3Client = { 32 | from(region, new AWSStaticCredentialsProvider(awsCredentials), clientConfiguration)(executorService) 33 | } 34 | 35 | def from( 36 | region : Regions, 37 | awsCredentialsProvider : AWSCredentialsProvider = new DefaultAWSCredentialsProviderChain, 38 | clientConfiguration : ClientConfiguration = new ClientConfiguration() 39 | )( 40 | executorService : ExecutorService = defaultExecutorService(clientConfiguration, "aws.wrap.s3") 41 | ): AmazonS3Client = { 42 | 43 | val client = AmazonS3ClientBuilder 44 | .standard() 45 | .withRegion(region) 46 | .withCredentials(awsCredentialsProvider) 47 | .withClientConfiguration(clientConfiguration) 48 | .build() 49 | 50 | new AmazonS3Client(client, executorService) 51 | } 52 | 53 | def build(builder: AmazonS3ClientBuilder) = { 54 | val client = builder.build() 55 | 56 | new AmazonS3Client(client, defaultExecutorService(builder.getClientConfiguration, "aws.wrap.s3")) 57 | } 58 | 59 | } 60 | 61 | class AmazonS3Client( 62 | val client : AmazonS3, 63 | val executorService : ExecutorService 64 | ) extends AmazonS3Wrapper { 65 | import scala.collection.immutable.Seq 66 | 67 | implicit lazy val ecForBlockingOps = ExecutionContextForBlockingOps(ec) 68 | 69 | /** 70 | * Return a client with additional logic to obtain a Stream logic as a Future. 71 | */ 72 | def materialized(flowMaterializer : ActorMaterializer): AmazonS3ClientMaterialized = 73 | new AmazonS3ClientMaterialized(client, executorService, flowMaterializer) 74 | 75 | /** List all files with a given prefix of a S3 bucket. 76 | * @param bucket S3 bucket 77 | * @param maybePrefix S3 prefix. If not present, all the files of the bucket will be returned. 78 | */ 79 | def listFilesAsStream(bucket: String, maybePrefix: Option[String] = None): Source[S3ObjectSummary, akka.NotUsed] = { 80 | import collection.JavaConverters._ 81 | 82 | def getFirstListing: Future[ObjectListing] = maybePrefix match { 83 | case Some(prefix) => listObjects(bucket, prefix) 84 | case None => listObjects(bucket) 85 | } 86 | 87 | def unfold(listing: ObjectListing) = 88 | Some(Some(listing) -> listing.getObjectSummaries.asScala.to[Seq]) 89 | 90 | Source.unfoldAsync[Option[ObjectListing], Seq[S3ObjectSummary]](None) { 91 | case None => getFirstListing.map(unfold) 92 | case Some(oldListing) if oldListing.isTruncated => listNextBatchOfObjects(oldListing).map(unfold) 93 | case Some(_) => Future.successful(None) 94 | }.mapConcat(identity) 95 | } 96 | 97 | /** Stream a S3 file. 98 | * @param bucket the bucket name 99 | * @param key the key of file 100 | * @param inputStreamTransform optional inputstream transformation (for example GZIP decompression, ...) 101 | */ 102 | def getFileAsStream(bucket: String, key: String, inputStreamTransform: InputStream => InputStream = identity): Source[ByteString, akka.NotUsed] = { 103 | SourceExt.seededLazyAsync(getObject(bucket, key)) { o => 104 | StreamConverters.fromInputStream(() => inputStreamTransform(o.getObjectContent)) 105 | } 106 | } 107 | 108 | /** 109 | * Stream a gzipped S3 file and decompress it on the fly. 110 | * @param bucket S3 bucket 111 | * @param key S3 key 112 | * @return 113 | */ 114 | def uncompressGzippedFileAsStream(bucket: String, key: String) = 115 | getFileAsStream(bucket, key, is => new GZIPInputStream(is)) 116 | 117 | /** 118 | * Stream sequentially a SE multipart file. 119 | * @param bucket S3 bucket 120 | * @param prefix S3 prefix. Files of path prefix* will be taken into account. 121 | * @param inputStreamTransform optional inputstream transformation (for example GZIP decompression, ...) 122 | */ 123 | def getMultipartFileAsStream(bucket: String, prefix: String, inputStreamTransform: InputStream => InputStream = identity): Source[ByteString, akka.NotUsed] = { 124 | listFilesAsStream(bucket, Some(prefix)) 125 | .flatMapConcat { s3Object => getFileAsStream(bucket, s3Object.getKey, inputStreamTransform) } 126 | } 127 | 128 | /** 129 | * Upload a stream of bytes as a S3 file and returns an element of type CompleteMultipartUploadResult (or nothing if upstream was empty). 130 | * @param bucket S3 bucket 131 | * @param key S3 key 132 | * @param chunkUploadConcurrency chunk upload concurrency. Order is guaranteed even with chunkUploadConcurrency > 1. 133 | */ 134 | def uploadStreamAsFile(bucket: String, key: String, chunkUploadConcurrency: Int = 1): Flow[ByteString, CompleteMultipartUploadResult, akka.NotUsed] = { 135 | val request = new InitiateMultipartUploadRequest(bucket, key) 136 | uploadStreamAsFile(request, chunkUploadConcurrency) 137 | } 138 | 139 | def uploadStreamAsFile(intiate: InitiateMultipartUploadRequest, chunkUploadConcurrency: Int): Flow[ByteString, CompleteMultipartUploadResult, akka.NotUsed] = { 140 | import scala.collection.JavaConverters._ 141 | 142 | val uploadChunkSize = 8 * 1024 * 1024 // recommended by AWS 143 | 144 | def initiateUpload: Future[String] = initiateMultipartUpload(intiate).map(_.getUploadId) 145 | 146 | Flow[ByteString] 147 | .via(FlowExt.rechunkByteStringBySize(uploadChunkSize)) 148 | .via(FlowExt.zipWithConstantLazyAsync(initiateUpload)) 149 | .via(FlowExt.zipWithIndex) 150 | .mapAsyncUnordered(chunkUploadConcurrency) { case ((bytes, uploadId), partNumber) => 151 | val uploadRequest = new UploadPartRequest() 152 | .withBucketName(intiate.getBucketName) 153 | .withKey(intiate.getKey) 154 | .withPartNumber((partNumber + 1).toInt) 155 | .withUploadId(uploadId) 156 | .withInputStream(new ByteArrayInputStream(bytes.toArray)) 157 | .withPartSize(bytes.length.toLong) 158 | 159 | uploadPart(uploadRequest).map(r => (r.getPartETag, uploadId)).recoverWith { 160 | case e: Exception => 161 | abortMultipartUpload(new AbortMultipartUploadRequest(intiate.getBucketName, intiate.getKey, uploadId)) 162 | Future.failed(e) 163 | } 164 | } 165 | .via(FlowExt.fold(Vector.empty[(PartETag, String)])(_ :+ _)) 166 | .mapAsync(1) { etags => 167 | etags.headOption match { 168 | case Some((_, uploadId)) => 169 | val compRequest = new CompleteMultipartUploadRequest( 170 | intiate.getBucketName, intiate.getKey, uploadId, etags.map(_._1).asJava 171 | ) 172 | 173 | val futResult = completeMultipartUpload(compRequest).map(Option.apply).recoverWith { case e: Exception => 174 | abortMultipartUpload(new AbortMultipartUploadRequest(intiate.getBucketName, intiate.getKey, uploadId)) 175 | Future.failed(e) 176 | } 177 | futResult 178 | 179 | case None => Future.successful(None) 180 | } 181 | } 182 | .mapConcat(_.to[scala.collection.immutable.Seq]) 183 | } 184 | 185 | /** 186 | * Upload a stream as a multipart file with a given number of upstream chunk per file. Part file are uploaded sequentially but 187 | * chunk inside a part file can be uploaded concurrently (tuned with chunkUploadConcurrency). 188 | * The Flow returns a CompleteMultipartUploadResult for each part file uploaded. 189 | * @param bucket S3 bucket 190 | * @param prefix S3 prefix. The actual part files will be named prefix.part.00000001, prefix.part.00000002, ... 191 | * @param nbChunkPerFile the number of upstream chunks (for example lines) to include in each part file 192 | * @param chunkUploadConcurrency chunk upload concurrency. Order is guaranteed even with chunkUploadConcurrency > 1. 193 | */ 194 | def uploadStreamAsMultipartFile(bucket: String, prefix: String, nbChunkPerFile: Int, 195 | chunkUploadConcurrency: Int = 1): Flow[ByteString, CompleteMultipartUploadResult, akka.NotUsed] = { 196 | 197 | def formatKey(fileNb: Long) = { 198 | val pad = "%08d".format(fileNb) 199 | s"$prefix.part.$pad" 200 | } 201 | 202 | uploadStreamAsMultipartFile(bucket, i => formatKey(i / nbChunkPerFile), nbChunkPerFile, chunkUploadConcurrency) 203 | } 204 | 205 | /** 206 | * Upload a stream as a multipart file with a given number of upstream chunk per file. Part file are uploaded sequentially but 207 | * chunk inside a part file can be uploaded concurrently (tuned with chunkUploadConcurrency). 208 | * The Flow returns a CompleteMultipartUploadResult for each part file uploaded. 209 | * @param bucket S3 bucket 210 | * @param getKey S3 key factory that will generate a new key for each part file based on the index of upstream chunks... 211 | * @param nbChunkPerFile the number of upstream chunks (for example lines) to include in each part file 212 | * @param chunkUploadConcurrency chunk upload concurrency. Order is guaranteed even with chunkUploadConcurrency > 1. 213 | */ 214 | def uploadStreamAsMultipartFile(bucket: String, getKey: Long => String, nbChunkPerFile: Int, 215 | chunkUploadConcurrency : Int): Flow[ByteString, CompleteMultipartUploadResult, akka.NotUsed] = { 216 | 217 | Flow[ByteString] 218 | .via(FlowExt.zipWithIndex) 219 | .splitWhen { x => 220 | val i = x._2 221 | i != 0 && i % nbChunkPerFile == 0 222 | } 223 | .via( 224 | FlowExt.withHead(includeHeadInUpStream = true) { case (_, i) => 225 | Flow[(ByteString, Long)].map(_._1).via(uploadStreamAsFile(bucket, getKey(i), chunkUploadConcurrency)) 226 | } 227 | ) 228 | .concatSubstreams 229 | } 230 | def uploadStreamAsMultipartFile(bucket: String, getKey: Long => String, nbChunkPerFile: Int) : Flow[ByteString, CompleteMultipartUploadResult, akka.NotUsed] = 231 | uploadStreamAsMultipartFile(bucket,getKey,nbChunkPerFile,1) 232 | } 233 | -------------------------------------------------------------------------------- /s3/src/main/scala/AmazonS3ClientMaterialized.scala: -------------------------------------------------------------------------------- 1 | package com.mfglabs.commons.aws 2 | package s3 3 | 4 | import akka.stream._ 5 | import akka.stream.scaladsl._ 6 | import akka.util.ByteString 7 | import com.amazonaws.services.s3.model.{DeleteObjectsResult, S3ObjectSummary} 8 | import scala.concurrent.Future 9 | 10 | /** 11 | * Additional functions which materialize stream to Future. 12 | */ 13 | class AmazonS3ClientMaterialized( 14 | client : com.amazonaws.services.s3.AmazonS3, 15 | executorService : java.util.concurrent.ExecutorService, 16 | implicit val flowMaterializer : ActorMaterializer 17 | ) extends AmazonS3Client(client, executorService) { 18 | 19 | def deleteObjects(bucket: String, commonPrefix: String): Future[Seq[DeleteObjectsResult.DeletedObject]] = { 20 | for { 21 | objects <- listFiles(bucket, Some(commonPrefix)) 22 | deleted <- deleteObjects(bucket, objects.map(_.getKey):_*) 23 | } yield deleted 24 | } 25 | 26 | /** List all files with a given prefix of a S3 bucket. 27 | * 28 | * @param bucket the bucket name 29 | * @param path an optional path to search in bucket 30 | * @return a future of seq of file keys & last modified dates (or a failure) 31 | */ 32 | def listFiles(bucket: String, path: Option[String] = None): Future[Seq[S3ObjectSummary]] = { 33 | listFilesAsStream(bucket, path).runWith(Sink.seq) 34 | } 35 | 36 | /** 37 | * Get a S3 file. 38 | * @param bucket 39 | * @param key 40 | * @return binary file 41 | */ 42 | def getFile(bucket: String, key: String): Future[ByteString] = { 43 | getFileAsStream(bucket, key).runFold(ByteString.empty)(_ ++ _).map(_.compact) 44 | } 45 | 46 | override def shutdown(): Unit = { 47 | flowMaterializer.shutdown() 48 | super.shutdown() 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /s3/src/main/scala/AmazonS3Wrapper.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Pellucid Analytics 3 | * Copyright 2015 Daniel W. H. James 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.mfglabs.commons.aws 19 | package s3 20 | 21 | import java.io.{InputStream, File} 22 | import java.net.URL 23 | 24 | import scala.collection.JavaConverters._ 25 | import scala.concurrent.{ExecutionContext, Future} 26 | 27 | import com.amazonaws.services.s3._ 28 | import com.amazonaws.services.s3.model._ 29 | 30 | /** 31 | * A lightweight wrapper for [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3Client.html AmazonS3Client]] 32 | * 33 | * The AWS Java SDK does not provide an asynchronous S3 client, 34 | * so this class follows the approach of the asynchronous clients 35 | * that are provided by the SDK. Namely, to make the synchronous 36 | * calls within an executor service. The methods in this class 37 | * all return Scala futures. 38 | * 39 | * @param client: the underlying AmazonS3Client 40 | * @param clientConfiguration 41 | * @param executorService 42 | * an executor service for synchronous calls to the underlying AmazonS3Client. 43 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3Client.html AmazonS3Client]] 44 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/AWSCredentialsProvider.html AWSCredentialsProvider]] 45 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/ClientConfiguration.html ClientConfiguration]] 46 | * @see java.util.concurrent.ExecutorService 47 | */ 48 | trait AmazonS3Wrapper extends FutureHelper.MethodWrapper { 49 | def client : AmazonS3 50 | def executorService : java.util.concurrent.ExecutorService 51 | 52 | implicit val ec = ExecutionContext.fromExecutorService(executorService) 53 | 54 | /** 55 | * Shutdown the client and the executor service. 56 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/AmazonWebServiceClient.html#shutdown() AmazonWebServiceClient.shutdown()]] 57 | */ 58 | def shutdown(): Unit = { 59 | client.shutdown() 60 | executorService.shutdownNow() 61 | () 62 | } 63 | 64 | /** 65 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#abortMultipartUpload(com.amazonaws.services.s3.model.AbortMultipartUploadRequest) AWS Java SDK]] 66 | */ 67 | def abortMultipartUpload(req: AbortMultipartUploadRequest): Future[Unit] = 68 | wrapMethod[AbortMultipartUploadRequest, Unit](client.abortMultipartUpload _, req) 69 | 70 | /** 71 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#completeMultipartUpload(com.amazonaws.services.s3.model.CompleteMultipartUploadRequest) AWS Java SDK]] 72 | */ 73 | def completeMultipartUpload( 74 | completeMultipartUploadRequest: CompleteMultipartUploadRequest 75 | ): Future[CompleteMultipartUploadResult] = 76 | wrapMethod(client.completeMultipartUpload, completeMultipartUploadRequest) 77 | 78 | /** 79 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#copyObject(com.amazonaws.services.s3.model.CopyObjectRequest) AWS Java SDK]] 80 | */ 81 | def copyObject( 82 | copyObjectRequest: CopyObjectRequest 83 | ): Future[CopyObjectResult] = 84 | wrapMethod(client.copyObject, copyObjectRequest) 85 | 86 | /** 87 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#copyObject(com.amazonaws.services.s3.model.CopyObjectRequest) AWS Java SDK]] 88 | */ 89 | def copyObject( 90 | sourceBucketName: String, 91 | sourceKey: String, 92 | destinationBucketName: String, 93 | destinationKey: String 94 | ): Future[CopyObjectResult] = 95 | copyObject(new CopyObjectRequest(sourceBucketName, sourceKey, destinationBucketName, destinationKey)) 96 | 97 | /** 98 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#copyPart(com.amazonaws.services.s3.model.CopyPartRequest) AWS Java SDK]] 99 | */ 100 | def copyPart( 101 | copyPartRequest: CopyPartRequest 102 | ): Future[CopyPartResult] = 103 | wrapMethod(client.copyPart, copyPartRequest) 104 | 105 | /** 106 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#createBucket(com.amazonaws.services.s3.model.CreateBucketRequest) AWS Java SDK]] 107 | */ 108 | def createBucket( 109 | createBucketRequest: CreateBucketRequest 110 | ): Future[Bucket] = 111 | wrapMethod[CreateBucketRequest, Bucket](client.createBucket, createBucketRequest) 112 | 113 | /** 114 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#createBucket(com.amazonaws.services.s3.model.CreateBucketRequest) AWS Java SDK]] 115 | */ 116 | def createBucket( 117 | bucketName: String 118 | ): Future[Bucket] = 119 | createBucket(new CreateBucketRequest(bucketName)) 120 | 121 | /** 122 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#createBucket(com.amazonaws.services.s3.model.CreateBucketRequest) AWS Java SDK]] 123 | */ 124 | def createBucket( 125 | bucketName: String, 126 | region: Region 127 | ): Future[Bucket] = 128 | createBucket(new CreateBucketRequest(bucketName, region)) 129 | 130 | /** 131 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#createBucket(com.amazonaws.services.s3.model.CreateBucketRequest) AWS Java SDK]] 132 | */ 133 | def createBucket( 134 | bucketName: String, 135 | region: String 136 | ): Future[Bucket] = 137 | createBucket(new CreateBucketRequest(bucketName, region)) 138 | 139 | /** 140 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#deleteBucket(com.amazonaws.services.s3.model.DeleteBucketRequest) AWS Java SDK]] 141 | */ 142 | def deleteBucket( 143 | deleteBucketRequest: DeleteBucketRequest 144 | ): Future[Unit] = 145 | wrapMethod[DeleteBucketRequest, Unit](client.deleteBucket, deleteBucketRequest) 146 | 147 | /** 148 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#deleteBucket(com.amazonaws.services.s3.model.DeleteBucketRequest) AWS Java SDK]] 149 | */ 150 | def deleteBucket( 151 | bucketName: String 152 | ): Future[Unit] = 153 | deleteBucket(new DeleteBucketRequest(bucketName)) 154 | 155 | /** 156 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#deleteObject(com.amazonaws.services.s3.model.DeleteObjectRequest) AWS Java SDK]] 157 | */ 158 | def deleteObject( 159 | deleteObjectRequest: DeleteObjectRequest 160 | ): Future[Unit] = 161 | wrapMethod(client.deleteObject, deleteObjectRequest) 162 | 163 | /** 164 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#deleteObject(com.amazonaws.services.s3.model.DeleteObjectRequest) AWS Java SDK]] 165 | */ 166 | def deleteObject( 167 | bucketName: String, 168 | key: String 169 | ): Future[Unit] = 170 | deleteObject(new DeleteObjectRequest(bucketName, key)) 171 | 172 | /** 173 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#deleteObjects(com.amazonaws.services.s3.model.DeleteObjectsRequest) AWS Java SDK]] 174 | */ 175 | def deleteObjects( 176 | deleteObjectsRequest: DeleteObjectsRequest 177 | ): Future[Seq[DeleteObjectsResult.DeletedObject]] = 178 | wrapMethod((req: DeleteObjectsRequest) => client.deleteObjects(req).getDeletedObjects.asScala.toSeq, deleteObjectsRequest) 179 | 180 | /** 181 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#deleteObjects(com.amazonaws.services.s3.model.DeleteObjectsRequest) AWS Java SDK]] 182 | */ 183 | def deleteObjects( 184 | bucketName: String, 185 | keys: String* 186 | ): Future[Seq[DeleteObjectsResult.DeletedObject]] = { 187 | deleteObjects(new DeleteObjectsRequest(bucketName).withKeys(keys:_ *)) 188 | } 189 | 190 | /** 191 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#deleteVersion(com.amazonaws.services.s3.model.DeleteVersionRequest) AWS Java SDK]] 192 | */ 193 | def deleteVersion( 194 | deleteVersionRequest: DeleteVersionRequest 195 | ): Future[Unit] = 196 | wrapMethod(client.deleteVersion, deleteVersionRequest) 197 | 198 | /** 199 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#deleteVersion(com.amazonaws.services.s3.model.DeleteVersionRequest) AWS Java SDK]] 200 | */ 201 | def deleteVersion( 202 | bucketName: String, 203 | key: String, 204 | versionId: String 205 | ): Future[Unit] = 206 | deleteVersion(new DeleteVersionRequest(bucketName, key, versionId)) 207 | 208 | /** 209 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#doesBucketExist(java.lang.String) AWS Java SDK]] 210 | */ 211 | def doesBucketExist( 212 | bucketName: String 213 | ): Future[Boolean] = 214 | wrapMethod(client.doesBucketExist, bucketName) 215 | 216 | /** 217 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#getBucketLocation(com.amazonaws.services.s3.model.GetBucketLocationRequest) AWS Java SDK]] 218 | */ 219 | def getBucketLocation( 220 | getBucketLocationRequest: GetBucketLocationRequest 221 | ): Future[String] = 222 | wrapMethod[GetBucketLocationRequest, String](client.getBucketLocation, getBucketLocationRequest) 223 | 224 | /** 225 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#getBucketLocation(com.amazonaws.services.s3.model.GetBucketLocationRequest) AWS Java SDK]] 226 | */ 227 | def getBucketLocation( 228 | bucketName: String 229 | ): Future[String] = 230 | getBucketLocation(new GetBucketLocationRequest(bucketName)) 231 | 232 | /** 233 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#generatePresignedUrl(com.amazonaws.services.s3.model.GeneratePresignedUrlRequest) AWS Java SDK]] 234 | */ 235 | def generatePresignedUrlRequest( 236 | generatePresignedUrlRequest: GeneratePresignedUrlRequest 237 | ): Future[URL] = 238 | wrapMethod(client.generatePresignedUrl, generatePresignedUrlRequest) 239 | 240 | /** 241 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#getObject(com.amazonaws.services.s3.model.GetObjectRequest) AWS Java SDK]] 242 | */ 243 | def getObject( 244 | getObjectRequest: GetObjectRequest 245 | ): Future[S3Object] = 246 | wrapMethod[GetObjectRequest, S3Object](client.getObject, getObjectRequest) 247 | 248 | /** 249 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#getObject(com.amazonaws.services.s3.model.GetObjectRequest) AWS Java SDK]] 250 | */ 251 | def getObject( 252 | bucketName: String, 253 | key: String 254 | ): Future[S3Object] = 255 | getObject(new GetObjectRequest(bucketName, key)) 256 | 257 | /** 258 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#getObject(com.amazonaws.services.s3.model.GetObjectRequest) AWS Java SDK]] 259 | */ 260 | def getObject( 261 | getObjectRequest: GetObjectRequest, 262 | destinationFile: File 263 | ): Future[ObjectMetadata] = 264 | wrapMethod[(GetObjectRequest, File), ObjectMetadata]({ case (r: GetObjectRequest, f: File) => client.getObject(r, f) }, (getObjectRequest, destinationFile)) 265 | 266 | /** 267 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#getObjectMetadata(com.amazonaws.services.s3.model.GetObjectMetadataRequest) AWS Java SDK]] 268 | */ 269 | def getObjectMetadata( 270 | getObjectMetadataRequest: GetObjectMetadataRequest 271 | ): Future[ObjectMetadata] = 272 | wrapMethod(client.getObjectMetadata, getObjectMetadataRequest) 273 | 274 | /** 275 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#getObjectMetadata(com.amazonaws.services.s3.model.GetObjectMetadataRequest) AWS Java SDK]] 276 | */ 277 | def getObjectMetadata( 278 | bucketName: String, 279 | key: String 280 | ): Future[ObjectMetadata] = 281 | getObjectMetadata(new GetObjectMetadataRequest(bucketName, key)) 282 | 283 | /** 284 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#initiateMultipartUpload(com.amazonaws.services.s3.model.InitiateMultipartUploadRequest) AWS Java SDK]] 285 | */ 286 | def initiateMultipartUpload( 287 | initiateMultipartUploadRequest: InitiateMultipartUploadRequest 288 | ): Future[InitiateMultipartUploadResult] = 289 | wrapMethod(client.initiateMultipartUpload, initiateMultipartUploadRequest) 290 | 291 | /** 292 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#listBuckets(com.amazonaws.services.s3.model.ListBucketsRequest) AWS Java SDK]] 293 | */ 294 | def listBuckets( 295 | listBucketsRequest: ListBucketsRequest 296 | ): Future[Seq[Bucket]] = 297 | wrapMethod((req: ListBucketsRequest) => client.listBuckets(req).asScala.toSeq, listBucketsRequest) 298 | 299 | /** 300 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#listBuckets(com.amazonaws.services.s3.model.ListBucketsRequest) AWS Java SDK]] 301 | */ 302 | def listBuckets(): Future[Seq[Bucket]] = 303 | listBuckets(new ListBucketsRequest()) 304 | 305 | /** 306 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#listNextBatchOfObjects(com.amazonaws.services.s3.model.ListNextBatchOfObjectsRequest)]] 307 | */ 308 | def listNextBatchOfObjects(req: ListNextBatchOfObjectsRequest): Future[ObjectListing] = 309 | wrapMethod[ListNextBatchOfObjectsRequest, ObjectListing](client.listNextBatchOfObjects, req) 310 | 311 | /** 312 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#listNextBatchOfObjects(com.amazonaws.services.s3.model.ObjectListing) AWS Java SDK]] 313 | */ 314 | def listNextBatchOfObjects(req: ObjectListing): Future[ObjectListing] = 315 | wrapMethod[ObjectListing, ObjectListing](client.listNextBatchOfObjects _, req) 316 | 317 | /** 318 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#listObjects(com.amazonaws.services.s3.model.ListObjectsRequest) AWS Java SDK]] 319 | */ 320 | def listObjects( 321 | listObjectsRequest: ListObjectsRequest 322 | ): Future[ObjectListing] = 323 | wrapMethod[ListObjectsRequest, ObjectListing](client.listObjects, listObjectsRequest) 324 | 325 | /** 326 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#listObjects(com.amazonaws.services.s3.model.ListObjectsRequest) AWS Java SDK]] 327 | */ 328 | def listObjects( 329 | bucketName: String 330 | ): Future[ObjectListing] = 331 | listObjects( 332 | new ListObjectsRequest() 333 | .withBucketName(bucketName) 334 | ) 335 | 336 | /** 337 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#listObjects(com.amazonaws.services.s3.model.ListObjectsRequest) AWS Java SDK]] 338 | */ 339 | def listObjects( 340 | bucketName: String, 341 | prefix: String 342 | ): Future[ObjectListing] = 343 | listObjects( 344 | new ListObjectsRequest() 345 | .withBucketName(bucketName) 346 | .withPrefix(prefix) 347 | ) 348 | 349 | /** 350 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#listVersions(com.amazonaws.services.s3.model.ListVersionsRequest) AWS Java SDK]] 351 | */ 352 | def listVersions( 353 | listVersionsRequest: ListVersionsRequest 354 | ): Future[VersionListing] = 355 | wrapMethod(client.listVersions, listVersionsRequest) 356 | 357 | /** 358 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#listVersions(com.amazonaws.services.s3.model.ListVersionsRequest) AWS Java SDK]] 359 | */ 360 | def listVersions( 361 | bucketName: String, 362 | prefix: String 363 | ): Future[VersionListing] = 364 | listVersions( 365 | new ListVersionsRequest() 366 | .withBucketName(bucketName) 367 | .withPrefix(prefix) 368 | ) 369 | 370 | /** 371 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#listVersions(com.amazonaws.services.s3.model.ListVersionsRequest) AWS Java SDK]] 372 | */ 373 | def listVersions( 374 | bucketName: String, 375 | prefix: String, 376 | keyMarker: String, 377 | versionIdMarker: String, 378 | delimiter: String, 379 | maxKeys: Int 380 | ): Future[VersionListing] = 381 | listVersions(new ListVersionsRequest(bucketName, prefix, keyMarker, versionIdMarker, delimiter, maxKeys)) 382 | 383 | /** 384 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#putObject(com.amazonaws.services.s3.model.PutObjectRequest) AWS Java SDK]] 385 | */ 386 | def putObject( 387 | putObjectRequest: PutObjectRequest 388 | ): Future[PutObjectResult] = 389 | wrapMethod[PutObjectRequest, PutObjectResult](client.putObject, putObjectRequest) 390 | 391 | /** 392 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#putObject(com.amazonaws.services.s3.model.PutObjectRequest) AWS Java SDK]] 393 | */ 394 | def putObject( 395 | bucketName: String, 396 | key: String, 397 | file: File 398 | ): Future[PutObjectResult] = 399 | putObject(new PutObjectRequest(bucketName, key, file)) 400 | 401 | /** 402 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#putObject(com.amazonaws.services.s3.model.PutObjectRequest) AWS Java SDK]] 403 | */ 404 | def putObject( 405 | bucketName: String, 406 | key: String, 407 | input: InputStream, 408 | metadata: ObjectMetadata 409 | ): Future[PutObjectResult] = 410 | putObject(new PutObjectRequest(bucketName, key, input, metadata)) 411 | 412 | /** 413 | * @see [[http://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/AmazonS3.html#uploadPart(com.amazonaws.services.s3.model.UploadPartRequest) AWS Java SDK]] 414 | */ 415 | def uploadPart(req: UploadPartRequest): Future[UploadPartResult] = 416 | wrapMethod[UploadPartRequest, UploadPartResult](client.uploadPart _, req) 417 | 418 | } 419 | -------------------------------------------------------------------------------- /s3/src/main/scala/futureTransfer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Pellucid Analytics 3 | * Copyright 2015 Daniel W. H. James 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.mfglabs.commons.aws 19 | package s3 20 | 21 | import scala.concurrent.{Future, Promise} 22 | 23 | import com.amazonaws.event.{ProgressListener, ProgressEvent, ProgressEventType} 24 | import com.amazonaws.services.s3.transfer.Transfer 25 | 26 | import org.slf4j.{Logger, LoggerFactory} 27 | 28 | 29 | /** 30 | * A helper object providing a Scala Future interface for S3 Transfers. 31 | * 32 | * Transfers to and from S3 using the TransferManager provider a listener 33 | * interface, and [[FutureTransfer.listenFor]] adapts this interface to 34 | * Scala futures. 35 | * 36 | * @see [[com.amazonaws.services.s3.transfer.TransferManager TransferManager]] 37 | * @see [[com.amazonaws.services.s3.transfer.Transfer Transfer]] 38 | */ 39 | object FutureTransfer { 40 | 41 | private val logger: Logger = LoggerFactory.getLogger("com.mfglabs.commons.aws.s3.FutureTransfer") 42 | 43 | /** 44 | * Attach a listener to an S3 Transfer and return it as a Future. 45 | * 46 | * This helper method attaches a progress and state change listeners to the given 47 | * Transfer object. The returned future is completed with the 48 | * same transfer when the transfer is ‘done’ (canceled, completed, 49 | * or failed). The future will always been completed successfully 50 | * even if the transfer itself has failed. It is up to the caller 51 | * to extract the result of the transfer and perform any error 52 | * handling. 53 | * 54 | * In essence, this helper just gives back the transfer when it is done. 55 | * 56 | * The detailed progress of the transfer is logged at debug level to the 57 | * `com.github.dwhjames.awswrap.s3.FutureTransfer` logger. 58 | * 59 | * @tparam T 60 | * a subtype of Transfer. 61 | * @param transfer 62 | * an S3 Transfer to listen for progress. 63 | * @return the transfer in a future. 64 | * @see [[com.amazonaws.services.s3.transfer.Transfer Transfer]] 65 | * @see [[com.amazonaws.event.ProgressListener ProgressListener]] 66 | */ 67 | def listenFor[T <: Transfer](transfer: T): Future[transfer.type] = { 68 | import com.amazonaws.services.s3.transfer.internal.{ AbstractTransfer, TransferStateChangeListener } 69 | val transferDescription = transfer.getDescription 70 | def debugLog(eventType: String): Unit = { 71 | logger.debug(s"$eventType : $transferDescription") 72 | } 73 | def logTransferState(state: Transfer.TransferState): Unit = { 74 | if (logger.isDebugEnabled) { 75 | state match { 76 | case Transfer.TransferState.Waiting => 77 | debugLog("Waiting") 78 | case Transfer.TransferState.InProgress => 79 | debugLog("InProgress") 80 | case Transfer.TransferState.Completed => 81 | debugLog("Completed") 82 | case Transfer.TransferState.Canceled => 83 | debugLog("Canceled") 84 | case Transfer.TransferState.Failed => 85 | debugLog("Failed") 86 | case _ => 87 | logger.warn(s"unrecognized transfer state for transfer $transferDescription") 88 | } 89 | } 90 | } 91 | def logProgressEvent(progressEvent: ProgressEvent): Unit = { 92 | if (logger.isDebugEnabled) { 93 | progressEvent.getEventType match { 94 | case ProgressEventType.CLIENT_REQUEST_FAILED_EVENT => 95 | debugLog("CLIENT_REQUEST_FAILED_EVENT") 96 | case ProgressEventType.CLIENT_REQUEST_RETRY_EVENT => 97 | debugLog("CLIENT_REQUEST_RETRY_EVENT") 98 | case ProgressEventType.CLIENT_REQUEST_STARTED_EVENT => 99 | debugLog("CLIENT_REQUEST_STARTED_EVENT") 100 | case ProgressEventType.CLIENT_REQUEST_SUCCESS_EVENT => 101 | debugLog("CLIENT_REQUEST_SUCCESS_EVENT") 102 | case ProgressEventType.HTTP_REQUEST_COMPLETED_EVENT => 103 | debugLog("HTTP_REQUEST_COMPLETED_EVENT") 104 | case ProgressEventType.HTTP_REQUEST_CONTENT_RESET_EVENT => 105 | debugLog("HTTP_REQUEST_CONTENT_RESET_EVENT") 106 | case ProgressEventType.HTTP_REQUEST_STARTED_EVENT => 107 | debugLog("HTTP_REQUEST_STARTED_EVENT") 108 | case ProgressEventType.HTTP_RESPONSE_COMPLETED_EVENT => 109 | debugLog("HTTP_RESPONSE_COMPLETED_EVENT") 110 | case ProgressEventType.HTTP_RESPONSE_CONTENT_RESET_EVENT => 111 | debugLog("HTTP_RESPONSE_CONTENT_RESET_EVENT") 112 | case ProgressEventType.HTTP_RESPONSE_STARTED_EVENT => 113 | debugLog("HTTP_RESPONSE_STARTED_EVENT") 114 | case ProgressEventType.REQUEST_BYTE_TRANSFER_EVENT => 115 | debugLog("REQUEST_BYTE_TRANSFER_EVENT") 116 | case ProgressEventType.REQUEST_CONTENT_LENGTH_EVENT => 117 | debugLog("REQUEST_CONTENT_LENGTH_EVENT") 118 | case ProgressEventType.RESPONSE_BYTE_DISCARD_EVENT => 119 | debugLog("RESPONSE_BYTE_DISCARD_EVENT") 120 | case ProgressEventType.RESPONSE_BYTE_TRANSFER_EVENT => 121 | debugLog("RESPONSE_BYTE_TRANSFER_EVENT") 122 | case ProgressEventType.RESPONSE_CONTENT_LENGTH_EVENT => 123 | debugLog("RESPONSE_CONTENT_LENGTH_EVENT") 124 | case ProgressEventType.TRANSFER_CANCELED_EVENT => 125 | debugLog("TRANSFER_CANCELED_EVENT") 126 | case ProgressEventType.TRANSFER_COMPLETED_EVENT => 127 | debugLog("TRANSFER_COMPLETED_EVENT") 128 | case ProgressEventType.TRANSFER_FAILED_EVENT => 129 | debugLog("TRANSFER_FAILED_EVENT") 130 | case ProgressEventType.TRANSFER_PART_COMPLETED_EVENT => 131 | debugLog("TRANSFER_PART_COMPLETED_EVENT") 132 | case ProgressEventType.TRANSFER_PART_FAILED_EVENT => 133 | debugLog("TRANSFER_PART_FAILED_EVENT") 134 | case ProgressEventType.TRANSFER_PART_STARTED_EVENT => 135 | debugLog("TRANSFER_PART_STARTED_EVENT") 136 | case ProgressEventType.TRANSFER_PREPARING_EVENT => 137 | debugLog("TRANSFER_PREPARING_EVENT") 138 | case ProgressEventType.TRANSFER_STARTED_EVENT => 139 | debugLog("TRANSFER_STARTED_EVENT") 140 | case _ => 141 | logger.warn(s"unrecognized progress event type for transfer $transferDescription") 142 | } 143 | } 144 | } 145 | 146 | val p = Promise[transfer.type] 147 | 148 | if (transfer.isInstanceOf[AbstractTransfer]) { 149 | /* Attach a state change listener to the transfer. 150 | * At this point, the transfer is already in progress 151 | * and may even have already completed. We will have 152 | * missed any state change events that have already been 153 | * fired, including the completion event! 154 | */ 155 | transfer.asInstanceOf[AbstractTransfer].addStateChangeListener(new TransferStateChangeListener { 156 | /* Note that the transferStateChanged will be called in the Java SDK’s 157 | * special thread for callbacks, so any blocking calls here have 158 | * the potential to induce deadlock. 159 | */ 160 | override def transferStateChanged(t: Transfer, state: Transfer.TransferState): Unit = { 161 | logTransferState(state) 162 | 163 | if (state == Transfer.TransferState.Completed || 164 | state == Transfer.TransferState.Canceled || 165 | state == Transfer.TransferState.Failed) { 166 | val success = p trySuccess transfer 167 | if (logger.isDebugEnabled) { 168 | if (success) { 169 | logger.debug(s"promise successfully completed from transfer state change listener for $transferDescription") 170 | } 171 | } 172 | } 173 | } 174 | }) 175 | } 176 | 177 | /* Attach a progress listener to the transfer. 178 | * At this point, the transfer is already in progress 179 | * and may even have already completed. We will have 180 | * missed any progress events that have already been 181 | * fired, including the completion event! 182 | */ 183 | transfer.addProgressListener(new ProgressListener { 184 | /* Note that the progressChanged will be called in the Java SDK’s 185 | * `java-sdk-progress-listener-callback-thread` special thread 186 | * for progress event callbacks, so any blocking calls here have 187 | * the potential to induce deadlock. 188 | */ 189 | override def progressChanged(progressEvent: ProgressEvent): Unit = { 190 | logProgressEvent(progressEvent) 191 | 192 | val code = progressEvent.getEventType() 193 | if (code == ProgressEventType.TRANSFER_CANCELED_EVENT || 194 | code == ProgressEventType.TRANSFER_COMPLETED_EVENT || 195 | code == ProgressEventType.TRANSFER_FAILED_EVENT) { 196 | val success = p trySuccess transfer 197 | if (logger.isDebugEnabled) { 198 | if (success) { 199 | logger.debug(s"promise successfully completed from progress listener for $transferDescription") 200 | } 201 | } 202 | } 203 | } 204 | }) 205 | 206 | /* In case the progress listener never fires due to the 207 | * transfer already being done, poll the transfer once. 208 | */ 209 | if (transfer.isDone) { 210 | val success = p trySuccess transfer 211 | if (logger.isDebugEnabled) { 212 | if (success) { 213 | logger.debug(s"promise successfully completed from outside of callbacks for $transferDescription") 214 | } 215 | } 216 | } 217 | 218 | p.future 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /s3/src/test/resources/medium.txt: -------------------------------------------------------------------------------- 1 | m 2 | f 3 | g 4 | labs 5 | a 6 | b 7 | s 8 | -------------------------------------------------------------------------------- /s3/src/test/resources/small.txt: -------------------------------------------------------------------------------- 1 | This is toto. 2 | -------------------------------------------------------------------------------- /s3/src/test/scala/S3Spec.scala: -------------------------------------------------------------------------------- 1 | package com.mfglabs.commons.aws 2 | package s3 3 | 4 | import akka.actor.ActorSystem 5 | import akka.stream._ 6 | import akka.stream.scaladsl._ 7 | import akka.util.ByteString 8 | import com.amazonaws.services.s3.model.{AmazonS3Exception, CompleteMultipartUploadResult} 9 | import com.mfglabs.stream._ 10 | import org.scalatest._ 11 | import concurrent.ScalaFutures 12 | import org.scalatest.time.{Minutes, Millis, Span} 13 | 14 | import scala.concurrent._ 15 | import scala.concurrent.duration._ 16 | 17 | class S3Spec extends FlatSpec with Matchers with ScalaFutures with BeforeAndAfterAll { 18 | import s3._ 19 | import scala.concurrent.ExecutionContext.Implicits.global 20 | 21 | val bucket = "mfg-commons-aws" 22 | val keyPrefix = "test/core" 23 | val multipartkeyPrefix = "test/multipart-upload-core" 24 | 25 | implicit override val patienceConfig = 26 | PatienceConfig(timeout = Span(3, Minutes), interval = Span(20, Millis)) 27 | 28 | implicit val system = ActorSystem() 29 | implicit val fm = ActorMaterializer() 30 | 31 | val s3Client = AmazonS3Client.from( 32 | com.amazonaws.regions.Regions.EU_WEST_1, 33 | new com.amazonaws.auth.profile.ProfileCredentialsProvider("mfg") 34 | )().materialized(fm) 35 | 36 | it should "upload/list/delete small files" in { 37 | s3Client.deleteObjects(bucket, s"$keyPrefix").futureValue 38 | s3Client.putObject(bucket, s"$keyPrefix/small.txt", new java.io.File(getClass.getResource("/small.txt").getPath)).futureValue 39 | val l = s3Client.listFiles(bucket, Some(keyPrefix)).futureValue 40 | s3Client.deleteObjects(bucket, s"$keyPrefix/small.txt").futureValue 41 | val l2 = s3Client.listFiles(bucket, Some(keyPrefix)).futureValue 42 | 43 | (l map (_.getKey)) should equal(List(s"$keyPrefix/small.txt")) 44 | l2 should be('empty) 45 | } 46 | 47 | it should "throw an exception when a file is non-existent" in { 48 | val source = s3Client.getFileAsStream(bucket, "foo") 49 | intercept[AmazonS3Exception] { 50 | Await.result(source.runFold(ByteString.empty)(_ ++ _).map(_.compact), 5 seconds) 51 | } 52 | } 53 | 54 | it should "upload and download a big file as a single file" in { 55 | val futBytes = StreamConverters.fromInputStream(() => getClass.getResourceAsStream("/big.txt")) 56 | .via(s3Client.uploadStreamAsFile(bucket, s"$keyPrefix/big", chunkUploadConcurrency = 2)) 57 | .flatMapConcat(_ => s3Client.getFileAsStream(bucket, s"$keyPrefix/big")) 58 | .runFold(ByteString.empty)(_ ++ _) 59 | .map(_.compact) 60 | 61 | val expectedBytes = StreamConverters.fromInputStream(() => getClass.getResourceAsStream("/big.txt")) 62 | .runFold(ByteString.empty)(_ ++ _).map(_.compact) 63 | 64 | whenReady(futBytes zip expectedBytes) { case (bytes, expectedBytes) => 65 | bytes shouldEqual expectedBytes 66 | } 67 | } 68 | 69 | it should "download a big file and chunk it by line" in { 70 | val futLines = s3Client.getFileAsStream(bucket, s"$keyPrefix/big") 71 | .via(FlowExt.rechunkByteStringBySize(2 * 1024 * 1024)) 72 | .via(FlowExt.rechunkByteStringBySeparator(ByteString("\n"), 8 * 1024)) 73 | .map(_.utf8String) 74 | .runWith(Sink.seq) 75 | 76 | val futExpectedLines = StreamConverters 77 | .fromInputStream(() => getClass.getResourceAsStream("/big.txt")) 78 | .runFold(ByteString.empty)(_ ++ _) 79 | .map(_.compact.utf8String) 80 | .map(_.split("\n").to[scala.collection.immutable.Seq]) 81 | 82 | whenReady(futLines zip futExpectedLines) { case (lines, expectedLines) => 83 | lines shouldEqual expectedLines 84 | } 85 | } 86 | 87 | 88 | it should "upload and download a big file as a multipart file" in { 89 | val bytes = StreamConverters 90 | .fromInputStream(() => getClass.getResourceAsStream("/big.txt"), chunkSize = 2 * 1024 * 1024) 91 | .via(s3Client.uploadStreamAsMultipartFile(bucket, s"$keyPrefix/big", nbChunkPerFile = 1, chunkUploadConcurrency = 2)) 92 | .via(FlowExt.fold[CompleteMultipartUploadResult, Vector[CompleteMultipartUploadResult]](Vector.empty)(_ :+ _)) 93 | .flatMapConcat(_ => s3Client.getMultipartFileAsStream(bucket, s"$keyPrefix/big.part")) 94 | .runFold(ByteString.empty)(_ ++ _) 95 | .map(_.compact).futureValue 96 | 97 | val expectedBytes = StreamConverters 98 | .fromInputStream(() => getClass.getResourceAsStream("/big.txt")) 99 | .runFold(ByteString.empty)(_ ++ _).map(_.compact).futureValue 100 | 101 | bytes shouldEqual expectedBytes 102 | } 103 | 104 | 105 | override def afterAll() = { 106 | s3Client.shutdown() 107 | val _ = system.terminate().futureValue 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /scaladoc.sbt: -------------------------------------------------------------------------------- 1 | import scala.util.matching.Regex.Match 2 | 3 | packageDoc in Compile <<= packageDoc in ScalaUnidoc 4 | 5 | artifact in (ScalaUnidoc, packageDoc) := { 6 | val previous: Artifact = (artifact in (ScalaUnidoc, packageDoc)).value 7 | previous.copy(classifier = Some("javadoc")) 8 | } 9 | 10 | scalacOptions in (Compile, doc) ++= Seq( 11 | "-implicits", 12 | "-sourcepath", baseDirectory.value.getAbsolutePath, 13 | "-doc-source-url", s"https://github.com/MfgLabs/commons-aws/tree/${version.value}€{FILE_PATH}.scala" 14 | ) 15 | 16 | autoAPIMappings := true 17 | 18 | apiURL := Some(url("https://MfgLabs.github.io/commons-aws/api/current/")) 19 | -------------------------------------------------------------------------------- /sqs/src/main/scala/AmazonSQSClient.scala: -------------------------------------------------------------------------------- 1 | package com.mfglabs.commons.aws 2 | package sqs 3 | 4 | import akka.actor._ 5 | import akka.stream.scaladsl._ 6 | import com.amazonaws.client.builder.ExecutorFactory 7 | import com.amazonaws.services.sqs.model._ 8 | import com.mfglabs.stream._ 9 | import java.util.concurrent.ExecutorService 10 | import scala.concurrent.ExecutionContext 11 | import scala.concurrent.duration._ 12 | 13 | 14 | object AmazonSQSClient { 15 | import com.amazonaws.auth._ 16 | import com.amazonaws.ClientConfiguration 17 | import com.amazonaws.regions.Regions 18 | import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder 19 | import FutureHelper.defaultExecutorService 20 | 21 | def apply( 22 | region : Regions, 23 | awsCredentials : AWSCredentials, 24 | clientConfiguration : ClientConfiguration = new ClientConfiguration() 25 | )( 26 | executorService : ExecutorService = defaultExecutorService(clientConfiguration, "aws.wrap.sqs") 27 | ): AmazonSQSClient = { 28 | from(region, new AWSStaticCredentialsProvider(awsCredentials), clientConfiguration)(executorService) 29 | } 30 | 31 | def from( 32 | region : Regions, 33 | awsCredentialsProvider : AWSCredentialsProvider = new DefaultAWSCredentialsProviderChain, 34 | clientConfiguration : ClientConfiguration = new ClientConfiguration() 35 | )( 36 | executorService : ExecutorService = defaultExecutorService(clientConfiguration, "aws.wrap.sqs") 37 | ): AmazonSQSClient = { 38 | val client = AmazonSQSAsyncClientBuilder 39 | .standard() 40 | .withRegion(region) 41 | .withCredentials(awsCredentialsProvider) 42 | .withClientConfiguration(clientConfiguration) 43 | .withExecutorFactory(new ExecutorFactory { def newExecutor() = executorService }) 44 | .build() 45 | 46 | new AmazonSQSClient(client, executorService) 47 | } 48 | 49 | def build(builder: AmazonSQSAsyncClientBuilder)( 50 | executorService : ExecutorService = defaultExecutorService(builder.getClientConfiguration, "aws.wrap.sqs") 51 | ) = { 52 | val client = builder 53 | .withExecutorFactory(new ExecutorFactory { def newExecutor() = executorService }) 54 | .build() 55 | 56 | new AmazonSQSClient(client, executorService) 57 | } 58 | 59 | } 60 | 61 | 62 | class AmazonSQSClient( 63 | val client : com.amazonaws.services.sqs.AmazonSQSAsync, 64 | val executorService : ExecutorService 65 | ) extends AmazonSQSWrapper { 66 | 67 | import scala.collection.JavaConverters._ 68 | 69 | val defaultMessageOpsConcurrency = 16 70 | 71 | implicit val ec = ExecutionContext.fromExecutorService(executorService) 72 | 73 | /** 74 | * Send SQS messages as a stream 75 | * Important note: SQS does not ensure message ordering, so setting messageSendingConcurrency parameter equal to 1 does not 76 | * guarantee total message ordering. 77 | * @param messageSendingConcurrency 78 | */ 79 | def sendMessageAsStream(messageSendingConcurrency: Int = defaultMessageOpsConcurrency): Flow[SendMessageRequest, SendMessageResult, akka.NotUsed] = { 80 | Flow[SendMessageRequest].mapAsync(messageSendingConcurrency) { msg => 81 | sendMessage(msg) 82 | } 83 | } 84 | 85 | /** 86 | * Receive messages from a SQS queue as a stream. 87 | * @param queueUrl SQS queue url 88 | * @param longPollingMaxWait SQS long-polling parameter. 89 | * @param autoAck If true, the SQS messages will be automatically ack once they are received. If false, you must call 90 | * deleteMessage yourself when you want to ack the message. 91 | * @param messageAttributeNames the message attribute names asked to be returned 92 | */ 93 | def receiveMessageAsStream(queueUrl: String, messageAckingConcurrency: Int = defaultMessageOpsConcurrency, 94 | longPollingMaxWait: FiniteDuration = 20 seconds, autoAck: Boolean = false, 95 | messageAttributeNames: Seq[String] = Seq.empty): Source[Message, ActorRef] = { 96 | val source = SourceExt.bulkPullerAsync(0L) { (total, currentDemand) => 97 | val msg = new ReceiveMessageRequest(queueUrl) 98 | msg.setWaitTimeSeconds(longPollingMaxWait.toSeconds.toInt) // > 0 seconds allow long-polling. 20 seconds is the maximum 99 | msg.setMaxNumberOfMessages(Math.min(currentDemand, 10)) // 10 is SQS limit 100 | msg.setMessageAttributeNames(messageAttributeNames.asJava) 101 | receiveMessage(msg).map(res => (res.getMessages.asScala, false)) 102 | } 103 | 104 | if (autoAck) 105 | source.mapAsync(messageAckingConcurrency) { msg => 106 | deleteMessage(queueUrl, msg.getReceiptHandle).map(_ => msg) 107 | } 108 | else source 109 | } 110 | 111 | 112 | /** 113 | * Receive messages from a SQS queue as a stream and if connection closes, 114 | * retry using an exponential backoff reconnection strategy (2^retryNb * retryMinInterval). 115 | * 116 | * @param queueUrl SQS queue url 117 | * @param maxRetryDuration maximum retry duration. 118 | * @param retryMinInterval minimum delay before retrying. 119 | * @param longPollingMaxWait SQS long-polling parameter. 120 | * @param autoAck If true, the SQS messages will be automatically ack once they are received. If false, you must call 121 | * deleteMessage yourself when you want to ack the message. 122 | * @param messageAttributeNames the message attribute names asked to be returned 123 | */ 124 | def receiveMessageAsStreamWithRetryExpBackoff( 125 | queueUrl: String, messageAckingConcurrency: Int = defaultMessageOpsConcurrency, 126 | maxRetryDuration: FiniteDuration = 540.seconds, retryMinInterval: FiniteDuration = 1.second, 127 | longPollingMaxWait: FiniteDuration = 20 seconds, autoAck: Boolean = false, 128 | messageAttributeNames: Seq[String] = Seq.empty): Source[Message, ActorRef] = { 129 | val source = SourceExt.bulkPullerAsyncWithErrorExpBackoff(0L, maxRetryDuration, retryMinInterval) { (total, currentDemand) => 130 | val msg = new ReceiveMessageRequest(queueUrl) 131 | msg.setWaitTimeSeconds(longPollingMaxWait.toSeconds.toInt) // > 0 seconds allow long-polling. 20 seconds is the maximum 132 | msg.setMaxNumberOfMessages(Math.min(currentDemand, 10)) // 10 is SQS limit 133 | msg.setMessageAttributeNames(messageAttributeNames.asJava) 134 | receiveMessage(msg).map(res => (res.getMessages.asScala, false)) 135 | } 136 | 137 | if (autoAck) 138 | source.mapAsync(messageAckingConcurrency) { msg => 139 | deleteMessage(queueUrl, msg.getReceiptHandle).map(_ => msg) 140 | } 141 | else source 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /sqs/src/main/scala/AmazonSQSWrapper.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2015 Pellucid Analytics 3 | * Copyright 2015 Daniel W. H. James 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.mfglabs.commons.aws 19 | package sqs 20 | 21 | import scala.concurrent.{Future, ExecutionContext} 22 | import scala.collection.JavaConverters._ 23 | 24 | import com.amazonaws.services.sqs.AmazonSQSAsync 25 | import com.amazonaws.services.sqs.model._ 26 | 27 | trait AmazonSQSWrapper { 28 | import FutureHelper._ 29 | 30 | def client: AmazonSQSAsync 31 | implicit def ec: ExecutionContext 32 | 33 | def addPermission( 34 | addPermissionRequest: AddPermissionRequest 35 | ): Future[AddPermissionResult] = 36 | wrapAsyncMethod(client.addPermissionAsync, addPermissionRequest) 37 | 38 | def addPermission( 39 | queueUrl: String, 40 | label: String, 41 | accountActions: Map[String, String] 42 | ): Future[AddPermissionResult] = { 43 | val (accounts, actions) = accountActions.unzip 44 | addPermission( 45 | new AddPermissionRequest( 46 | queueUrl, 47 | label, 48 | accounts.toSeq.asJava, 49 | actions.toSeq.asJava 50 | ) 51 | ) 52 | } 53 | 54 | def changeMessageVisibility( 55 | changeMessageVisibilityRequest: ChangeMessageVisibilityRequest 56 | ): Future[ChangeMessageVisibilityResult] = 57 | wrapAsyncMethod(client.changeMessageVisibilityAsync, changeMessageVisibilityRequest) 58 | 59 | def changeMessageVisibility( 60 | queueUrl: String, 61 | receiptHandle: String, 62 | visibilityTimeout: Int 63 | ): Future[ChangeMessageVisibilityResult] = 64 | changeMessageVisibility(new ChangeMessageVisibilityRequest(queueUrl, receiptHandle, visibilityTimeout)) 65 | 66 | def changeMessageVisibilityBatch( 67 | changeMessageVisibilityBatchRequest: ChangeMessageVisibilityBatchRequest 68 | ): Future[ChangeMessageVisibilityBatchResult] = 69 | wrapAsyncMethod(client.changeMessageVisibilityBatchAsync, changeMessageVisibilityBatchRequest) 70 | 71 | def changeMessageVisibilityBatch( 72 | queueUrl: String, 73 | messageVisibilities: Seq[(String, String, Int)] 74 | ): Future[ChangeMessageVisibilityBatchResult] = 75 | changeMessageVisibilityBatch( 76 | new ChangeMessageVisibilityBatchRequest( 77 | queueUrl, 78 | messageVisibilities.map { case (id, receiptHandle, visibilityTimeout) => 79 | new ChangeMessageVisibilityBatchRequestEntry(id, receiptHandle) 80 | .withVisibilityTimeout(visibilityTimeout) 81 | } .asJava 82 | ) 83 | ) 84 | 85 | def createQueue( 86 | createQueueRequest: CreateQueueRequest 87 | ): Future[CreateQueueResult] = 88 | wrapAsyncMethod[CreateQueueRequest, CreateQueueResult](client.createQueueAsync, createQueueRequest) 89 | 90 | def createQueue( 91 | queueName: String, 92 | attributes: Map[QueueAttributeName, Any] = Map.empty 93 | ): Future[CreateQueueResult] = 94 | createQueue( 95 | new CreateQueueRequest(queueName) 96 | .withAttributes( 97 | attributes.map{ case (n, v) => 98 | (n.toString, v.toString) 99 | }.asJava 100 | ) 101 | ) 102 | 103 | def deleteMessage( 104 | deleteMessageRequest: DeleteMessageRequest 105 | ): Future[DeleteMessageResult] = 106 | wrapAsyncMethod(client.deleteMessageAsync, deleteMessageRequest) 107 | 108 | def deleteMessage( 109 | queueUrl: String, 110 | receiptHandle: String 111 | ): Future[DeleteMessageResult] = 112 | deleteMessage(new DeleteMessageRequest(queueUrl, receiptHandle)) 113 | 114 | def deleteMessageBatch( 115 | deleteMessageBatchRequest: DeleteMessageBatchRequest 116 | ): Future[DeleteMessageBatchResult] = 117 | wrapAsyncMethod(client.deleteMessageBatchAsync, deleteMessageBatchRequest) 118 | 119 | def deleteMessageBatch( 120 | queueUrl: String, 121 | entries: Seq[(String, String)] 122 | ): Future[DeleteMessageBatchResult] = 123 | deleteMessageBatch( 124 | new DeleteMessageBatchRequest( 125 | queueUrl, 126 | entries.map{ case (id, receiptHandle) => 127 | new DeleteMessageBatchRequestEntry(id, receiptHandle) 128 | }.asJava 129 | ) 130 | ) 131 | 132 | def deleteQueue( 133 | deleteQueueRequest: DeleteQueueRequest 134 | ): Future[DeleteQueueResult] = 135 | wrapAsyncMethod[DeleteQueueRequest, DeleteQueueResult](client.deleteQueueAsync, deleteQueueRequest) 136 | 137 | def deleteQueue( 138 | queueUrl: String 139 | ): Future[DeleteQueueResult] = 140 | deleteQueue(new DeleteQueueRequest(queueUrl)) 141 | 142 | def getQueueAttributes( 143 | getQueueAttributesRequest: GetQueueAttributesRequest 144 | ): Future[GetQueueAttributesResult] = 145 | wrapAsyncMethod(client.getQueueAttributesAsync, getQueueAttributesRequest) 146 | 147 | def getQueueAttributes( 148 | queueUrl: String, 149 | attributeNames: Seq[String] 150 | ): Future[Map[String, String]] = 151 | getQueueAttributes( 152 | new GetQueueAttributesRequest(queueUrl) 153 | .withAttributeNames(attributeNames: _*) 154 | ).map(_.getAttributes.asScala.toMap) 155 | 156 | def getQueueUrl( 157 | getQueueUrlRequest: GetQueueUrlRequest 158 | ): Future[GetQueueUrlResult] = 159 | wrapAsyncMethod[GetQueueUrlRequest, GetQueueUrlResult](client.getQueueUrlAsync, getQueueUrlRequest) 160 | 161 | def getQueueUrl( 162 | queueName: String 163 | ): Future[String] = 164 | getQueueUrl(new GetQueueUrlRequest(queueName)).map(_.getQueueUrl) 165 | 166 | def listQueues( 167 | listQueuesRequest: ListQueuesRequest 168 | ): Future[ListQueuesResult] = 169 | wrapAsyncMethod[ListQueuesRequest, ListQueuesResult](client.listQueuesAsync, listQueuesRequest) 170 | 171 | def listQueues( 172 | queueNamePrefix: String = null 173 | ): Future[Seq[String]] = 174 | listQueues(new ListQueuesRequest(queueNamePrefix)).map(_.getQueueUrls.asScala.toSeq) 175 | 176 | def receiveMessage( 177 | receiveMessageRequest: ReceiveMessageRequest 178 | ): Future[ReceiveMessageResult] = 179 | wrapAsyncMethod[ReceiveMessageRequest, ReceiveMessageResult](client.receiveMessageAsync, receiveMessageRequest) 180 | 181 | def receiveMessage( 182 | queueUrl: String 183 | ): Future[Message] = 184 | receiveMessage(new ReceiveMessageRequest(queueUrl)).map(_.getMessages.get(0)) 185 | 186 | def receiveMessage( 187 | queueUrl: String, 188 | maxNumberOfMessages: Int 189 | ): Future[Seq[Message]] = 190 | receiveMessage( 191 | new ReceiveMessageRequest(queueUrl) 192 | .withMaxNumberOfMessages(maxNumberOfMessages) 193 | ).map(_.getMessages.asScala.toSeq) 194 | 195 | def removePermission( 196 | removePermissionRequest: RemovePermissionRequest 197 | ): Future[RemovePermissionResult] = 198 | wrapAsyncMethod(client.removePermissionAsync, removePermissionRequest) 199 | 200 | def removePermission( 201 | queueUrl: String, 202 | label: String 203 | ): Future[RemovePermissionResult] = 204 | removePermission(new RemovePermissionRequest(queueUrl, label)) 205 | 206 | def sendMessage( 207 | sendMessageRequest: SendMessageRequest 208 | ): Future[SendMessageResult] = 209 | wrapAsyncMethod(client.sendMessageAsync, sendMessageRequest) 210 | 211 | def sendMessage( 212 | queueUrl: String, 213 | messageBody: String 214 | ): Future[SendMessageResult] = 215 | sendMessage(new SendMessageRequest(queueUrl, messageBody)) 216 | 217 | def sendMessageBatch( 218 | sendMessageBatchRequest: SendMessageBatchRequest 219 | ): Future[SendMessageBatchResult] = 220 | wrapAsyncMethod(client.sendMessageBatchAsync, sendMessageBatchRequest) 221 | 222 | def sendMessageBatch( 223 | queueUrl: String, 224 | entries: Seq[(String, String)] 225 | ): Future[SendMessageBatchResult] = 226 | sendMessageBatch( 227 | new SendMessageBatchRequest( 228 | queueUrl, 229 | entries.map{ case (id, messageBody) => 230 | new SendMessageBatchRequestEntry(id, messageBody) 231 | }.asJava 232 | ) 233 | ) 234 | 235 | def setQueueAttributes( 236 | setQueueAttributesRequest: SetQueueAttributesRequest 237 | ): Future[SetQueueAttributesResult] = 238 | wrapAsyncMethod(client.setQueueAttributesAsync, setQueueAttributesRequest) 239 | 240 | def setQueueAttributes( 241 | queueUrl: String, 242 | attributes: Map[String, String] 243 | ): Future[SetQueueAttributesResult] = 244 | setQueueAttributes(new SetQueueAttributesRequest(queueUrl, attributes.asJava)) 245 | 246 | def shutdown(): Unit = 247 | client.shutdown() 248 | 249 | } 250 | -------------------------------------------------------------------------------- /sqs/src/test/scala/SQSSpec.scala: -------------------------------------------------------------------------------- 1 | package com.mfglabs.commons.aws 2 | package sqs 3 | 4 | import akka.actor._ 5 | import akka.stream._ 6 | import akka.stream.scaladsl._ 7 | import com.amazonaws.services.sqs.model.{CreateQueueRequest, MessageAttributeValue, SendMessageRequest} 8 | import org.scalatest.concurrent.ScalaFutures 9 | import org.scalatest.time.{Millis, Seconds, Span} 10 | import org.scalatest.{FlatSpec, Matchers} 11 | 12 | import scala.concurrent.duration._ 13 | import scala.util.Random 14 | 15 | class SQSSpec extends FlatSpec with Matchers with ScalaFutures { 16 | import com.amazonaws.regions.Regions 17 | import scala.collection.JavaConverters._ 18 | 19 | implicit override val patienceConfig = 20 | PatienceConfig(timeout = Span(60, Seconds), interval = Span(5, Millis)) 21 | 22 | implicit val as = ActorSystem() 23 | implicit val fm = ActorMaterializer() 24 | 25 | val sqsClient = AmazonSQSClient.from(Regions.EU_WEST_1)() 26 | 27 | val testQueueName = "commons-aws-sqs-test-" + Random.nextInt() 28 | val testQueueName2 = "commons-aws-sqs-test-" + Random.nextInt() 29 | 30 | val messageAttributeKey = "attribute_key" 31 | 32 | "SQS client" should "delete all test queue if any" in { 33 | sqsClient.listQueues("commons-aws-sqs-test-").futureValue.headOption.foreach(queueUrl => sqsClient.deleteQueue(queueUrl).futureValue) 34 | } 35 | 36 | it should "send message and receive them as streams" in { 37 | val newQueueReq = new CreateQueueRequest() 38 | newQueueReq.setAttributes(Map("VisibilityTimeout" -> 10.toString).asJava) // 10 seconds 39 | newQueueReq.setQueueName(testQueueName) 40 | val queueUrl = sqsClient.createQueue(newQueueReq).futureValue.getQueueUrl 41 | 42 | val msgs = for (i <- 1 to 200) yield s"Message $i" 43 | val futSent = Source(msgs) 44 | .map { body => 45 | val req = new SendMessageRequest() 46 | req.addMessageAttributesEntry(messageAttributeKey, new MessageAttributeValue().withDataType("String").withStringValue(body)) 47 | req.setMessageBody(body) 48 | req.setQueueUrl(queueUrl) 49 | req 50 | } 51 | .via(sqsClient.sendMessageAsStream()) 52 | .take(200) 53 | .runWith(Sink.seq) 54 | val futReceived = sqsClient.receiveMessageAsStream(queueUrl, autoAck = true, messageAttributeNames = List(messageAttributeKey)).take(200).runWith(Sink.seq) 55 | 56 | val (_, received) = futSent.zip(futReceived).futureValue 57 | 58 | received.map(_.getBody).sorted shouldEqual msgs.sorted 59 | received.map(_.getMessageAttributes.get(messageAttributeKey).getStringValue).sorted shouldEqual msgs.sorted 60 | 61 | // testing auto-ack (queue must be empty) 62 | val res = sqsClient 63 | .receiveMessageAsStream(queueUrl, longPollingMaxWait = 1 second) 64 | .takeWithin(10 seconds) 65 | .runWith(Sink.seq) 66 | .futureValue 67 | res shouldBe empty 68 | 69 | sqsClient.deleteQueue(queueUrl).futureValue 70 | } 71 | 72 | 73 | it should "send message and receive them as streams with retry mechanism" in { 74 | val newQueueReq = new CreateQueueRequest() 75 | newQueueReq.setAttributes(Map("VisibilityTimeout" -> 10.toString).asJava) // 10 seconds 76 | newQueueReq.setQueueName(testQueueName2) 77 | val queueUrl = sqsClient.createQueue(newQueueReq).futureValue.getQueueUrl 78 | 79 | val msgs = for (i <- 1 to 200) yield s"Message $i" 80 | val futSent = Source(msgs) 81 | .map { body => 82 | val req = new SendMessageRequest() 83 | req.setMessageBody(body) 84 | req.setQueueUrl(queueUrl) 85 | req 86 | } 87 | .via(sqsClient.sendMessageAsStream()) 88 | .take(200) 89 | .runWith(Sink.seq) 90 | val futReceived = sqsClient.receiveMessageAsStreamWithRetryExpBackoff(queueUrl, autoAck = true).take(200).runWith(Sink.seq) 91 | 92 | val (_, received) = futSent.zip(futReceived).futureValue 93 | 94 | received.map(_.getBody).sorted shouldEqual msgs.sorted 95 | 96 | // testing auto-ack (queue must be empty) 97 | val res = sqsClient 98 | .receiveMessageAsStream(queueUrl, longPollingMaxWait = 1 second) 99 | .takeWithin(10 seconds) 100 | .runWith(Sink.seq) 101 | .futureValue 102 | res shouldBe empty 103 | 104 | sqsClient.deleteQueue(queueUrl).futureValue 105 | } 106 | 107 | } 108 | 109 | --------------------------------------------------------------------------------