Skip to content

Commit

Permalink
Improve StaticFile.fromResource handling
Browse files Browse the repository at this point in the history
Should fix both #2468 and #2834
  • Loading branch information
hamnis committed Dec 1, 2019
1 parent acc09b5 commit 5f879f2
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 6 deletions.
16 changes: 11 additions & 5 deletions core/src/main/scala/org/http4s/StaticFile.scala
Expand Up @@ -29,26 +29,32 @@ object StaticFile {
name: String,
blocker: Blocker,
req: Option[Request[F]] = None,
preferGzipped: Boolean = false): OptionT[F, Response[F]] = {
preferGzipped: Boolean = false,
classloader: Option[ClassLoader] = None): OptionT[F, Response[F]] = {
val loader = classloader.getOrElse(getClass.getClassLoader)

val tryGzipped = preferGzipped && req.flatMap(_.headers.get(`Accept-Encoding`)).exists {
acceptEncoding =>
acceptEncoding.satisfiedBy(ContentCoding.gzip) || acceptEncoding.satisfiedBy(
ContentCoding.`x-gzip`)
}
val normalizedName = name.split("/").filter(_.nonEmpty).mkString("/")

def getResource(name: String) =
OptionT(Sync[F].delay(Option(loader.getResource(name))))

val gzUrl: OptionT[F, URL] =
if (tryGzipped) OptionT(Sync[F].delay(Option(getClass.getResource(name + ".gz"))))
else OptionT.none
if (tryGzipped) getResource(normalizedName + ".gz") else OptionT.none

gzUrl
.flatMap { url =>
// Guess content type from the name without ".gz"
val contentType = nameToContentType(name)
val contentType = nameToContentType(normalizedName)
val headers = `Content-Encoding`(ContentCoding.gzip) :: contentType.toList

fromURL(url, blocker, req).map(_.removeHeader(`Content-Type`).putHeaders(headers: _*))
}
.orElse(OptionT(Sync[F].delay(Option(getClass.getResource(name))))
.orElse(getResource(normalizedName)
.flatMap(fromURL(_, blocker, req)))
}

Expand Down
60 changes: 59 additions & 1 deletion tests/src/test/scala/org/http4s/StaticFileSpec.scala
Expand Up @@ -2,9 +2,10 @@ package org.http4s

import cats.effect.IO
import java.io.File
import java.net.URL
import java.nio.file.Files

import org.http4s.Status.{NotModified, Ok}
import org.http4s.Status._
import org.http4s.headers.ETag.EntityTag
import org.http4s.headers._
import org.specs2.matcher.MatchResult
Expand Down Expand Up @@ -33,6 +34,63 @@ class StaticFileSpec extends Http4sSpec {
check(new File(getClass.getResource(p).toURI), om)
}
}
"load from resource" in {
def check(resource: String, status: Status): MatchResult[Status] = {
val res1 = StaticFile
.fromResource(resource, testBlocker)
.value
.unsafeRunSync()

res1.map(_.status).getOrElse(NotFound) must be(status)
}

val tests = Seq(
"/Animated_PNG_example_bouncing_beach_ball.png" -> Ok,
"/ball.png" -> Ok,
"ball.png" -> Ok,
"Animated_PNG_example_bouncing_beach_ball.png" -> Ok,
"/test.fiddlefaddle" -> Ok,
"test.fiddlefaddle" -> Ok,
"//test.fiddlefaddle" -> Ok,
"missing.html" -> NotFound,
"/missing.html" -> NotFound
)

forall(tests) {
case (resource, status) => check(resource, status)
}
}

"load from resource using different classloader" in {
val loader = new ClassLoader() {
override def getResource(name: String): URL =
getClass.getClassLoader.getResource(name)
}

def check(resource: String, status: Status): MatchResult[Status] = {
val res1 = StaticFile
.fromResource(resource, testBlocker, classloader = Some(loader))
.value
.unsafeRunSync()

res1.map(_.status).getOrElse(NotFound) must be(status)
}

val tests = Seq(
"/Animated_PNG_example_bouncing_beach_ball.png" -> Ok,
"/ball.png" -> Ok,
"ball.png" -> Ok,
"Animated_PNG_example_bouncing_beach_ball.png" -> Ok,
"/test.fiddlefaddle" -> Ok,
"test.fiddlefaddle" -> Ok,
"missing.html" -> NotFound,
"/missing.html" -> NotFound
)

forall(tests) {
case (resource, status) => check(resource, status)
}
}

"handle an empty file" in {
val emptyFile = File.createTempFile("empty", ".tmp")
Expand Down

0 comments on commit 5f879f2

Please sign in to comment.