app/services/S3.scala (174 lines of code) (raw):

package services import _root_.metrics.S3Metrics.S3ClientExceptionsMetric import com.amazonaws.auth.AWSCredentialsProvider import com.amazonaws.services.s3.model.CannedAccessControlList.{ Private, PublicRead } import com.amazonaws.services.s3.model._ import com.amazonaws.services.s3.{AmazonS3, AmazonS3ClientBuilder} import com.amazonaws.util.StringInputStream import com.gu.pandomainauth.model.User import conf.ApplicationConfiguration import org.joda.time.DateTime import logging.Logging import scala.io.{Codec, Source} sealed trait S3Accounts { def bucket: String def client: Option[AmazonS3] } case class CmsFrontsS3Account( config: ApplicationConfiguration, awsEndpoints: AwsEndpoints ) extends S3Accounts { lazy val bucket = config.aws.frontsBucket lazy val client: Option[AmazonS3] = config.aws.credentials.map(credentials => S3.client(credentials, config.aws.region) ) } object S3 { def client(credentials: AWSCredentialsProvider, region: String): AmazonS3 = { AmazonS3ClientBuilder .standard() .withCredentials(credentials) .withRegion(region) .build() } } trait S3 extends Logging { def cmsFrontsS3Account: CmsFrontsS3Account private def withS3Result[T]( account: S3Accounts, key: String )(action: S3Object => T): Option[T] = account.client.flatMap { client => try { val request = new GetObjectRequest(account.bucket, key) val result = client.getObject(request) // http://stackoverflow.com/questions/17782937/connectionpooltimeoutexception-when-iterating-objects-in-s3 try { Some(action(result)) } catch { case e: Exception => S3ClientExceptionsMetric.increment() throw e } finally { result.close() } } catch { case e: AmazonS3Exception if e.getStatusCode == 404 => { logger.warn( "S3: attempted to get, but not found at %s - %s" format (account.bucket, key) ) None } case e: Exception => { logger.error( "S3: attempted to get, but got an error at %s - %s" format (account.bucket, key), e ) S3ClientExceptionsMetric.increment() throw e } } } def get(key: String)(implicit codec: Codec): Option[String] = withS3Result(cmsFrontsS3Account, key) { result => Source.fromInputStream(result.getObjectContent).mkString } def getLastModified(key: String): Option[DateTime] = withS3Result(cmsFrontsS3Account, key) { result => new DateTime(result.getObjectMetadata.getLastModified) } def putPublic( key: String, value: String, contentType: String, accounts: List[S3Accounts] ): Unit = { put(key: String, value: String, contentType: String, PublicRead, accounts) } def putPrivate( key: String, value: String, contentType: String, accounts: List[S3Accounts] ): Unit = { put(key: String, value: String, contentType: String, Private, accounts) } private def put( key: String, value: String, contentType: String, accessControlList: CannedAccessControlList, accounts: List[S3Accounts] ): Unit = { val metadata = new ObjectMetadata() metadata.setCacheControl("no-cache,no-store") metadata.setContentType(contentType) metadata.setContentLength(value.getBytes("UTF-8").length) accounts.map(putRequest(_, key, value, metadata, accessControlList)) } private def putRequest( account: S3Accounts, key: String, value: String, metadata: ObjectMetadata, accessControlList: CannedAccessControlList ): Option[PutObjectResult] = { val request = new PutObjectRequest( account.bucket, key, new StringInputStream(value), metadata ).withCannedAcl(accessControlList) try { account.client.map(_.putObject(request)) } catch { case e: Exception => logger.error( "S3: attempted to put, but got an error at %s - %s" format (account.bucket, key), e ) S3ClientExceptionsMetric.increment() throw e } } } class S3FrontsApi( val config: ApplicationConfiguration, val isTest: Boolean, val awsEndpoints: AwsEndpoints ) extends S3 { lazy val stage = if (isTest) "TEST" else config.facia.stage.toUpperCase val namespace = "frontsapi" lazy val location = s"$stage/$namespace" val cmsFrontsS3Account = new CmsFrontsS3Account(config, awsEndpoints) def getLiveFapiPressedKeyForPath(path: String): String = s"$location/pressed/live/$path/fapi/pressed.json" def getMasterConfig: Option[String] = get(s"$location/config/config.json") def putCollectionJson(id: String, json: String) = { val putLocation: String = s"$location/collection/$id/collection.json" putPrivate(putLocation, json, "application/json", List(cmsFrontsS3Account)) } def archive(id: String, json: String, identity: User) = { val now = DateTime.now val putLocation = s"$location/history/collection/${now.year.get}/${"%02d".format( now.monthOfYear.get )}/${"%02d".format(now.dayOfMonth.get)}/$id/${now}.${identity.email}.json" putPrivate(putLocation, json, "application/json", List(cmsFrontsS3Account)) } def putMasterConfig(json: String) = { val putLocation = s"$location/config/config.json" putPrivate(putLocation, json, "application/json", List(cmsFrontsS3Account)) } def archiveMasterConfig(json: String, identity: User) = { val now = DateTime.now val putLocation = s"$location/history/config/${now.year.get}/${"%02d".format(now.monthOfYear.get)}/${"%02d" .format(now.dayOfMonth.get)}/${now}.${identity.email}.json" putPrivate(putLocation, json, "application/json", List(cmsFrontsS3Account)) } def getPressedLastModified(path: String): Option[String] = getLastModified(getLiveFapiPressedKeyForPath(path)).map(_.toString) }