在Play2/Scala中从MultipartFormData中内存中提取文件

10

我目前正在使用Play2/Scala,使用FileUploader Javascript实用程序将文件上传到我的服务器:

def fileUploader = Action(parse.multipartFormData) { request =>
  request.body.file("qqfile").map { picture =>
    import java.io.File
    val filename = picture.filename 
    val contentType = picture.contentType
    picture.ref.moveTo(new File("/tmp",filename))
    Ok(Json.toJson(Map( "success" -> "true" )))
  }.getOrElse {
    Ok(Json.toJson(Map( "error" -> "error occured")))
  }
}

我只处理小文件(<10MB),并且希望使用Mongo驱动程序将这些文件直接写入Mongo文档或GridFS。我意识到我可以从磁盘上读取保存的文件,但是否有一种方法可以在不首先缓冲磁盘上的文件的情况下从内存中处理所有内容?Play文档在此处建议编写自定义BodyParser (http://www.playframework.com/documentation/2.1.0/ScalaFileUpload),但似乎没有关于如何编写的任何文档。从Scaladocs中无法了解API /实现的工作方式。我尝试查找MultiPartFormData源代码以查看其工作原理,但似乎在其Git存储库中找不到它:

https://github.com/playframework/Play20/tree/master/framework/src/play/src/main/scala/play/api/mvc

我已经搜索了很多,但似乎找不到一个好的例子。


1
multipartFormData的body解析器可以在这里找到:https://github.com/playframework/Play20/blob/2.1.0/framework/src/play/src/main/scala/play/api/mvc/ContentTypes.scala#L541 - EECOLOR
3个回答

13

未经测试 BodyParsersMultipart对象为我们做了很多工作。我们需要做的第一件事是编写FilePart的处理程序。我在这里假设您希望以Array[Byte]形式获取文件部分。

def handleFilePartAsByteArray: PartHandler[FilePart[Array[Byte]]] =
  handleFilePart {
    case FileInfo(partName, filename, contentType) =>
      // simply write the data to the a ByteArrayOutputStream
      Iteratee.fold[Array[Byte], ByteArrayOutputStream](
        new ByteArrayOutputStream()) { (os, data) =>
          os.write(data)
          os
        }.mapDone { os =>
          os.close()
          os.toByteArray
        }
  }

下一步是定义您的body解析器:
def multipartFormDataAsBytes:BodyParser[MultipartFormData[Array[Byte]]] = 
  multipartFormData(handleFilePartAsByteArray)

然后,为了使用它,在你的 Action 中指定它:

def fileUploader = Action(multipartFormDataAsBytes) { request =>
  request.body.files foreach {
    case FilePart(key, filename, contentType, bytes) => // do something
  }
  Ok("done")
}

以上代码中的某些类型和方法可能有点难找。以下是完整的导入列表,以防您需要:

import play.api.mvc.BodyParsers.parse.Multipart.PartHandler
import play.api.mvc.BodyParsers.parse.Multipart.handleFilePart
import play.api.mvc.BodyParsers.parse.Multipart.FileInfo
import play.api.mvc.BodyParsers.parse.multipartFormData
import play.api.mvc.MultipartFormData.FilePart
import play.api.libs.iteratee.Iteratee
import java.io.ByteArrayOutputStream
import play.api.mvc.BodyParser
import play.api.mvc.MultipartFormData

5

自发布以来,Play API 已经做了相当大的改变。我也有一个类似的用例,不想使用临时文件,并将上述内容翻译成了以下代码,在 Play 2.6 中似乎可以正常工作,如果有人需要:

def byteStringFilePartHandler: FilePartHandler[ByteString] = {
    case FileInfo(partName, filename, contentType) =>
      Accumulator(Sink.fold[ByteString, ByteString](ByteString()) { (accumulator, data) =>
        accumulator ++ data
      }.mapMaterializedValue(fbs => fbs.map(bs => {
        FilePart(partName, filename, contentType, bs)
      })))
}

def multipartFormDataAsBytes: BodyParser[MultipartFormData[ByteString]] =
  playBodyParsers.multipartFormData(byteStringFilePartHandler)

如果你要在控制器中使用它,请确保注入 PlayBodyParsers 并提供一个 ExecutionContext,下面是引入等相关内容:

import akka.stream.scaladsl.Sink
import akka.util.ByteString
import javax.inject._
import play.api.libs.streams.Accumulator
import play.api.mvc.MultipartFormData.FilePart
import play.api.mvc._
import play.core.parsers.Multipart.{FileInfo, FilePartHandler}
import scala.concurrent.ExecutionContext


@Singleton
class HomeController @Inject()(cc: ControllerComponents, playBodyParsers: PlayBodyParsers)
                              (implicit ec: ExecutionContext) extends AbstractController(cc) {

  def index = Action(multipartFormDataAsBytes) { request =>
    request.body.file("image").foreach((image) => {
      val arr = image.ref.toByteBuffer.array()
      println(arr)
    })
    Ok("got bytes!")
  }
}

2

继Matt的回答之后(嗨,Matt!),我发现需要微调一下针对Play 2.8的内容(我猜测API进化了一些):

  def byteStringFilePartHandler: FilePartHandler[ByteString] = {
    case FileInfo(partName, filename, contentType, dispositionType) =>
      Accumulator(Sink.fold[ByteString, ByteString](ByteString()) { (accumulator, data) =>
        accumulator ++ data
      }.mapMaterializedValue(fbs => fbs.map(bs => {
        FilePart(partName, filename, contentType, bs)
      })))
  }

  def multipartFormDataAsBytes: BodyParser[MultipartFormData[ByteString]] =
    controllerComponents.parsers.multipartFormData(byteStringFilePartHandler)

由于我的用例是上传文本文件,因此我从结果中使用以下代码获取请求

    val body: String = request.body.files.head.ref.utf8String

(一份更加严谨的代码会在此处使用headOption,以确保安全性。)

请问您能分享导入的类吗? 我在使用Accumulator(Sink.fold[ByteString, ByteString](ByteString())方法时遇到了问题,提示未指定值参数:f: Function2[Any, Any, Any]。 - Saurabh Gangamwar
1
@Saurabh47g 抱歉——我已经不在那份工作上了,所以我无法再访问代码了... - Justin du Coeur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接