app/controllers/S3ObjectsController.scala (84 lines of code) (raw):

package controllers import com.gu.googleauth.AuthAction import io.circe.{Decoder, Encoder} import io.circe.syntax._ import play.api.libs.circe.Circe import play.api.mvc._ import services.S3Client.S3ObjectSettings import services.{S3Json, VersionedS3Data} import zio.blocking.Blocking import zio.{IO, ZEnv, ZIO} import S3ObjectsController.extractFilename import com.typesafe.scalalogging.LazyLogging import utils.Circe.noNulls import scala.concurrent.{ExecutionContext, Future} object S3ObjectsController { // For extracting the json file name (without the extension) from an S3 key private val jsonFilenamePattern = """^.*/([\w-].*)\.json$""".r def extractFilename(key: String): Option[String] = key match { case jsonFilenamePattern(filename) => Some(filename) case _ => None } } /** * Controller for managing JSON data in objects under a specific S3 path: * `$stage/$path/$name.json` */ abstract class S3ObjectsController[T : Decoder : Encoder]( authAction: AuthAction[AnyContent], components: ControllerComponents, stage: String, path: String, nameGenerator: T => String, runtime: zio.Runtime[ZEnv] )(implicit ec: ExecutionContext) extends AbstractController(components) with Circe with LazyLogging { val s3Client = services.S3 private def run(f: => ZIO[ZEnv, Throwable, Result]): Future[Result] = runtime.unsafeRunToFuture { f.catchAll { error => IO.succeed(InternalServerError(error.getMessage)) } } private def buildObjectSettings(name: String) = S3ObjectSettings( bucket = "support-admin-console", key = s"$stage/$path/$name", publicRead = false ) /** * Saves a single object of type T to a new file. * Overwrites any existing object of the same name. */ def set = authAction.async(circe.json[T]) { request => val data = request.body val objectSettings = buildObjectSettings(nameGenerator(data)) run { S3Json .createOrUpdateAsJson(data)(s3Client) .apply(objectSettings) .map(_ => Ok("saved")) .mapError { error => logger.error(s"Failed to save to ${objectSettings.key}: $data. Error was: $error") error } } } /** * Fetches all object keys from S3 and returns just the names */ def list = authAction.async { request => val objectSettings = buildObjectSettings("") //empty string means list all files run { s3Client .listKeys(objectSettings) .map { keys => val names: List[String] = keys.flatMap(extractFilename) Ok(noNulls(names.asJson)) } .mapError { error => logger.error(s"Failed to fetch list of object names: $error") error } } } /** * Returns the object data for the given name */ def get(name: String) = authAction.async { request => val objectSettings = buildObjectSettings(name) run { S3Json .getFromJson[T](s3Client) .apply(objectSettings) .map { case VersionedS3Data(data, _) => Ok(data.asJson) } .mapError { error => logger.error(s"Failed to get object ${objectSettings.key}: $error") error } } } }