我将从问题开始:如何使用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字节,所以它太小了,不能将一百兆字节的文件发送到云端。
我必须说这对我来说是一种全新的方法,而且很可能是我误解了一些东西(不想告诉我,我误解了一切;>)
我将非常感谢任何提示或链接,这将有助于我完成该主题。如果有任何相似用途的样本,那么对我来说这是最好的选择。
答案 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