播放2.x:使用Iteratees上传活动文件

时间:2012-08-11 19:13:14

标签: scala file-upload playframework-2.0 azure-storage iterate

我将从问题开始:如何使用Scala API的Iteratee将文件上传到云端存储(在我的情况下是Azure Blob存储,但我认为不是现在最重要的)

背景:

我需要将输入分块为大约1 MB的块,用于存储大型媒体文件(300 MB +)作为Azure的BlockBlobs。不幸的是,我的Scala知识仍然很差(我的项目是基于Java的,其中Scala的唯一用途是上传控制器)。

我尝试使用此代码:Why makes calling error or done in a BodyParser's Iteratee the request hang in Play Framework 2.0?(作为Input Iteratee) - 它运行良好,但我可以使用的每个Element的大小为8192字节,所以它太小了,不能将一百兆字节的文件发送到云端。

我必须说这对我来说是一种全新的方法,而且很可能是我误解了一些东西(不想告诉我,我误解了一切;>)

我将非常感谢任何提示或链接,这将有助于我完成该主题。如果有任何相似用途的样本,那么对我来说这是最好的选择。

4 个答案:

答案 0 :(得分:35)

基本上你需要的是重新输入更大的块,1024 * 1024字节。

首先让我们有一个Iteratee消耗长达1米的字节(确定最后一个块更小)

val consumeAMB = 
  Traversable.takeUpTo[Array[Byte]](1024*1024) &>> Iteratee.consume()

使用它,我们可以构建一个Enumeratee(适配器),它将使用一个名为groups的API重新组合块:

val rechunkAdapter:Enumeratee[Array[Byte],Array[Byte]] =
  Enumeratee.grouped(consumeAMB)

此处分组使用Iteratee来确定每个块中放入多少内容。它使用我们的consumeAMB。这意味着结果是Enumeratee,将输入重新排列为1MB的Array[Byte]

现在我们需要编写BodyParser,它将使用Iteratee.foldM方法发送每个字节块:

val writeToStore: Iteratee[Array[Byte],_] =
  Iteratee.foldM[Array[Byte],_](connectionHandle){ (c,bytes) => 
    // write bytes and return next handle, probable in a Future
  }

foldM传递状态并在其传递的函数(S,Input[Array[Byte]]) => Future[S]中使用它以返回新的状态Future。 foldM将不会再次调用该函数,直到Future完成并且有可用的输入块。

身体解析器将重新组合输入并将其推入商店:

BodyParser( rh => (rechunkAdapter &>> writeToStore).map(Right(_)))

返回一个右键表示你在正文解析结束时返回一个正文(这恰好是这里的处理程序)。

答案 1 :(得分:3)

如果您的目标是流式传输到S3,那么我已经实施并测试了一个帮助器:

def uploadStream(bucket: String, key: String, enum: Enumerator[Array[Byte]])
                (implicit ec: ExecutionContext): Future[CompleteMultipartUploadResult] = {
  import scala.collection.JavaConversions._

  val initRequest = new InitiateMultipartUploadRequest(bucket, key)
  val initResponse = s3.initiateMultipartUpload(initRequest)
  val uploadId = initResponse.getUploadId

  val rechunker: Enumeratee[Array[Byte], Array[Byte]] = Enumeratee.grouped {
    Traversable.takeUpTo[Array[Byte]](5 * 1024 * 1024) &>> Iteratee.consume()
  }

  val uploader = Iteratee.foldM[Array[Byte], Seq[PartETag]](Seq.empty) { case (etags, bytes) =>
    val uploadRequest = new UploadPartRequest()
      .withBucketName(bucket)
      .withKey(key)
      .withPartNumber(etags.length + 1)
      .withUploadId(uploadId)
      .withInputStream(new ByteArrayInputStream(bytes))
      .withPartSize(bytes.length)

    val etag = Future { s3.uploadPart(uploadRequest).getPartETag }
    etag.map(etags :+ _)
  }

  val futETags = enum &> rechunker |>>> uploader

  futETags.map { etags =>
    val compRequest = new CompleteMultipartUploadRequest(bucket, key, uploadId, etags.toBuffer[PartETag])
    s3.completeMultipartUpload(compRequest)
  }.recoverWith { case e: Exception =>
    s3.abortMultipartUpload(new AbortMultipartUploadRequest(bucket, key, uploadId))
    Future.failed(e)
  }

}

答案 2 :(得分:0)

对于那些也试图找出这个流媒体问题解决方案的人来说,你也可以使用parse.multipartFormData中已经实现的内容,而不是编写一个全新的BodyParser。 您可以实现类似下面的内容来覆盖默认处理程序 handleFilePartAsTemporaryFile

def handleFilePartAsS3FileUpload: PartHandler[FilePart[String]] = {
  handleFilePart {
    case FileInfo(partName, filename, contentType) =>

      (rechunkAdapter &>> writeToS3).map {
        _ =>
          val compRequest = new CompleteMultipartUploadRequest(...)
          amazonS3Client.completeMultipartUpload(compRequest)
          ...
      }
  }
}

def multipartFormDataS3: BodyParser[MultipartFormData[String]] = multipartFormData(handleFilePartAsS3FileUpload)

我能够完成这项工作,但我仍然不确定整个上传过程是否已流式传输。我尝试了一些大文件,似乎S3上传只在整个文件从客户端发送时才开始。

我查看了上面的解析器实现,我认为所有内容都是使用Iteratee连接的,因此应该对文件进行流式处理。 如果有人对此有所了解,那将非常有帮助。

答案 3 :(得分:0)

将以下内容添加到配置文件

play.http.parser.maxMemoryBuffer = 256K