Skip to content

Instantly share code, notes, and snippets.

@sirthias
Created March 6, 2014 16:01
Show Gist options
  • Save sirthias/9392921 to your computer and use it in GitHub Desktop.
Save sirthias/9392921 to your computer and use it in GitHub Desktop.
+ routing: add `conditional` support to FileAndResourceDirectives
diff --git a/spray-routing/src/main/resources/reference.conf b/spray-routing/src/main/resources/reference.conf
index 67c6177..08aaf84 100644
--- a/spray-routing/src/main/resources/reference.conf
+++ b/spray-routing/src/main/resources/reference.conf
@@ -21,6 +21,9 @@ spray.routing {
# the size of an individual chunk when streaming file content
file-chunking-chunk-size = 128k
+ # Enables/disables ETag and `If-Modified-Since` support for FileAndResourceDirectives
+ file-get-conditional = on
+
# Enables/disables the rendering of the "rendered by" footer in directory listings
render-vanity-footer = yes
diff --git a/spray-routing/src/main/scala/spray/routing/RoutingSettings.scala b/spray-routing/src/main/scala/spray/routing/RoutingSettings.scala
index 6612dbf..c0bd6ed 100644
--- a/spray-routing/src/main/scala/spray/routing/RoutingSettings.scala
+++ b/spray-routing/src/main/scala/spray/routing/RoutingSettings.scala
@@ -24,6 +24,7 @@ case class RoutingSettings(
verboseErrorMessages: Boolean,
fileChunkingThresholdSize: Long,
fileChunkingChunkSize: Int,
+ fileGetConditional: Boolean,
users: Config,
renderVanityFooter: Boolean) {
@@ -36,6 +37,7 @@ object RoutingSettings extends SettingsCompanion[RoutingSettings]("spray.routing
c getBoolean "verbose-error-messages",
c getBytes "file-chunking-threshold-size",
c getIntBytes "file-chunking-chunk-size",
+ c getBoolean "file-get-conditional",
c getConfig "users",
c getBoolean "render-vanity-footer")
diff --git a/spray-routing/src/main/scala/spray/routing/directives/FileAndResourceDirectives.scala b/spray-routing/src/main/scala/spray/routing/directives/FileAndResourceDirectives.scala
index 34f0fe4..29bc785 100644
--- a/spray-routing/src/main/scala/spray/routing/directives/FileAndResourceDirectives.scala
+++ b/spray-routing/src/main/scala/spray/routing/directives/FileAndResourceDirectives.scala
@@ -28,6 +28,7 @@ import HttpHeaders._
/* format: OFF */
trait FileAndResourceDirectives {
+ import CacheConditionDirectives._
import ChunkingDirectives._
import ExecutionDirectives._
import MethodDirectives._
@@ -64,8 +65,8 @@ trait FileAndResourceDirectives {
get {
detach() {
if (file.isFile && file.canRead) {
- respondWithLastModifiedHeader(file.lastModified) {
- autoChunk(settings.fileChunkingThresholdSize, settings.fileChunkingChunkSize) {
+ autoChunked.apply {
+ conditionalFor(file.length, file.lastModified).apply {
complete(HttpEntity(contentType, HttpData(file)))
}
}
@@ -73,10 +74,20 @@ trait FileAndResourceDirectives {
}
}
+ private def autoChunked(implicit settings: RoutingSettings, refFactory: ActorRefFactory): Directive0 =
+ autoChunk(settings.fileChunkingThresholdSize, settings.fileChunkingChunkSize)
+
+ private def conditionalFor(length: Long, lastModified: Long)(implicit settings: RoutingSettings): Directive0 =
+ if (settings.fileGetConditional) {
+ val tag = java.lang.Long.toHexString(lastModified ^ java.lang.Long.reverse(length))
+ val lastModifiedDateTime = DateTime(math.min(lastModified, System.currentTimeMillis))
+ conditional(EntityTag(tag), lastModifiedDateTime)
+ } else BasicDirectives.noop
+
/**
* Adds a Last-Modified header to all HttpResponses from its inner Route.
*/
- def respondWithLastModifiedHeader(timestamp: Long): Directive0 =
+ def respondWithLastModifiedHeader(timestamp: Long): Directive0 = // TODO: remove since it is not needed anymore here
respondWithHeader(`Last-Modified`(DateTime(math.min(timestamp, System.currentTimeMillis))))
/**
@@ -104,19 +115,22 @@ trait FileAndResourceDirectives {
theClassLoader.getResource(resourceName) match {
case null ⇒ reject
case url ⇒
- val lastModified = {
+ val (length, lastModified) = {
val conn = url.openConnection()
conn.setUseCaches(false) // otherwise the JDK will keep the JAR file open when we close!
+ val len = conn.getContentLengthLong
val lm = conn.getLastModified
conn.getInputStream.close()
- lm
+ len -> lm
}
implicit val bufferMarshaller = BasicMarshallers.byteArrayMarshaller(contentType)
- respondWithLastModifiedHeader(lastModified) {
- complete {
- // readAllBytes closes the InputStream when done, which ends up closing the JAR file
- // if caching has been disabled on the connection
- FileUtils.readAllBytes(url.openStream())
+ autoChunked.apply { // TODO: add implicit RoutingSettings to method and use here
+ conditionalFor(length, lastModified).apply {
+ complete {
+ // readAllBytes closes the InputStream when done, which ends up closing the JAR file
+ // if caching has been disabled on the connection
+ FileUtils.readAllBytes(url.openStream())
+ }
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment