From ba397463c308786458437e2dedb13d7b6f006fce Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 4 Dec 2015 12:34:04 +0100 Subject: [PATCH] !htc, htp #16991 add support for precompressed media-types and `.gz` file extension suffixes --- .../akka/http/javadsl/model/MediaType.java | 2 +- .../akka/http/javadsl/model/MediaTypes.java | 4 +- .../impl/model/parser/CommonActions.scala | 2 +- .../akka/http/scaladsl/model/MediaType.scala | 254 ++++++++++-------- .../impl/model/parser/HttpHeaderSpec.scala | 8 +- .../FileAndResourceDirectivesSpec.scala | 25 +- .../akka/http/scaladsl/coding/Encoder.scala | 2 +- .../server/directives/CodingDirectives.scala | 10 + .../FileAndResourceDirectives.scala | 43 +-- 9 files changed, 203 insertions(+), 147 deletions(-) diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/MediaType.java b/akka-http-core/src/main/java/akka/http/javadsl/model/MediaType.java index 8f330fce21..b53721cc38 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/MediaType.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/MediaType.java @@ -22,7 +22,7 @@ public interface MediaType { /** * True when this media-type is generally compressible. */ - boolean compressible(); + boolean isCompressible(); /** * True when this media-type is not character-based. diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/MediaTypes.java b/akka-http-core/src/main/java/akka/http/javadsl/model/MediaTypes.java index c7daac4c5a..18b68730bb 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/MediaTypes.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/MediaTypes.java @@ -188,7 +188,9 @@ public abstract class MediaTypes { * Creates a custom media type. */ public static MediaType custom(String value, boolean binary, boolean compressible) { - return akka.http.scaladsl.model.MediaType.custom(value, binary, compressible, List.empty()); + akka.http.scaladsl.model.MediaType.Compressibility comp = compressible ? + akka.http.scaladsl.model.MediaType.Compressible$.MODULE$ : akka.http.scaladsl.model.MediaType.NotCompressible$.MODULE$; + return akka.http.scaladsl.model.MediaType.custom(value, binary, comp , List.empty()); } /** diff --git a/akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonActions.scala b/akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonActions.scala index 5a0e50842c..46ddddb41a 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonActions.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonActions.scala @@ -32,7 +32,7 @@ private[parser] trait CommonActions { if (charsetDefined) MediaType.customWithOpenCharset(mainLower, subType, params = params, allowArbitrarySubtypes = true) else - MediaType.customBinary(mainLower, subType, compressible = true, params = params, + MediaType.customBinary(mainLower, subType, MediaType.Compressible, params = params, allowArbitrarySubtypes = true) } } diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaType.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaType.scala index 36e32c5968..558bacc3a6 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaType.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaType.scala @@ -33,9 +33,11 @@ import akka.http.impl.util.JavaMapping.Implicits._ * Like binary MediaTypes `WithFixedCharset` types can be implicitly converted to a [[ContentType]]. */ sealed abstract class MediaType extends jm.MediaType with LazyValueBytesRenderable with WithQValue[MediaRange] { + import MediaType.Compressibility def fileExtensions: List[String] def params: Map[String, String] + def comp: Compressibility override def isApplication: Boolean = false override def isAudio: Boolean = false @@ -46,7 +48,7 @@ sealed abstract class MediaType extends jm.MediaType with LazyValueBytesRenderab override def isVideo: Boolean = false def withParams(params: Map[String, String]): MediaType - + def withComp(comp: Compressibility): MediaType def withQValue(qValue: Float): MediaRange = MediaRange(this, qValue.toFloat) override def equals(that: Any): Boolean = @@ -62,12 +64,13 @@ sealed abstract class MediaType extends jm.MediaType with LazyValueBytesRenderab */ def toRange = jm.MediaRanges.create(this) def toRange(qValue: Float) = jm.MediaRanges.create(this, qValue) + def isCompressible: Boolean = comp.compressible } object MediaType { - def applicationBinary(subType: String, compressible: Boolean, fileExtensions: String*): Binary = - new Binary("application/" + subType, "application", subType, compressible, fileExtensions.toList) { + def applicationBinary(subType: String, comp: Compressibility, fileExtensions: String*): Binary = + new Binary("application/" + subType, "application", subType, comp, fileExtensions.toList) { override def isApplication = true } @@ -82,18 +85,18 @@ object MediaType { override def isApplication = true } - def audio(subType: String, compressible: Boolean, fileExtensions: String*): Binary = - new Binary("audio/" + subType, "audio", subType, compressible, fileExtensions.toList) { + def audio(subType: String, comp: Compressibility, fileExtensions: String*): Binary = + new Binary("audio/" + subType, "audio", subType, comp, fileExtensions.toList) { override def isAudio = true } - def image(subType: String, compressible: Boolean, fileExtensions: String*): Binary = - new Binary("image/" + subType, "image", subType, compressible, fileExtensions.toList) { + def image(subType: String, comp: Compressibility, fileExtensions: String*): Binary = + new Binary("image/" + subType, "image", subType, comp, fileExtensions.toList) { override def isImage = true } - def message(subType: String, compressible: Boolean, fileExtensions: String*): Binary = - new Binary("message/" + subType, "message", subType, compressible, fileExtensions.toList) { + def message(subType: String, comp: Compressibility, fileExtensions: String*): Binary = + new Binary("message/" + subType, "message", subType, comp, fileExtensions.toList) { override def isMessage = true } @@ -102,17 +105,17 @@ object MediaType { override def isText = true } - def video(subType: String, compressible: Boolean, fileExtensions: String*): Binary = - new Binary("video/" + subType, "video", subType, compressible, fileExtensions.toList) { + def video(subType: String, comp: Compressibility, fileExtensions: String*): Binary = + new Binary("video/" + subType, "video", subType, comp, fileExtensions.toList) { override def isVideo = true } - def customBinary(mainType: String, subType: String, compressible: Boolean, fileExtensions: List[String] = Nil, + def customBinary(mainType: String, subType: String, comp: Compressibility, fileExtensions: List[String] = Nil, params: Map[String, String] = Map.empty, allowArbitrarySubtypes: Boolean = false): Binary = { require(mainType != "multipart", "Cannot create a MediaType.Multipart here, use `customMultipart` instead!") require(allowArbitrarySubtypes || subType != "*", "Cannot create a MediaRange here, use `MediaRange.custom` instead!") val _params = params - new Binary(renderValue(mainType, subType, params), mainType, subType, compressible, fileExtensions) { + new Binary(renderValue(mainType, subType, params), mainType, subType, comp, fileExtensions) { override def params = _params override def isApplication = mainType == "application" override def isAudio = mainType == "audio" @@ -162,11 +165,11 @@ object MediaType { new Multipart(subType, params) } - def custom(value: String, binary: Boolean, compressible: Boolean = true, + def custom(value: String, binary: Boolean, comp: Compressibility = Compressible, fileExtensions: List[String] = Nil): MediaType = { val parts = value.split('/') require(parts.length == 2, s"`$value` is not a valid media-type. It must consist of two parts separated by '/'.") - if (binary) customBinary(parts(0), parts(1), compressible, fileExtensions) + if (binary) customBinary(parts(0), parts(1), comp, fileExtensions) else customWithOpenCharset(parts(0), parts(1), fileExtensions) } @@ -187,12 +190,14 @@ object MediaType { r.get } - sealed abstract class Binary(val value: String, val mainType: String, val subType: String, val compressible: Boolean, + sealed abstract class Binary(val value: String, val mainType: String, val subType: String, val comp: Compressibility, val fileExtensions: List[String]) extends MediaType with jm.MediaType.Binary { def binary = true def params: Map[String, String] = Map.empty def withParams(params: Map[String, String]): Binary with MediaType = - customBinary(mainType, subType, compressible, fileExtensions, params) + customBinary(mainType, subType, comp, fileExtensions, params) + def withComp(comp: Compressibility): Binary with MediaType = + customBinary(mainType, subType, comp, fileExtensions, params) /** * JAVA API @@ -202,7 +207,9 @@ object MediaType { sealed abstract class NonBinary extends MediaType with jm.MediaType.NonBinary { def binary = false - def compressible = true + def comp = Compressible + def withComp(comp: Compressibility): Binary with MediaType = + customBinary(mainType, subType, comp, fileExtensions, params) } sealed abstract class WithFixedCharset(val value: String, val mainType: String, val subType: String, @@ -244,20 +251,30 @@ object MediaType { def withBoundary(boundary: String): MediaType.Multipart = withParams(if (boundary.isEmpty) params - "boundary" else params.updated("boundary", boundary)) } + + sealed abstract class Compressibility(val compressible: Boolean) + case object Compressible extends Compressibility(compressible = true) + case object NotCompressible extends Compressibility(compressible = false) + case object Gzipped extends Compressibility(compressible = false) } object MediaTypes extends ObjectRegistry[(String, String), MediaType] { private[this] var extensionMap = Map.empty[String, MediaType] - def forExtension(ext: String): Option[MediaType] = extensionMap.get(ext.toRootLowerCase) + def forExtensionOption(ext: String): Option[MediaType] = extensionMap.get(ext.toLowerCase) + def forExtension(ext: String): MediaType = extensionMap.getOrElse(ext.toLowerCase, `application/octet-stream`) - private def register[T <: MediaType](mediaType: T): T = { - def registerFileExtension(ext: String): Unit = { - val lcExt = ext.toRootLowerCase + private def registerFileExtensions[T <: MediaType](mediaType: T): T = { + mediaType.fileExtensions.foreach { ext ⇒ + val lcExt = ext.toLowerCase require(!extensionMap.contains(lcExt), s"Extension '$ext' clash: media-types '${extensionMap(lcExt)}' and '$mediaType'") extensionMap = extensionMap.updated(lcExt, mediaType) } - mediaType.fileExtensions.foreach(registerFileExtension) + mediaType + } + + private def register[T <: MediaType](mediaType: T): T = { + registerFileExtensions(mediaType) register(mediaType.mainType.toRootLowerCase -> mediaType.subType.toRootLowerCase, mediaType) } @@ -265,122 +282,121 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] { /////////////////////////// PREDEFINED MEDIA-TYPE DEFINITION //////////////////////////// // format: OFF - private final val compressible = true // compile-time constant - private final val uncompressible = false // compile-time constant - private def abin(st: String, c: Boolean, fe: String*) = register(applicationBinary(st, c, fe: _*)) - private def awfc(st: String, cs: HttpCharset, fe: String*) = register(applicationWithFixedCharset(st, cs, fe: _*)) - private def awoc(st: String, fe: String*) = register(applicationWithOpenCharset(st, fe: _*)) - private def aud(st: String, c: Boolean, fe: String*) = register(audio(st, c, fe: _*)) - private def img(st: String, c: Boolean, fe: String*) = register(image(st, c, fe: _*)) - private def msg(st: String, fe: String*) = register(message(st, compressible, fe: _*)) - private def txt(st: String, fe: String*) = register(text(st, fe: _*)) - private def vid(st: String, fe: String*) = register(video(st, compressible, fe: _*)) + private def abin(st: String, c: Compressibility, fe: String*) = register(applicationBinary(st, c, fe: _*)) + private def awfc(st: String, cs: HttpCharset, fe: String*) = register(applicationWithFixedCharset(st, cs, fe: _*)) + private def awoc(st: String, fe: String*) = register(applicationWithOpenCharset(st, fe: _*)) + private def aud(st: String, c: Compressibility, fe: String*) = register(audio(st, c, fe: _*)) + private def img(st: String, c: Compressibility, fe: String*) = register(image(st, c, fe: _*)) + private def msg(st: String, fe: String*) = register(message(st, Compressible, fe: _*)) + private def txt(st: String, fe: String*) = register(text(st, fe: _*)) + private def vid(st: String, fe: String*) = register(video(st, NotCompressible, fe: _*)) // dummy value currently only used by ContentType.NoContentType - private[http] val NoMediaType = MediaType.customBinary("none", "none", compressible = false) + private[http] val NoMediaType = MediaType.customBinary("none", "none", comp = NotCompressible) val `application/atom+xml` = awoc("atom+xml", "atom") val `application/base64` = awoc("base64", "mm", "mme") - val `application/excel` = abin("excel", uncompressible, "xl", "xla", "xlb", "xlc", "xld", "xlk", "xll", "xlm", "xls", "xlt", "xlv", "xlw") - val `application/font-woff` = abin("font-woff", uncompressible, "woff") - val `application/gnutar` = abin("gnutar", uncompressible, "tgz") - val `application/java-archive` = abin("java-archive", uncompressible, "jar", "war", "ear") + val `application/excel` = abin("excel", NotCompressible, "xl", "xla", "xlb", "xlc", "xld", "xlk", "xll", "xlm", "xls", "xlt", "xlv", "xlw") + val `application/font-woff` = abin("font-woff", NotCompressible, "woff") + val `application/gnutar` = abin("gnutar", NotCompressible, "tgz") + val `application/java-archive` = abin("java-archive", NotCompressible, "jar", "war", "ear") val `application/javascript` = awoc("javascript", "js") val `application/json` = awfc("json", HttpCharsets.`UTF-8`, "json") val `application/json-patch+json` = awfc("json-patch+json", HttpCharsets.`UTF-8`) - val `application/lha` = abin("lha", uncompressible, "lha") - val `application/lzx` = abin("lzx", uncompressible, "lzx") - val `application/mspowerpoint` = abin("mspowerpoint", uncompressible, "pot", "pps", "ppt", "ppz") - val `application/msword` = abin("msword", uncompressible, "doc", "dot", "w6w", "wiz", "word", "wri") - val `application/octet-stream` = abin("octet-stream", uncompressible, "a", "bin", "class", "dump", "exe", "lhx", "lzh", "o", "psd", "saveme", "zoo") - val `application/pdf` = abin("pdf", uncompressible, "pdf") - val `application/postscript` = abin("postscript", compressible, "ai", "eps", "ps") + val `application/lha` = abin("lha", NotCompressible, "lha") + val `application/lzx` = abin("lzx", NotCompressible, "lzx") + val `application/mspowerpoint` = abin("mspowerpoint", NotCompressible, "pot", "pps", "ppt", "ppz") + val `application/msword` = abin("msword", NotCompressible, "doc", "dot", "w6w", "wiz", "word", "wri") + val `application/octet-stream` = abin("octet-stream", NotCompressible, "a", "bin", "class", "dump", "exe", "lhx", "lzh", "o", "psd", "saveme", "zoo") + val `application/pdf` = abin("pdf", NotCompressible, "pdf") + val `application/postscript` = abin("postscript", Compressible, "ai", "eps", "ps") val `application/rss+xml` = awoc("rss+xml", "rss") val `application/soap+xml` = awoc("soap+xml") val `application/vnd.api+json` = awfc("vnd.api+json", HttpCharsets.`UTF-8`) val `application/vnd.google-earth.kml+xml` = awoc("vnd.google-earth.kml+xml", "kml") - val `application/vnd.google-earth.kmz` = abin("vnd.google-earth.kmz", uncompressible, "kmz") - val `application/vnd.ms-fontobject` = abin("vnd.ms-fontobject", compressible, "eot") - val `application/vnd.oasis.opendocument.chart` = abin("vnd.oasis.opendocument.chart", compressible, "odc") - val `application/vnd.oasis.opendocument.database` = abin("vnd.oasis.opendocument.database", compressible, "odb") - val `application/vnd.oasis.opendocument.formula` = abin("vnd.oasis.opendocument.formula", compressible, "odf") - val `application/vnd.oasis.opendocument.graphics` = abin("vnd.oasis.opendocument.graphics", compressible, "odg") - val `application/vnd.oasis.opendocument.image` = abin("vnd.oasis.opendocument.image", compressible, "odi") - val `application/vnd.oasis.opendocument.presentation` = abin("vnd.oasis.opendocument.presentation", compressible, "odp") - val `application/vnd.oasis.opendocument.spreadsheet` = abin("vnd.oasis.opendocument.spreadsheet", compressible, "ods") - val `application/vnd.oasis.opendocument.text` = abin("vnd.oasis.opendocument.text", compressible, "odt") - val `application/vnd.oasis.opendocument.text-master` = abin("vnd.oasis.opendocument.text-master", compressible, "odm", "otm") - val `application/vnd.oasis.opendocument.text-web` = abin("vnd.oasis.opendocument.text-web", compressible, "oth") - val `application/vnd.openxmlformats-officedocument.presentationml.presentation` = abin("vnd.openxmlformats-officedocument.presentationml.presentation", compressible, "pptx") - val `application/vnd.openxmlformats-officedocument.presentationml.slide` = abin("vnd.openxmlformats-officedocument.presentationml.slide", compressible, "sldx") - val `application/vnd.openxmlformats-officedocument.presentationml.slideshow` = abin("vnd.openxmlformats-officedocument.presentationml.slideshow", compressible, "ppsx") - val `application/vnd.openxmlformats-officedocument.presentationml.template` = abin("vnd.openxmlformats-officedocument.presentationml.template", compressible, "potx") - val `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` = abin("vnd.openxmlformats-officedocument.spreadsheetml.sheet", compressible, "xlsx") - val `application/vnd.openxmlformats-officedocument.spreadsheetml.template` = abin("vnd.openxmlformats-officedocument.spreadsheetml.template", compressible, "xltx") - val `application/vnd.openxmlformats-officedocument.wordprocessingml.document` = abin("vnd.openxmlformats-officedocument.wordprocessingml.document", compressible, "docx") - val `application/vnd.openxmlformats-officedocument.wordprocessingml.template` = abin("vnd.openxmlformats-officedocument.wordprocessingml.template", compressible, "dotx") - val `application/x-7z-compressed` = abin("x-7z-compressed", uncompressible, "7z", "s7z") - val `application/x-ace-compressed` = abin("x-ace-compressed", uncompressible, "ace") - val `application/x-apple-diskimage` = abin("x-apple-diskimage", uncompressible, "dmg") - val `application/x-arc-compressed` = abin("x-arc-compressed", uncompressible, "arc") - val `application/x-bzip` = abin("x-bzip", uncompressible, "bz") - val `application/x-bzip2` = abin("x-bzip2", uncompressible, "boz", "bz2") - val `application/x-chrome-extension` = abin("x-chrome-extension", uncompressible, "crx") - val `application/x-compress` = abin("x-compress", uncompressible, "z") - val `application/x-compressed` = abin("x-compressed", uncompressible, "gz") - val `application/x-debian-package` = abin("x-debian-package", compressible, "deb") - val `application/x-dvi` = abin("x-dvi", compressible, "dvi") - val `application/x-font-truetype` = abin("x-font-truetype", compressible, "ttf") - val `application/x-font-opentype` = abin("x-font-opentype", compressible, "otf") - val `application/x-gtar` = abin("x-gtar", uncompressible, "gtar") - val `application/x-gzip` = abin("x-gzip", uncompressible, "gzip") + val `application/vnd.google-earth.kmz` = abin("vnd.google-earth.kmz", NotCompressible, "kmz") + val `application/vnd.ms-fontobject` = abin("vnd.ms-fontobject", Compressible, "eot") + val `application/vnd.oasis.opendocument.chart` = abin("vnd.oasis.opendocument.chart", Compressible, "odc") + val `application/vnd.oasis.opendocument.database` = abin("vnd.oasis.opendocument.database", Compressible, "odb") + val `application/vnd.oasis.opendocument.formula` = abin("vnd.oasis.opendocument.formula", Compressible, "odf") + val `application/vnd.oasis.opendocument.graphics` = abin("vnd.oasis.opendocument.graphics", Compressible, "odg") + val `application/vnd.oasis.opendocument.image` = abin("vnd.oasis.opendocument.image", Compressible, "odi") + val `application/vnd.oasis.opendocument.presentation` = abin("vnd.oasis.opendocument.presentation", Compressible, "odp") + val `application/vnd.oasis.opendocument.spreadsheet` = abin("vnd.oasis.opendocument.spreadsheet", Compressible, "ods") + val `application/vnd.oasis.opendocument.text` = abin("vnd.oasis.opendocument.text", Compressible, "odt") + val `application/vnd.oasis.opendocument.text-master` = abin("vnd.oasis.opendocument.text-master", Compressible, "odm", "otm") + val `application/vnd.oasis.opendocument.text-web` = abin("vnd.oasis.opendocument.text-web", Compressible, "oth") + val `application/vnd.openxmlformats-officedocument.presentationml.presentation` = abin("vnd.openxmlformats-officedocument.presentationml.presentation", Compressible, "pptx") + val `application/vnd.openxmlformats-officedocument.presentationml.slide` = abin("vnd.openxmlformats-officedocument.presentationml.slide", Compressible, "sldx") + val `application/vnd.openxmlformats-officedocument.presentationml.slideshow` = abin("vnd.openxmlformats-officedocument.presentationml.slideshow", Compressible, "ppsx") + val `application/vnd.openxmlformats-officedocument.presentationml.template` = abin("vnd.openxmlformats-officedocument.presentationml.template", Compressible, "potx") + val `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` = abin("vnd.openxmlformats-officedocument.spreadsheetml.sheet", Compressible, "xlsx") + val `application/vnd.openxmlformats-officedocument.spreadsheetml.template` = abin("vnd.openxmlformats-officedocument.spreadsheetml.template", Compressible, "xltx") + val `application/vnd.openxmlformats-officedocument.wordprocessingml.document` = abin("vnd.openxmlformats-officedocument.wordprocessingml.document", Compressible, "docx") + val `application/vnd.openxmlformats-officedocument.wordprocessingml.template` = abin("vnd.openxmlformats-officedocument.wordprocessingml.template", Compressible, "dotx") + val `application/x-7z-compressed` = abin("x-7z-compressed", NotCompressible, "7z", "s7z") + val `application/x-ace-compressed` = abin("x-ace-compressed", NotCompressible, "ace") + val `application/x-apple-diskimage` = abin("x-apple-diskimage", NotCompressible, "dmg") + val `application/x-arc-compressed` = abin("x-arc-compressed", NotCompressible, "arc") + val `application/x-bzip` = abin("x-bzip", NotCompressible, "bz") + val `application/x-bzip2` = abin("x-bzip2", NotCompressible, "boz", "bz2") + val `application/x-chrome-extension` = abin("x-chrome-extension", NotCompressible, "crx") + val `application/x-compress` = abin("x-compress", NotCompressible, "z") + val `application/x-compressed` = abin("x-compressed", NotCompressible, "gz") + val `application/x-debian-package` = abin("x-debian-package", Compressible, "deb") + val `application/x-dvi` = abin("x-dvi", Compressible, "dvi") + val `application/x-font-truetype` = abin("x-font-truetype", Compressible, "ttf") + val `application/x-font-opentype` = abin("x-font-opentype", Compressible, "otf") + val `application/x-gtar` = abin("x-gtar", NotCompressible, "gtar") + val `application/x-gzip` = abin("x-gzip", NotCompressible, "gzip") val `application/x-latex` = awoc("x-latex", "latex", "ltx") - val `application/x-rar-compressed` = abin("x-rar-compressed", uncompressible, "rar") - val `application/x-redhat-package-manager` = abin("x-redhat-package-manager", uncompressible, "rpm") - val `application/x-shockwave-flash` = abin("x-shockwave-flash", uncompressible, "swf") - val `application/x-tar` = abin("x-tar", compressible, "tar") - val `application/x-tex` = abin("x-tex", compressible, "tex") - val `application/x-texinfo` = abin("x-texinfo", compressible, "texi", "texinfo") + val `application/x-rar-compressed` = abin("x-rar-compressed", NotCompressible, "rar") + val `application/x-redhat-package-manager` = abin("x-redhat-package-manager", NotCompressible, "rpm") + val `application/x-shockwave-flash` = abin("x-shockwave-flash", NotCompressible, "swf") + val `application/x-tar` = abin("x-tar", Compressible, "tar") + val `application/x-tex` = abin("x-tex", Compressible, "tex") + val `application/x-texinfo` = abin("x-texinfo", Compressible, "texi", "texinfo") val `application/x-vrml` = awoc("x-vrml", "vrml") val `application/x-www-form-urlencoded` = awoc("x-www-form-urlencoded") - val `application/x-x509-ca-cert` = abin("x-x509-ca-cert", compressible, "der") - val `application/x-xpinstall` = abin("x-xpinstall", uncompressible, "xpi") + val `application/x-x509-ca-cert` = abin("x-x509-ca-cert", Compressible, "der") + val `application/x-xpinstall` = abin("x-xpinstall", NotCompressible, "xpi") val `application/xhtml+xml` = awoc("xhtml+xml") val `application/xml-dtd` = awoc("xml-dtd") val `application/xml` = awoc("xml") - val `application/zip` = abin("zip", uncompressible, "zip") + val `application/zip` = abin("zip", NotCompressible, "zip") - val `audio/aiff` = aud("aiff", compressible, "aif", "aifc", "aiff") - val `audio/basic` = aud("basic", compressible, "au", "snd") - val `audio/midi` = aud("midi", compressible, "mid", "midi", "kar") - val `audio/mod` = aud("mod", uncompressible, "mod") - val `audio/mpeg` = aud("mpeg", uncompressible, "m2a", "mp2", "mp3", "mpa", "mpga") - val `audio/ogg` = aud("ogg", uncompressible, "oga", "ogg") - val `audio/voc` = aud("voc", uncompressible, "voc") - val `audio/vorbis` = aud("vorbis", uncompressible, "vorbis") - val `audio/voxware` = aud("voxware", uncompressible, "vox") - val `audio/wav` = aud("wav", compressible, "wav") - val `audio/x-realaudio` = aud("x-pn-realaudio", uncompressible, "ra", "ram", "rmm", "rmp") - val `audio/x-psid` = aud("x-psid", compressible, "sid") - val `audio/xm` = aud("xm", uncompressible, "xm") - val `audio/webm` = aud("webm", uncompressible) + val `audio/aiff` = aud("aiff", Compressible, "aif", "aifc", "aiff") + val `audio/basic` = aud("basic", Compressible, "au", "snd") + val `audio/midi` = aud("midi", Compressible, "mid", "midi", "kar") + val `audio/mod` = aud("mod", NotCompressible, "mod") + val `audio/mpeg` = aud("mpeg", NotCompressible, "m2a", "mp2", "mp3", "mpa", "mpga") + val `audio/ogg` = aud("ogg", NotCompressible, "oga", "ogg") + val `audio/voc` = aud("voc", NotCompressible, "voc") + val `audio/vorbis` = aud("vorbis", NotCompressible, "vorbis") + val `audio/voxware` = aud("voxware", NotCompressible, "vox") + val `audio/wav` = aud("wav", Compressible, "wav") + val `audio/x-realaudio` = aud("x-pn-realaudio", NotCompressible, "ra", "ram", "rmm", "rmp") + val `audio/x-psid` = aud("x-psid", Compressible, "sid") + val `audio/xm` = aud("xm", NotCompressible, "xm") + val `audio/webm` = aud("webm", NotCompressible) - val `image/gif` = img("gif", uncompressible, "gif") - val `image/jpeg` = img("jpeg", uncompressible, "jpe", "jpeg", "jpg") - val `image/pict` = img("pict", compressible, "pic", "pict") - val `image/png` = img("png", uncompressible, "png") - val `image/svg+xml` = img("svg+xml", compressible, "svg") - val `image/tiff` = img("tiff", compressible, "tif", "tiff") - val `image/x-icon` = img("x-icon", compressible, "ico") - val `image/x-ms-bmp` = img("x-ms-bmp", compressible, "bmp") - val `image/x-pcx` = img("x-pcx", compressible, "pcx") - val `image/x-pict` = img("x-pict", compressible, "pct") - val `image/x-quicktime` = img("x-quicktime", uncompressible, "qif", "qti", "qtif") - val `image/x-rgb` = img("x-rgb", compressible, "rgb") - val `image/x-xbitmap` = img("x-xbitmap", compressible, "xbm") - val `image/x-xpixmap` = img("x-xpixmap", compressible, "xpm") - val `image/webp` = img("webp", uncompressible, "webp") + val `image/gif` = img("gif", NotCompressible, "gif") + val `image/jpeg` = img("jpeg", NotCompressible, "jpe", "jpeg", "jpg") + val `image/pict` = img("pict", Compressible, "pic", "pict") + val `image/png` = img("png", NotCompressible, "png") + val `image/svg+xml` = img("svg+xml", Compressible, "svg") + val `image/svgz` = registerFileExtensions(image("svg+xml", Gzipped, "svgz")) + val `image/tiff` = img("tiff", Compressible, "tif", "tiff") + val `image/x-icon` = img("x-icon", Compressible, "ico") + val `image/x-ms-bmp` = img("x-ms-bmp", Compressible, "bmp") + val `image/x-pcx` = img("x-pcx", Compressible, "pcx") + val `image/x-pict` = img("x-pict", Compressible, "pct") + val `image/x-quicktime` = img("x-quicktime", NotCompressible, "qif", "qti", "qtif") + val `image/x-rgb` = img("x-rgb", Compressible, "rgb") + val `image/x-xbitmap` = img("x-xbitmap", Compressible, "xbm") + val `image/x-xpixmap` = img("x-xpixmap", Compressible, "xpm") + val `image/webp` = img("webp", NotCompressible, "webp") val `message/http` = msg("http") val `message/delivery-status` = msg("delivery-status") @@ -406,7 +422,7 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] { val `text/asp` = txt("asp", "asp") val `text/cache-manifest` = txt("cache-manifest", "manifest") - val `text/calendar` = txt("calendar", "ics", "icz") + val `text/calendar` = txt("calendar", "ics") val `text/css` = txt("css", "css") val `text/csv` = txt("csv", "csv") val `text/html` = txt("html", "htm", "html", "htmls", "htx") diff --git a/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala index ea3eaa3aad..5bb0ef48e8 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala @@ -19,7 +19,7 @@ import HttpEncodings._ import HttpMethods._ class HttpHeaderSpec extends FreeSpec with Matchers { - val `application/vnd.spray` = MediaType.applicationBinary("vnd.spray", compressible = true) + val `application/vnd.spray` = MediaType.applicationBinary("vnd.spray", MediaType.Compressible) val PROPFIND = HttpMethod.custom("PROPFIND") "The HTTP header model must correctly parse and render the headers" - { @@ -38,9 +38,9 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Accept: */*, text/*; foo=bar, custom/custom; bar=\"b>az\"" =!= Accept(`*/*`, MediaRange.custom("text", Map("foo" -> "bar")), - MediaType.customBinary("custom", "custom", compressible = true, params = Map("bar" -> "b>az"))) + MediaType.customBinary("custom", "custom", MediaType.Compressible, params = Map("bar" -> "b>az"))) "Accept: application/*+xml; version=2" =!= - Accept(MediaType.customBinary("application", "*+xml", compressible = true, params = Map("version" -> "2"))) + Accept(MediaType.customBinary("application", "*+xml", MediaType.Compressible, params = Map("version" -> "2"))) } "Accept-Charset" in { @@ -208,7 +208,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers { `Content-Type`(`multipart/mixed` withBoundary "ABC/123" withCharset `UTF-8`) .renderedTo("""multipart/mixed; boundary="ABC/123"; charset=UTF-8""") "Content-Type: application/*" =!= - `Content-Type`(MediaType.customBinary("application", "*", compressible = false, allowArbitrarySubtypes = true)) + `Content-Type`(MediaType.customBinary("application", "*", MediaType.Compressible, allowArbitrarySubtypes = true)) } "Content-Range" in { diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala index 68b6b4c591..ac31b1cb17 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala @@ -9,7 +9,6 @@ import java.io.File import scala.concurrent.duration._ import scala.concurrent.{ ExecutionContext, Future } import scala.util.Properties -import akka.util.ByteString import org.scalatest.matchers.Matcher import org.scalatest.{ Inside, Inspectors } import akka.http.scaladsl.model.MediaTypes._ @@ -92,6 +91,30 @@ class FileAndResourceDirectivesSpec extends RoutingSpec with Inspectors with Ins } } finally file.delete } + + "support precompressed files with registered MediaType" in { + val file = File.createTempFile("akkaHttpTest", ".svgz") + try { + writeAllText("123", file) + Get() ~> getFromFile(file) ~> check { + mediaType shouldEqual `image/svg+xml` + header[`Content-Encoding`] shouldEqual Some(`Content-Encoding`(HttpEncodings.gzip)) + responseAs[String] shouldEqual "123" + } + } finally file.delete + } + + "support files with registered MediaType and .gz suffix" in { + val file = File.createTempFile("akkaHttpTest", ".js.gz") + try { + writeAllText("456", file) + Get() ~> getFromFile(file) ~> check { + mediaType shouldEqual `application/javascript` + header[`Content-Encoding`] shouldEqual Some(`Content-Encoding`(HttpEncodings.gzip)) + responseAs[String] shouldEqual "456" + } + } finally file.delete + } } "getFromResource" should { diff --git a/akka-http/src/main/scala/akka/http/scaladsl/coding/Encoder.scala b/akka-http/src/main/scala/akka/http/scaladsl/coding/Encoder.scala index bf29f27b5d..39bfc4d8a9 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/coding/Encoder.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/coding/Encoder.scala @@ -46,7 +46,7 @@ object Encoder { case res @ HttpResponse(status, _, _, _) ⇒ isCompressible(res) && status.isSuccess } private[coding] def isCompressible(msg: HttpMessage): Boolean = - msg.entity.contentType.mediaType.compressible + msg.entity.contentType.mediaType.isCompressible private[coding] val isContentEncodingHeader: HttpHeader ⇒ Boolean = _.isInstanceOf[`Content-Encoding`] } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala index f25e7d463f..9ff0fb8ddf 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala @@ -108,6 +108,16 @@ trait CodingDirectives { */ def decodeRequest: Directive0 = decodeRequestWith(DefaultCoders: _*) + + /** + * Inspects the response entity and adds a `Content-Encoding: gzip` response header if + * the entities media-type is precompressed with gzip and no `Content-Encoding` header is present yet. + */ + def withPrecompressedMediaTypeSupport: Directive0 = + mapResponse { response ⇒ + if (response.entity.contentType.mediaType.comp != MediaType.Gzipped) response + else response.withDefaultHeaders(headers.`Content-Encoding`(HttpEncodings.gzip)) + } } object CodingDirectives extends CodingDirectives { diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectives.scala index 00e800593b..47640228bb 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectives.scala @@ -26,7 +26,6 @@ trait FileAndResourceDirectives { import RouteDirectives._ import BasicDirectives._ import RouteConcatenation._ - import RangeDirectives._ /** * Completes GET requests with the content of the given file. @@ -51,12 +50,10 @@ trait FileAndResourceDirectives { if (file.isFile && file.canRead) conditionalFor(file.length, file.lastModified) { if (file.length > 0) { - withRangeSupport { - extractSettings { settings ⇒ - complete { - HttpEntity.Default(contentType, file.length, - Source.file(file).withAttributes(ActorAttributes.dispatcher(settings.fileIODispatcher))) - } + withRangeSupportAndPrecompressedMediaTypeSupportAndExtractSettings { settings ⇒ + complete { + HttpEntity.Default(contentType, file.length, + Source.file(file).withAttributes(ActorAttributes.dispatcher(settings.fileIODispatcher))) } } } else complete(HttpEntity.Empty) @@ -90,13 +87,11 @@ trait FileAndResourceDirectives { case Some(ResourceFile(url, length, lastModified)) ⇒ conditionalFor(length, lastModified) { if (length > 0) { - withRangeSupport { - extractSettings { settings ⇒ - complete { - HttpEntity.Default(contentType, length, - Source.inputStream(() ⇒ url.openStream()) - .withAttributes(ActorAttributes.dispatcher(settings.fileIODispatcher))) - } + withRangeSupportAndPrecompressedMediaTypeSupportAndExtractSettings { settings ⇒ + complete { + HttpEntity.Default(contentType, length, + Source.inputStream(() ⇒ url.openStream()) + .withAttributes(ActorAttributes.dispatcher(settings.fileIODispatcher))) } } } else complete(HttpEntity.Empty) @@ -186,6 +181,11 @@ trait FileAndResourceDirectives { } object FileAndResourceDirectives extends FileAndResourceDirectives { + private val withRangeSupportAndPrecompressedMediaTypeSupportAndExtractSettings = + RangeDirectives.withRangeSupport & + CodingDirectives.withPrecompressedMediaTypeSupport & + BasicDirectives.extractSettings + private def withTrailingSlash(path: String): String = if (path endsWith "/") path else path + '/' private def fileSystemPath(base: String, path: Uri.Path, log: LoggingAdapter, separator: Char = File.separatorChar): String = { import java.lang.StringBuilder @@ -260,11 +260,16 @@ object ContentTypeResolver { def withDefaultCharset(charset: HttpCharset): ContentTypeResolver = new ContentTypeResolver { def apply(fileName: String) = { - val ext = fileName.lastIndexOf('.') match { - case -1 ⇒ "" - case x ⇒ fileName.substring(x + 1) - } - val mediaType = MediaTypes.forExtension(ext) getOrElse MediaTypes.`application/octet-stream` + val lastDotIx = fileName.lastIndexOf('.') + val mediaType = if (lastDotIx >= 0) { + fileName.substring(lastDotIx + 1) match { + case "gz" ⇒ fileName.lastIndexOf('.', lastDotIx - 1) match { + case -1 ⇒ MediaTypes.`application/octet-stream` + case x ⇒ MediaTypes.forExtension(fileName.substring(x + 1, lastDotIx)).withComp(MediaType.Gzipped) + } + case ext ⇒ MediaTypes.forExtension(ext) + } + } else MediaTypes.`application/octet-stream` ContentType(mediaType, () ⇒ charset) } }