I ended up using the code below. Wasn't too hard, but there really should have been a Spray sample available somewhere about this.
multipart/form-data
forms must always be used (instead of the traditional application/x-www-form-urlencoded
) if binary uploads are involved. More details here.
My requirements were:
- need to upload binary files of reasonably large size
- wanted to have metadata as fields (not embedded in URL or the upload's filename)
Some questions:
- is the way I'm managing errors the "best" way?
It's in the essence of REST API design to treat the client as a "human" (in debugging, we are), giving meaningful error messages in case something is wrong with the message.
post {
// Note: We cannot use a regular 'return' to provide a routing mid-way. The last item matters, but we can
// have a 'var' which collects the correct success / error info into it. It's a write-once variable.
//
var ret: Option[Route] = None
// Multipart form
//
// To exercise this:
// $ curl -i -F "file=@filename.bin" -F "computer=MYPC" http://localhost:8080/your/route; echo
//
entity(as[MultipartFormData]) { formData =>
val file = formData.get("file")
// e.g. Some(
// BodyPart( HttpEntity( application/octet-stream, ...binary data...,
// List(Content-Type: application/octet-stream, Content-Disposition: form-data; name=file; filename=<string>)))
log.debug( s".file: $file")
val computer = formData.get("computer")
// e.g. Some( BodyPart( HttpEntity(text/plain; charset=UTF-8,MYPC), List(Content-Disposition: form-data; name=computer)))
log.debug( s"computer: $computer" )
// Note: The types are mentioned below simply to make code comprehension easier. Scala could deduce them.
//
for( file_bodypart: BodyPart <- file;
computer_bodypart: BodyPart <- computer ) {
// BodyPart: http://spray.io/documentation/1.1-SNAPSHOT/api/index.html#spray.http.BodyPart
val file_entity: HttpEntity = file_bodypart.entity
//
// HttpEntity: http://spray.io/documentation/1.1-SNAPSHOT/api/index.html#spray.http.HttpEntity
//
// HttpData: http://spray.io/documentation/1.1-SNAPSHOT/api/index.html#spray.http.HttpData
log.debug( s"File entity length: ${file_entity.data.length}" )
val file_bin= file_entity.data.toByteArray
log.debug( s"File bin length: ${file_bin.length}" )
val computer_name = computer_bodypart.entity.asString //note: could give encoding as parameter
log.debug( s"Computer name: $computer_name" )
// We have the data here, pass it on to an actor and return OK
//
...left out, application specific...
ret = Some(complete("Got the file, thanks.")) // the string doesn't actually matter, we're just being polite
}
ret.getOrElse(
complete( BadRequest, "Missing fields, expecting file=<binary>, computer=<string>" )
)
}
}