Skip to content

Commit

Permalink
Feature: Add the ability to get resources from JAR archives (#1393)
Browse files Browse the repository at this point in the history
* Add the ability to get resources from JAR archives

* Update zio-http/src/main/scala/zhttp/http/Http.scala

* Use comma to combine unit tests

* Optimize method determineMediaType to eliminate extra flatMap

Co-authored-by: Tushar Mathur <[email protected]>
  • Loading branch information
nartamonov and tusharmath authored Aug 28, 2022
1 parent 8902ade commit 11f2d06
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 29 deletions.
61 changes: 53 additions & 8 deletions zio-http/src/main/scala/zhttp/http/Http.scala
Original file line number Diff line number Diff line change
Expand Up @@ -808,17 +808,11 @@ object Http {
val response = Response(headers = length, body = Body.fromFile(file))
val pathName = file.toPath.toString

// Extract file extension
val ext = pathName.lastIndexOf(".") match {
case -1 => None
case i => Some(pathName.substring(i + 1))
}

// 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.
// {{{<a href="MSDN Doc">https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type</a>}}}
Http.succeed(ext.flatMap(MediaType.forFileExtension).fold(response)(response.withMediaType))
Http.succeed(determineMediaType(pathName).fold(response)(response.withMediaType))
} else Http.empty
}
}
Expand Down Expand Up @@ -869,7 +863,58 @@ object Http {
* Creates an Http app from a resource path
*/
def fromResource(path: String): HttpApp[Any, Throwable] =
Http.getResource(path).flatMap(url => Http.fromFile(new File(url.getPath)))
Http.getResource(path).flatMap(url => Http.fromResourceWithURL(url))

private[zhttp] def fromResourceWithURL(url: java.net.URL): HttpApp[Any, Throwable] = {
url.getProtocol match {
case "file" =>
Http.fromFile(new File(url.getPath))
case "jar" =>
val path = new java.net.URI(url.getPath).getPath // remove "file:" prefix and normalize whitespace
val bangIndex = path.indexOf('!')
val filePath = path.substring(0, bangIndex)
val resourcePath = path.substring(bangIndex + 2)
val appZIO =
ZIO
.attemptBlockingIO(new java.util.zip.ZipFile(filePath))
.asSomeError
.flatMap { jar =>
val closeJar = ZIO.attemptBlocking(jar.close()).ignoreLogged
(for {
entry <- ZIO.attemptBlocking(Option(jar.getEntry(resourcePath))).some
_ <- ZIO.when(entry.isDirectory)(ZIO.fail(None))
contentLength = entry.getSize
inStream <- ZIO.attemptBlockingIO(jar.getInputStream(entry)).asSomeError
mediaType = determineMediaType(resourcePath)
app =
Http
.fromStream(
ZStream
.fromInputStream(inStream)
.ensuring(closeJar),
)
.withContentLength(contentLength)
} yield mediaType.fold(app)(t => app.withMediaType(t))).onError(_ => closeJar)
}

Http.fromZIO(appZIO).flatten.catchAll {
case Some(e) => Http.fail(e)
case None => Http.empty
}
case proto =>
Http.fail(new IllegalArgumentException(s"Unsupported protocol: $proto"))
}
}

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)
}
}

/**
* Creates a Http that always succeeds with a 200 status code and the provided
Expand Down
Binary file added zio-http/src/test/resources/TestArchive.jar
Binary file not shown.
73 changes: 52 additions & 21 deletions zio-http/src/test/scala/zhttp/service/StaticFileServerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,43 @@ object StaticFileServerSpec extends HttpRunnableSpec {
private val env =
EventLoopGroup.nio() ++ ChannelFactory.nio ++ ServerChannelFactory.nio ++ DynamicServer.live

private val fileOk = Http.fromResource("TestFile.txt").deploy
private val fileNotFound = Http.fromResource("Nothing").deploy

private val testArchivePath = getClass.getResource("/TestArchive.jar").getPath
private val resourceOk =
Http.fromResourceWithURL(new java.net.URL(s"jar:file:$testArchivePath!/TestFile.txt")).deploy
private val resourceNotFound =
Http.fromResourceWithURL(new java.net.URL(s"jar:file:$testArchivePath!/NonExistent.txt")).deploy

override def spec = suite("StaticFileServer") {
ZIO.scoped(serve(DynamicServer.app).as(List(staticSpec)))
}.provideLayerShared(env) @@ timeout(5 seconds)

private def staticSpec = suite("Static RandomAccessFile Server")(
suite("fromResource")(
suite("file") {
val fileOk = Http.fromResource("TestFile.txt").deploy
val fileNotFound = Http.fromResource("Nothing").deploy
suite("file")(
test("should have 200 status code") {
val res = fileOk.run().map(_.status)
assertZIO(res)(equalTo(Status.Ok))
} +
test("should have content-length") {
val res = fileOk.run().map(_.contentLength)
assertZIO(res)(isSome(equalTo(7L)))
} +
test("should have content") {
val res = fileOk.run().flatMap(_.body.asString)
assertZIO(res)(equalTo("foo\nbar"))
} +
test("should have content-type") {
val res = fileOk.run().map(_.mediaType)
assertZIO(res)(isSome(equalTo(MediaType.text.plain)))
} +
test("should respond with empty") {
val res = fileNotFound.run().map(_.status)
assertZIO(res)(equalTo(Status.NotFound))
}
},
},
test("should have content-length") {
val res = fileOk.run().map(_.contentLength)
assertZIO(res)(isSome(equalTo(7L)))
},
test("should have content") {
val res = fileOk.run().flatMap(_.body.asString)
assertZIO(res)(equalTo("foo\nbar"))
},
test("should have content-type") {
val res = fileOk.run().map(_.mediaType)
assertZIO(res)(isSome(equalTo(MediaType.text.plain)))
},
test("should respond with empty") {
val res = fileNotFound.run().map(_.status)
assertZIO(res)(equalTo(Status.NotFound))
},
),
),
suite("fromFile")(
suite("failure on construction")(
Expand All @@ -64,6 +71,30 @@ object StaticFileServerSpec extends HttpRunnableSpec {
},
),
),
suite("fromResourceWithURL")(
suite("with 'jar' protocol")(
test("should have 200 status code") {
val res = resourceOk.run().map(_.status)
assertZIO(res)(equalTo(Status.Ok))
},
test("should have content-length") {
val res = resourceOk.run().map(_.contentLength)
assertZIO(res)(isSome(equalTo(7L)))
},
test("should have content") {
val res = resourceOk.run().flatMap(_.body.asString)
assertZIO(res)(equalTo("foo\nbar"))
},
test("should have content-type") {
val res = resourceOk.run().map(_.mediaType)
assertZIO(res)(isSome(equalTo(MediaType.text.plain)))
},
test("should respond with empty") {
val res = resourceNotFound.run().map(_.status)
assertZIO(res)(equalTo(Status.NotFound))
},
),
),
)

}

0 comments on commit 11f2d06

Please sign in to comment.