From cd3a9ccf855069a6504faa64b6def44d92d60fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andre=CC=81n?= Date: Mon, 9 Nov 2015 12:14:28 +0100 Subject: [PATCH] =doc 18471 Document file uploads --- .../server/FileUploadExamplesSpec.scala | 96 +++++++++++++++++++ .../rst/scala/http/routing-dsl/index.rst | 22 +++++ 2 files changed, 118 insertions(+) create mode 100644 akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/FileUploadExamplesSpec.scala diff --git a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/FileUploadExamplesSpec.scala b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/FileUploadExamplesSpec.scala new file mode 100644 index 0000000000..b32b16f4f4 --- /dev/null +++ b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/FileUploadExamplesSpec.scala @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2009-2015 Typesafe Inc. + */ +package docs.http.scaladsl.server + +import java.io.File + +import akka.actor.ActorRef +import akka.http.scaladsl.model.Multipart.FormData.BodyPart +import akka.stream.io.{ Framing, SynchronousFileSink } +import akka.stream.scaladsl._ +import akka.http.scaladsl.model.Multipart +import akka.util.ByteString + +import scala.concurrent.duration._ +import scala.concurrent.Future + +class FileUploadExamplesSpec extends RoutingSpec { + + case class Video(file: File, title: String, author: String) + object db { + def create(video: Video): Future[Unit] = Future.successful(Unit) + } + + "simple-upload" in { + val uploadVideo = + path("video") { + entity(as[Multipart.FormData]) { formData => + + // collect all parts of the multipart as it arrives into a map + val allPartsF: Future[Map[String, Any]] = formData.parts.mapAsync[(String, Any)](1) { + + case b: BodyPart if b.name == "file" => + // stream into a file as the chunks of it arrives and return a future + // file to where it got stored + val file = File.createTempFile("upload", "tmp") + b.entity.dataBytes.runWith(SynchronousFileSink(file)).map(_ => + (b.name -> file)) + + case b: BodyPart => + // collect form field values + b.toStrict(2.seconds).map(strict => + (b.name -> strict.entity.data.utf8String)) + + }.runFold(Map.empty[String, Any])((map, tuple) => map + tuple) + + val done = allPartsF.map { allParts => + // You would have some better validation/unmarshalling here + db.create(Video( + file = allParts("file").asInstanceOf[File], + title = allParts("title").asInstanceOf[String], + author = allParts("author").asInstanceOf[String])) + } + + // when processing have finished create a response for the user + onSuccess(allPartsF) { allParts => + complete { + "ok!" + } + } + } + } + } + + object MetadataActor { + case class Entry(id: Long, values: Seq[String]) + } + val metadataActor: ActorRef = system.deadLetters + + "stream-csv-upload" in { + val splitLines = Framing.delimiter(ByteString("\n"), 256) + + val csvUploads = + path("metadata" / LongNumber) { id => + entity(as[Multipart.FormData]) { formData => + val done = formData.parts.mapAsync(1) { + case b: BodyPart if b.filename.exists(_.endsWith(".csv")) => + b.entity.dataBytes + .via(splitLines) + .map(_.utf8String.split(",").toVector) + .runForeach(csv => + metadataActor ! MetadataActor.Entry(id, csv)) + case _ => Future.successful(Unit) + }.runWith(Sink.ignore) + + // when processing have finished create a response for the user + onSuccess(done) { + complete { + "ok!" + } + } + } + } + } + +} diff --git a/akka-docs-dev/rst/scala/http/routing-dsl/index.rst b/akka-docs-dev/rst/scala/http/routing-dsl/index.rst index 6e0c25dfa4..d962292cee 100644 --- a/akka-docs-dev/rst/scala/http/routing-dsl/index.rst +++ b/akka-docs-dev/rst/scala/http/routing-dsl/index.rst @@ -74,3 +74,25 @@ Failures and exceptions inside the Routing DSL Exception handling within the Routing DSL is done by providing :class:`ExceptionHandler` s which are documented in-depth in the :ref:`exception-handling-scala` section of the documtnation. You can use them to transform exceptions into :class:`HttpResponse` s with apropriate error codes and human-readable failure descriptions. + +File uploads +^^^^^^^^^^^^ + +Handling a simple file upload from for example a browser form with a `file` input can be done +by accepting a `Multipart.FormData` entity, note that the body parts are `Source` rather than +all available right away, and so is the individual body part payload so you will need to consume +those streams both for the file and for the form fields. + +Here is a simple example which just dumps the uploaded file into a temporary file on disk, collects +some form fields and saves an entry to a fictive database: + +.. includecode2:: ../../code/docs/http/scaladsl/server/FileUploadExamplesSpec.scala + :snippet: simple-upload + +You can transform the uploaded files as they arrive rather than storing then in a temporary file as +in the previous example. In this example we accept any number of ``.csv`` files, parse those into lines +and split each line before we send it to an actor for further processing: + +.. includecode2:: ../../code/docs/http/scaladsl/server/FileUploadExamplesSpec.scala + :snippet: stream-csv-upload +