自定义版本的fileUpload指令无法实现

时间:2018-10-11 17:38:04

标签: scala akka akka-stream akka-http

当用户将文件上传到我的Web服务时,我想从POST请求中收集非二进制字段。它们包含上传文件的元数据。所以我修改了akka-http的fileUpload指令

def fileUpload3(fieldName: String): Directive1[(Map[String, String], FileInfo, Source[ByteString, Any])] =
  entity(as[Multipart.FormData]).flatMap { formData ⇒
    extractRequestContext.flatMap { ctx ⇒
      implicit val mat: Materializer = ctx.materializer

      val fut =
        formData.parts.fold((Map.empty[String, String], Option.empty[(FileInfo, Source[ByteString, Any])])) { case ((fields, pairOpt), part) ⇒
          if (part.filename.nonEmpty && part.name == fieldName) {
            fields → Some((FileInfo(part.name, part.filename.get, part.entity.contentType), part.entity.dataBytes))
          } else if (part.filename.isEmpty && part.entity.contentType.mediaType == MediaTypes.`text/plain` && part.entity.isInstanceOf[HttpEntity.Strict]) {
            fields.updated(part.name, part.entity.asInstanceOf[HttpEntity.Strict].data.utf8String) → pairOpt
          } else {
            fields → pairOpt
          }
        }
          .collect {
            case (fields, Some((info, stream))) ⇒
              (fields, info, stream)
          }
          .runWith(Sink.headOption[(Map[String, String], FileInfo, Source[ByteString, Any])])

      onSuccess(fut)
    }
  }.flatMap {
    case Some(tuple) ⇒ provide(tuple)
    case None ⇒ reject(MissingFormFieldRejection(fieldName))
  }

尽管我与original code的区别不大,但在我使用它时却失败了,但有以下例外:

akka.stream.AbruptIOTerminationException: Stream terminated without completing IO operation.
Caused by: akka.stream.impl.SubscriptionTimeoutException: Substream Source has not been materialized in 5000 milliseconds
    at akka.stream.impl.fusing.SubSource.timeout(StreamOfStreams.scala:746)

伙计,我想念什么?

1 个答案:

答案 0 :(得分:0)

起初我没有意识到,但是因为我们要从单个连续流中获取所有字段,所以即使akka流允许我们这样做,也无法通过Source[T]提取其中一个字段供以后进行流式使用所以。

因此,必须先清空多部分请求的每一部分,然后再处理下一个请求。

还请注意,以下函数将仅收集二进制文件之前的文本字段。

def fileUploadWithFields(fieldName: String): Directive1[(Map[String, String], FileInfo, Source[ByteString, Any])] =
  entity(as[Multipart.FormData]).flatMap { formData ⇒
    extractRequestContext.flatMap { ctx ⇒
      implicit val mat: Materializer = ctx.materializer

      // Because it's continuous stream of fields we MUST consume each field before switching to next one. [https://stackoverflow.com/q/52765993/226895]
      val fut = formData.parts
        .takeWhile(part ⇒ !(part.filename.isDefined && part.name == fieldName), inclusive = true)
        .fold((Map.empty[String, String], Option.empty[(FileInfo, Source[ByteString, Any])])) { case ((fields, pairOpt), part) ⇒
          if (part.filename.nonEmpty && part.name == fieldName) {
            //println(s"Got file field: $part")
            fields → Some((FileInfo(part.name, part.filename.get, part.entity.contentType), part.entity.dataBytes))
          } else if (part.filename.isEmpty && part.entity.contentType.mediaType.isText && part.entity.isInstanceOf[HttpEntity.Strict]) {
            //println(s"Got text field: $part")
            val text = part.entity.asInstanceOf[HttpEntity.Strict].data.utf8String
            fields.updated(part.name, text) → pairOpt
          } else {
            //println(s"IGNORING field: $part")
            part.entity.discardBytes()
            fields → pairOpt
          }
        }
        .collect {
          case (fields, Some((info, stream))) ⇒
            //println(s"Completed scanning fields: ${(fields, info, stream)}")
            (fields, info, stream)
        }
        .runWith(Sink.headOption[(Map[String, String], FileInfo, Source[ByteString, Any])])

      onSuccess(fut)
    }
  }.flatMap {
    case Some(tuple) ⇒ provide(tuple)
    case None ⇒ reject(MissingFormFieldRejection(fieldName))
  }