Play Framework 2.6 Alpakka S3 File Upload

959 views Asked by At

I use Play Framework 2.6 (Scala) and Alpakka AWS S3 Connector to upload files asynchronously to S3 bucket. My code looks like this:

def richUpload(extension: String, checkFunction: (String, Option[String]) => Boolean, cannedAcl: CannedAcl, bucket: String) = userAction(parse.multipartFormData(handleFilePartAsFile)).async { implicit request =>
  val s3Filename = request.user.get.id + "/" + java.util.UUID.randomUUID.toString + "." + extension
  val fileOption = request.body.file("file").map {
    case FilePart(key, filename, contentType, file) =>
      Logger.info(s"key = ${key}, filename = ${filename}, contentType = ${contentType}, file = $file")
      if(checkFunction(filename, contentType)) {
        s3Service.uploadSink(s3Filename, cannedAcl, bucket).runWith(FileIO.fromPath(file.toPath))
      } else {
        throw new Exception("Upload failed")
      }
  }

  fileOption match {
    case Some(opt) => opt.map(o => Ok(s3Filename))
    case _ => Future.successful(BadRequest("ERROR"))
  }
}

It works, but it returns filename before it uploads to S3. But I want to return value after it uploads to S3. Is there any solution?

Also, is it possible to stream file upload directly to S3, to show progress correctly and to not use temporary disk file?

1

There are 1 answers

0
Stefano Bonetti On BEST ANSWER

You need to flip around your source and sink to obtain the materialized value you are interested in. You have:

  1. a source that reads from your local files, and materializes to a Future[IOResult] upon completion of reading the file.
  2. a sink that writes to S3 and materializes to Future[MultipartUploadResult] upon completion of writing to S3.

You are interested in the latter, but in your code you are using the former. This is because the runWith function always keeps the materialized value of stage passed as parameter.

The types in the sample snippet below should clarify this:

  val fileSource: Source[ByteString, Future[IOResult]]              = ???
  val s3Sink    : Sink  [ByteString, Future[MultipartUploadResult]] = ???

  val m1: Future[IOResult]              = s3Sink.runWith(fileSource)
  val m2: Future[MultipartUploadResult] = fileSource.runWith(s3Sink)

After you have obtained a Future[MultipartUploadResult] you can map on it the same way and access the location field to get a file's URI, e.g.:

  val location: URI = fileSource.runWith(s3Sink).map(_.location)