-
-
Save bmc/2db513245a4d7213ba7aba4f67723d12 to your computer and use it in GitHub Desktop.
| object StaticFile { | |
| // Various necessary imports. Notes: | |
| // | |
| // 1. fs2 is necessary. See https://github.com/functional-streams-for-scala/fs2 | |
| // 2. streamz is necessary. See https://github.com/krasserm/streamz | |
| // 3. Apache Tika is used to infer MIME types from file names, because it's more reliable and | |
| // fully-featured than using java.nio.file.Files.probeContentType(). | |
| // | |
| // If using SBT, you'll want these library dependencies and resolvers: | |
| // | |
| // resolvers += "krasserm at bintray" at "http://dl.bintray.com/krasserm/maven" | |
| // libraryDependencies ++= Seq( | |
| // "com.typesafe.akka" %% "akka-http" % "10.0.0", | |
| // "com.typesafe.akka" %% "akka-http-spray-json" % "10.0.0", | |
| // "com.github.krasserm" %% "streamz-akka-stream" % "0.5.1", | |
| // "org.apache.tika" % "tika-core" % "1.14", | |
| // "co.fs2" %% "fs2-core" % "0.9.2", | |
| // "co.fs2" %% "fs2-io" % "0.9.2" | |
| // ) | |
| import akka.NotUsed | |
| import akka.http.scaladsl.server.Directives._ | |
| import akka.stream.scaladsl.{Source => AkkaSource} | |
| import akka.http.scaladsl.model._ | |
| import akka.util.ByteString | |
| import akka.stream.{Graph, SourceShape} | |
| import streamz.akka.stream._ | |
| import fs2._ | |
| import org.apache.tika.Tika | |
| import scala.concurrent.{ExecutionContext, Future} | |
| import scala.util.control.NonFatal | |
| import java.nio.file.{AccessDeniedException, NoSuchFileException, Paths} | |
| val BaseDir = "/path/to/static/base/dir" | |
| val tika = new Tika | |
| /** Serve up a static file, using fs2. Here's how to use this function inside | |
| * an Akka HTTP route: | |
| * | |
| * {{{ | |
| * pathPrefix("static" / Remaining) { urlPath => | |
| * get { | |
| * complete(StaticFile.serve(s"static/$urlPath")) | |
| * } | |
| * } ~ | |
| * path("/") { | |
| * get { | |
| * complete(StaticFile.serve("index.html")) | |
| * } | |
| * } ~ // etc | |
| * }}} | |
| * | |
| * @param urlPath the URL path, as extracted by Akka. | |
| * @param ctx the execution context (presumably from Akka, e.g., `ActorSystem("server").dispatcher`) | |
| * | |
| * @return An Akka `HttpResponse`, wrapped in a `Future`. | |
| */ | |
| def serve(urlPath: String)(implicit ctx: ExecutionContext): Future[HttpResponse] = { | |
| Future { | |
| // In this case, the base directory is hard-coded, and we need | |
| // to use it to determine the actual on-disk path. There are other | |
| // approaches one could use (e.g., reading the base directory from | |
| // a config, allowing an environment variable override, etc.). | |
| val pathPieces = BaseDir :+ urlPath.split("/") | |
| val path = Paths.get(pathPieces.mkString(java.io.File.separator)) | |
| // Use Apache Tika to get the MIME type from the path. This isn't | |
| // wrapped in an Option because it always seems to return *something*. | |
| val mime = tika.detect(path) | |
| // Parse the resulting MIME type string into an Akka HTTP ContentType. | |
| ContentType.parse(mime) match { | |
| case Right(contentType) => | |
| // We got a valid MIME type, so it's time to try to read the file. | |
| // fs2 will throw exceptions on error; those are explicitly handled | |
| // in the Future's recover() block, below. | |
| // Note: I have added explicit types for clarity. Obviously, you can | |
| // leave them off and let the compiler infer them. | |
| val source: Graph[SourceShape[ByteString], NotUsed] = | |
| io.file. // We want to use fs2 to read a file. | |
| readAll[Task](path, 8192). // Read the whole file as an effectful stream, in 8K chunks | |
| chunks. // Get the chunks | |
| map { ch: NonEmptyChunk[Byte] => | |
| // We need to map the fs2 chunks into Akka ByteString objects, since | |
| // that's what Akka expects in its streams. | |
| ByteString(ch.toArray) | |
| }. | |
| // Convert the FS2 Stream[A, ByteString] to an Akka Stream | |
| // Graph[SourceShape[ByteString], NotUsed]. As Martin Krasser | |
| // notes, the actual FS2 -> Akka Stream conversion happens here, | |
| // via streamz implicits. | |
| toSource | |
| // Map the Akka Stream Graph object into an Akka Source. | |
| val akkaSource: AkkaSource[ByteString, NotUsed] = AkkaSource.fromGraph(source) | |
| // We can return the Akka Source wrapped in the HttpResponse. Note that | |
| // NO data has been read from the file yet! When Akka HTTP materializes its Source, | |
| // to stream the bytes up to the browser, it will implicitly materialize the | |
| // underlying fs2 stream. The "edge" here is inside Akka HTTP's browser-delivery | |
| // logic. | |
| HttpResponse(StatusCodes.OK, entity = HttpEntity(contentType, akkaSource)) | |
| case Left(errors) => | |
| // We failed to determine the MIME type. | |
| HttpResponse(StatusCodes.InternalServerError, entity = s"Can't determine MIME type: ${errors.mkString(" ")}") | |
| } | |
| } | |
| .recover { | |
| // Here's where we handle any errors that occur. Note that fs2 I/O errors will | |
| // throw java.nio exceptions. We map them explicitly into HTTP result codes. | |
| case e: NoSuchFileException => | |
| HttpResponse(StatusCodes.NotFound, entity = "Not found.") | |
| case e: AccessDeniedException => | |
| HttpResponse(StatusCodes.Forbidden, entity = "Permission denied.") | |
| case NonFatal(e) => | |
| HttpResponse(StatusCodes.InternalServerError, entity = s"Read failure: $e") | |
| } | |
| } | |
| } |
Hi,
Lines 65 & 66 seem a bit odd. From the REPL of Scala 2.12.6, there is the following:
==========================================================
Welcome to Scala 2.12.6 (OpenJDK 64-Bit Server VM, Java 1.8.0_171).
Type in expressions for evaluation. Or try :help.
scala> import java.nio.file.{AccessDeniedException, NoSuchFileException, Paths}
import java.nio.file.{AccessDeniedException, NoSuchFileException, Paths}
scala> val urlPath = "index.html"
urlPath: String = index.html
scala> val BaseDir = "/path/to/some/dir"
BaseDir: String = /path/to/some/dir
scala> val pathPieces = BaseDir :+ urlPath.split("/")
pathPieces: scala.collection.immutable.IndexedSeq[Any] = Vector(/, p, a, t, h, /, t, o, /, s, o, m, e, /, d, i, r, Array(index.html))
scala> val path = Paths.get(pathPieces.mkString(java.io.File.separator))
path: java.nio.file.Path = /p/a/t/h/t/o/s/o/m/e/d/i/r/[Ljava.lang.String;@2bb717d7
The path does not look right. Am I missing something here?
Thanks.
This Gist incorporates suggestions from both @mpilquist and @krasserm.