From 8ceaf03997011fa7c9c00f971af0094604cb281d Mon Sep 17 00:00:00 2001 From: Landlocked Surfer Date: Tue, 13 Jun 2023 08:12:20 +0200 Subject: [PATCH] Move Http.fromFileZIO to Handler. (#2239) move Http.fromFileZIO to Handler. --- .../src/main/scala/zio/http/Handler.scala | 42 ++++++++++++++++++- .../src/test/scala/zio/http/HandlerSpec.scala | 33 +++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/zio-http/src/main/scala/zio/http/Handler.scala b/zio-http/src/main/scala/zio/http/Handler.scala index 91e4d2348f..496fd1d4e8 100644 --- a/zio-http/src/main/scala/zio/http/Handler.scala +++ b/zio-http/src/main/scala/zio/http/Handler.scala @@ -22,8 +22,9 @@ import zio.http.Header.HeaderType import zio.http.internal.HeaderModifier import zio.stream.ZStream -import java.io.File +import java.io.{File, FileNotFoundException, IOException} import java.nio.charset.Charset +import java.nio.file.{AccessDeniedException, NotDirectoryException} import scala.reflect.ClassTag import scala.util.control.NonFatal // scalafix:ok; @@ -725,6 +726,45 @@ object Handler { ): Handler[R, Err, In, Out] = http.toHandler(default) + private def determineMediaType(filePath: String): Option[MediaType] = { + filePath.lastIndexOf(".") match { + case -1 => None + case i => + // Extract file extension + val ext = filePath.substring(i + 1) + MediaType.forFileExtension(ext) + } + } + + def fromFileZIO[R](getFile: ZIO[R, Throwable, File])(implicit trace: Trace): Handler[R, Throwable, Any, Response] = { + Handler.fromZIO[R, Throwable, Response]( + getFile.flatMap { file => + if (!file.exists()) { + ZIO.fail(new FileNotFoundException()) + } else if (file.isFile && !file.canRead) { + ZIO.fail(new AccessDeniedException(file.getAbsolutePath)) + } else { + if (file.isFile) { + val length = Headers(Header.ContentLength(file.length())) + val response = http.Response(headers = length, body = Body.fromFile(file)) + val pathName = file.toPath.toString + + // Set MIME type in the response headers. This is only relevant in + // case of RandomAccessFile transfers as browsers use the MIME type, + // not the file extension, to determine how to process a URL. + // {{{https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type}}} + determineMediaType(pathName) match { + case Some(mediaType) => ZIO.succeed(response.withHeader(Header.ContentType(mediaType))) + case None => ZIO.succeed(response) + } + } else { + ZIO.fail(new NotDirectoryException(s"Found directory instead of a file.")) + } + } + }, + ) + } + /** * Creates a Handler that always succeeds with a 200 status code and the * provided ZStream as the body diff --git a/zio-http/src/test/scala/zio/http/HandlerSpec.scala b/zio-http/src/test/scala/zio/http/HandlerSpec.scala index 039540f52c..d78d22645d 100644 --- a/zio-http/src/test/scala/zio/http/HandlerSpec.scala +++ b/zio-http/src/test/scala/zio/http/HandlerSpec.scala @@ -16,6 +16,9 @@ package zio.http +import java.io.FileNotFoundException +import java.nio.file.{Files, NotDirectoryException} + import zio._ import zio.test.Assertion._ import zio.test.TestAspect.timeout @@ -351,5 +354,35 @@ object HandlerSpec extends ZIOSpecDefault with ExitAssertion { assert(http.apply {})(isSuccess(equalTo(1))) }, ), + suite("fromFileZIO")( + test("must load response from file") { + ZIO.acquireRelease(ZIO.attempt(Files.createTempFile("", ".jpg"))) { tempPath => + ZIO.attempt(Files.deleteIfExists(tempPath)).ignore + } flatMap { tempPath => + val tempFile = tempPath.toFile + val http = Handler.fromFileZIO(ZIO.succeed(tempFile)) + for { + r <- http.apply {} + } yield { + assert(r.status)(equalTo(Status.Ok)) && + assert(r.headers)(contains(Header.ContentType(MediaType.image.`jpeg`))) && + assert(r.body)(equalTo(Body.fromFile(tempFile))) + } + } + }, + test("must fail if file does not exist") { + val http = Handler.fromFileZIO(ZIO.succeed(new java.io.File("does-not-exist"))) + assertZIO(http.apply {}.exit)(failsWithA[FileNotFoundException]) + }, + test("must fail if given file is a directory") { + ZIO.acquireRelease(ZIO.attempt(Files.createTempDirectory(""))) { tempPath => + ZIO.attempt(Files.deleteIfExists(tempPath)).ignore + } flatMap { tempPath => + val tempFile = tempPath.toFile + val http = Handler.fromFileZIO(ZIO.succeed(tempFile)) + assertZIO(http.apply {}.exit)(failsWithA[NotDirectoryException]) + } + }, + ), ) @@ timeout(10 seconds) }