diff --git a/build.mill b/build.mill index 95013993ad..1d2a8798d8 100644 --- a/build.mill +++ b/build.mill @@ -125,6 +125,7 @@ def zippedExamples = T { build.example.websockets2.millSourcePath, build.example.websockets3.millSourcePath, build.example.websockets4.millSourcePath, + build.example.multipartFormSubmission.millSourcePath, ) for (example <- examples) yield { diff --git a/cask/src/cask/model/Params.scala b/cask/src/cask/model/Params.scala index 09871e57b4..6f3fba610c 100644 --- a/cask/src/cask/model/Params.scala +++ b/cask/src/cask/model/Params.scala @@ -1,10 +1,12 @@ package cask.model import java.io.{ByteArrayOutputStream, InputStream} - import cask.internal.Util import io.undertow.server.HttpServerExchange import io.undertow.server.handlers.CookieImpl +import io.undertow.util.HttpString +import scala.util.Try +import scala.collection.JavaConverters.collectionAsScalaIterableConverter case class QueryParams(value: Map[String, collection.Seq[String]]) case class RemainingPathSegments(value: Seq[String]) @@ -98,9 +100,13 @@ sealed trait FormEntry{ } } object FormEntry{ - def fromUndertow(from: io.undertow.server.handlers.form.FormData.FormValue) = { - if (!from.isFile) FormValue(from.getValue, from.getHeaders) - else FormFile(from.getFileName, from.getPath, from.getHeaders) + def fromUndertow(from: io.undertow.server.handlers.form.FormData.FormValue): FormEntry = { + val isOctetStream = Option(from.getHeaders) + .flatMap(headers => Option(headers.get(HttpString.tryFromString("Content-Type")))) + .exists(h => h.asScala.exists(v => v == "application/octet-stream")) + // browsers will set empty file fields to content type: octet-stream + if (isOctetStream || from.isFileItem) FormFile(from.getFileName, Try(from.getFileItem.getFile).toOption, from.getHeaders) + else FormValue(from.getValue, from.getHeaders) } } @@ -110,7 +116,9 @@ case class FormValue(value: String, } case class FormFile(fileName: String, - filePath: java.nio.file.Path, + filePath: Option[java.nio.file.Path], headers: io.undertow.util.HeaderMap) extends FormEntry{ def valueOrFileName = fileName } + +case class EmptyFormEntry() diff --git a/example/multipartFormSubmission/app/resources/example.txt b/example/multipartFormSubmission/app/resources/example.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/example/multipartFormSubmission/app/src/MultipartFormSubmission.scala b/example/multipartFormSubmission/app/src/MultipartFormSubmission.scala new file mode 100644 index 0000000000..f67ded041c --- /dev/null +++ b/example/multipartFormSubmission/app/src/MultipartFormSubmission.scala @@ -0,0 +1,26 @@ +package app + +object MultipartFormSubmission extends cask.MainRoutes { + + @cask.get("/") + def index() = + cask.model.Response( + """ + + + + +
+ + +
+ + + """, 200, Seq(("Content-Type", "text/html"))) + + @cask.postForm("/post") + def post(somefile: cask.FormFile) = + s"filename: ${somefile.fileName}" + + initialize() +} diff --git a/example/multipartFormSubmission/app/test/src/ExampleTests.scala b/example/multipartFormSubmission/app/test/src/ExampleTests.scala new file mode 100644 index 0000000000..9c065ead09 --- /dev/null +++ b/example/multipartFormSubmission/app/test/src/ExampleTests.scala @@ -0,0 +1,34 @@ +package app +import io.undertow.Undertow + +import utest._ + +object ExampleTests extends TestSuite{ + def withServer[T](example: cask.main.Main)(f: String => T): T = { + val server = Undertow.builder + .addHttpListener(8081, "localhost") + .setHandler(example.defaultHandler) + .build + server.start() + val res = + try f("http://localhost:8081") + finally server.stop() + res + } + + val tests = Tests { + test("MultipartFormSubmission") - withServer(MultipartFormSubmission) { host => + val withFile = requests.post(s"$host/post", data = requests.MultiPart( + requests.MultiItem("somefile", Array[Byte](1,2,3,4,5) , "example.txt"), + )) + withFile.text() ==> s"filename: example.txt" + withFile.statusCode ==> 200 + + val withoutFile = requests.post(s"$host/post", data = requests.MultiPart( + requests.MultiItem("somefile", Array[Byte]()), + )) + withoutFile.text() ==> s"filename: null" + withoutFile.statusCode ==> 200 + } + } +} diff --git a/example/multipartFormSubmission/package.mill b/example/multipartFormSubmission/package.mill new file mode 100644 index 0000000000..1908ce5236 --- /dev/null +++ b/example/multipartFormSubmission/package.mill @@ -0,0 +1,18 @@ +package build.example.multipartFormSubmission +import mill._, scalalib._ + +object app extends Cross[AppModule](build.scalaVersions) +trait AppModule extends CrossScalaModule{ + + def moduleDeps = Seq(build.cask(crossScalaVersion)) + + def ivyDeps = Agg[Dep]( + ) + object test extends ScalaTests with TestModule.Utest{ + + def ivyDeps = Agg( + ivy"com.lihaoyi::utest::0.8.4", + ivy"com.lihaoyi::requests::0.9.0", + ) + } +}