Merge pull request #17771 from spray/w/17664-add-multipart-to-entity-shortcuts

Simplify creating multipart/formdata requests with just akka-http-core
This commit is contained in:
drewhk 2015-06-23 12:41:38 +02:00
commit b0fbd90008
3 changed files with 73 additions and 25 deletions

View file

@ -5,6 +5,8 @@
package akka.http.impl.engine.rendering
import java.nio.charset.Charset
import akka.parboiled2.util.Base64
import scala.collection.immutable
import akka.event.LoggingAdapter
import akka.http.scaladsl.model._
@ -16,6 +18,8 @@ import akka.stream.stage._
import akka.util.ByteString
import HttpEntity._
import scala.concurrent.forkjoin.ThreadLocalRandom
/**
* INTERNAL API
*/
@ -110,4 +114,13 @@ private[http] object BodyPartRenderer {
case x r ~~ x ~~ CrLf
}
/**
* Creates a new random number of the given length and base64 encodes it (using a custom "safe" alphabet).
*/
def randomBoundary(length: Int = 18, random: java.util.Random = ThreadLocalRandom.current()): String = {
val array = new Array[Byte](length)
random.nextBytes(array)
Base64.custom.encodeToString(array, false)
}
}

View file

@ -4,15 +4,21 @@
package akka.http.scaladsl.model
import java.io.File
import akka.event.{ NoLogging, LoggingAdapter }
import scala.collection.immutable.VectorBuilder
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ Future, ExecutionContext }
import scala.collection.immutable
import scala.util.{ Failure, Success, Try }
import akka.stream.FlowMaterializer
import akka.stream.scaladsl.Source
import akka.stream.io.SynchronousFileSource
import akka.stream.scaladsl.{ FlattenStrategy, Source }
import akka.http.scaladsl.util.FastFuture
import akka.http.scaladsl.model.headers._
import akka.http.impl.engine.rendering.BodyPartRenderer
import FastFuture._
trait Multipart {
@ -25,12 +31,29 @@ trait Multipart {
* The Future is failed with an TimeoutException if one part isn't read completely after the given timeout.
*/
def toStrict(timeout: FiniteDuration)(implicit ec: ExecutionContext, fm: FlowMaterializer): Future[Multipart.Strict]
/**
* Creates an entity from this multipart object.
*/
def toEntity(charset: HttpCharset = HttpCharsets.`UTF-8`,
boundary: String = BodyPartRenderer.randomBoundary())(implicit log: LoggingAdapter = NoLogging): MessageEntity = {
val chunks =
parts
.transform(() BodyPartRenderer.streamed(boundary, charset.nioCharset, partHeadersSizeHint = 128, log))
.flatten(FlattenStrategy.concat)
HttpEntity.Chunked(mediaType withBoundary boundary, chunks)
}
}
object Multipart {
trait Strict extends Multipart {
def strictParts: immutable.Seq[BodyPart.Strict]
override def toEntity(charset: HttpCharset, boundary: String)(implicit log: LoggingAdapter = NoLogging): HttpEntity.Strict = {
val data = BodyPartRenderer.strict(strictParts, boundary, charset.nioCharset, partHeadersSizeHint = 128, log)
HttpEntity(mediaType withBoundary boundary, data)
}
}
trait BodyPart {
@ -154,6 +177,7 @@ object Multipart {
}
object FormData {
def apply(parts: BodyPart.Strict*): Strict = Strict(parts.toVector)
def apply(parts: BodyPart*): FormData = FormData(Source(parts.toVector))
def apply(fields: Map[String, HttpEntity.Strict]): Strict = Strict {
fields.map { case (name, entity) BodyPart.Strict(name, entity) }(collection.breakOut)
@ -164,6 +188,15 @@ object Multipart {
override def toString = s"FormData($parts)"
}
/**
* Creates a FormData instance that contains a single part backed by the given file.
*
* To create an instance with several parts or for multiple files, use
* ``FormData(BodyPart.fromFile("field1", ...), BodyPart.fromFile("field2", ...)``
*/
def fromFile(name: String, contentType: ContentType, file: File, chunkSize: Int = SynchronousFileSource.DefaultChunkSize): FormData =
FormData(Source.single(BodyPart.fromFile(name, contentType, file, chunkSize)))
/**
* Strict [[FormData]].
*/
@ -201,6 +234,12 @@ object Multipart {
override def toString = s"FormData.BodyPart($name, $entity, $additionalDispositionParams, $additionalHeaders)"
}
/**
* Creates a BodyPart backed by a File that will be streamed using a SynchronousFileSource.
*/
def fromFile(name: String, contentType: ContentType, file: File, chunkSize: Int = SynchronousFileSource.DefaultChunkSize): BodyPart =
BodyPart(name, HttpEntity(contentType, file, chunkSize), Map("filename" -> file.getName))
def unapply(value: BodyPart): Option[(String, BodyPartEntity, Map[String, String], immutable.Seq[HttpHeader])] =
Some((value.name, value.entity, value.additionalDispositionParams, value.additionalHeaders))

View file

@ -5,41 +5,37 @@
package akka.http.scaladsl.marshalling
import scala.concurrent.forkjoin.ThreadLocalRandom
import akka.parboiled2.util.Base64
import akka.event.{ NoLogging, LoggingAdapter }
import akka.stream.scaladsl.FlattenStrategy
import akka.http.impl.engine.rendering.BodyPartRenderer
import akka.http.scaladsl.model._
trait MultipartMarshallers {
protected val multipartBoundaryRandom: java.util.Random = ThreadLocalRandom.current()
/**
* Creates a new random 144-bit number and base64 encodes it (using a custom "safe" alphabet, yielding 24 characters).
*/
def randomBoundary: String = {
val array = new Array[Byte](18)
multipartBoundaryRandom.nextBytes(array)
Base64.custom.encodeToString(array, false)
}
implicit def multipartMarshaller[T <: Multipart](implicit log: LoggingAdapter = NoLogging): ToEntityMarshaller[T] =
Marshaller strict { value
val boundary = randomBoundary
val boundary = randomBoundary()
val contentType = ContentType(value.mediaType withBoundary boundary)
Marshalling.WithOpenCharset(contentType.mediaType, { charset
value match {
case x: Multipart.Strict
val data = BodyPartRenderer.strict(x.strictParts, boundary, charset.nioCharset, partHeadersSizeHint = 128, log)
HttpEntity(contentType, data)
case _
val chunks = value.parts
.transform(() BodyPartRenderer.streamed(boundary, charset.nioCharset, partHeadersSizeHint = 128, log))
.flatten(FlattenStrategy.concat)
HttpEntity.Chunked(contentType, chunks)
}
value.toEntity(charset, boundary)(log)
})
}
/**
* The random instance that is used to create multipart boundaries. This can be overriden (e.g. in tests) to
* choose how a boundary is created.
*/
protected def multipartBoundaryRandom: java.util.Random = ThreadLocalRandom.current()
/**
* The length of randomly generated multipart boundaries (before base64 encoding). Can be overridden
* to configure.
*/
protected def multipartBoundaryLength: Int = 18
/**
* The method used to create boundaries in `multipartMarshaller`. Can be overridden to create custom boundaries.
*/
protected def randomBoundary(): String =
BodyPartRenderer.randomBoundary(length = multipartBoundaryLength, random = multipartBoundaryRandom)
}
object MultipartMarshallers extends MultipartMarshallers