All Downloads are FREE. Search and download functionalities are using the official Maven repository.

spray.routing.directives.FileAndResourceDirectives.scala Maven / Gradle / Ivy

Go to download

A suite of lightweight Scala libraries for building and consuming RESTful web services on top of Akka

The newest version!
/*
 * Copyright © 2011-2013 the spray project 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package spray.routing
package directives

import java.io.File
import org.parboiled.common.FileUtils
import scala.annotation.tailrec
import akka.actor.ActorRefFactory
import spray.httpx.marshalling.{ Marshaller, BasicMarshallers }
import spray.util._
import spray.http._
import HttpHeaders._

/* format: OFF */
trait FileAndResourceDirectives {
  import CacheConditionDirectives._
  import ChunkingDirectives._
  import ExecutionDirectives._
  import MethodDirectives._
  import RangeDirectives._
  import RespondWithDirectives._
  import RouteDirectives._
  import MiscDirectives._
  import FileAndResourceDirectives._

  /**
   * Completes GET requests with the content of the given file. The actual I/O operation is
   * running detached in a `Future`, so it doesn't block the current thread (but potentially
   * some other thread !). If the file cannot be found or read the request is rejected.
   */
  def getFromFile(fileName: String)
                 (implicit settings: RoutingSettings, resolver: ContentTypeResolver, refFactory: ActorRefFactory): Route =
    getFromFile(new File(fileName))

  /**
   * Completes GET requests with the content of the given file. The actual I/O operation is
   * running detached in a `Future`, so it doesn't block the current thread (but potentially
   * some other thread !). If the file cannot be found or read the request is rejected.
   */
  def getFromFile(file: File)
                 (implicit settings: RoutingSettings, resolver: ContentTypeResolver, refFactory: ActorRefFactory): Route =
    getFromFile(file, resolver(file.getName))

  /**
   * Completes GET requests with the content of the given file. The actual I/O operation is
   * running detached in a `Future`, so it doesn't block the current thread (but potentially
   * some other thread !). If the file cannot be found or read the request is rejected.
   */
  def getFromFile(file: File, contentType: ContentType)
                 (implicit settings: RoutingSettings, refFactory: ActorRefFactory): Route =
    get {
      detach() {
        if (file.isFile && file.canRead) {
          autoChunked.apply {
            conditionalFor(file.length, file.lastModified).apply {
              withRangeSupport() {
                complete(HttpEntity(contentType, HttpData(file)))
              }
            }
          }
        } else reject
      }
    }

  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 = // TODO: remove when migrating to akka-http
    respondWithHeader(`Last-Modified`(DateTime(math.min(timestamp, System.currentTimeMillis))))

  /**
   * Completes GET requests with the content of the given resource. The actual I/O operation is
   * running detached in a `Future`, so it doesn't block the current thread (but potentially
   * some other thread !).
   * If the file cannot be found or read the Route rejects the request.
   */
  def getFromResource(resourceName: String)
                     (implicit resolver: ContentTypeResolver, refFactory: ActorRefFactory): Route =
    getFromResource(resourceName, resolver(resourceName))

  /**
   * Completes GET requests with the content of the given resource. The actual I/O operation is
   * running detached in a `Future`, so it doesn't block the current thread (but potentially
   * some other thread !).
   * If the file cannot be found or read the Route rejects the request.
   */
  def getFromResource(resourceName: String, contentType: ContentType)
                     (implicit refFactory: ActorRefFactory): Route =
    if (!resourceName.endsWith("/"))
      get {
        detach() {
          val theClassLoader = actorSystem(refFactory).dynamicAccess.classLoader
          theClassLoader.getResource(resourceName) match {
            case null ⇒ reject
            case url ⇒
              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()
                len -> lm
              }
              implicit val bufferMarshaller = BasicMarshallers.byteArrayMarshaller(contentType)
              autoChunked.apply { // TODO: add implicit RoutingSettings to method and use here
                conditionalFor(length, lastModified).apply {
                  withRangeSupport() {
                    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())
                    }
                  }
                }
              }
          }
        }
      }
    else reject // don't serve the content of resource "directories"

  /**
   * Completes GET requests with the content of a file underneath the given directory.
   * The unmatchedPath of the [[spray.RequestContext]] is first transformed by the given pathRewriter function before
   * being appended to the given directoryName to build the final fileName.
   * The actual I/O operation is running detached in a `Future`, so it doesn't block the
   * current thread. If the file cannot be read the Route rejects the request.
   */
  def getFromDirectory(directoryName: String)
                      (implicit settings: RoutingSettings, resolver: ContentTypeResolver,
                       refFactory: ActorRefFactory, log: LoggingContext): Route = {
    val base = withTrailingSlash(directoryName)
    unmatchedPath { path ⇒
      fileSystemPath(base, path) match {
        case ""       ⇒ reject
        case fileName ⇒ getFromFile(fileName)
      }
    }
  }

  /**
   * Completes GET requests with a unified listing of the contents of all given directories.
   * The actual rendering of the directory contents is performed by the in-scope `Marshaller[DirectoryListing]`.
   */
  def listDirectoryContents(directories: String*)
                           (implicit renderer: Marshaller[DirectoryListing], refFactory: ActorRefFactory,
                            log: LoggingContext): Route =
    (get & detach()) {
      unmatchedPath { path ⇒
        requestUri { fullUri ⇒
          val fullPath = fullUri.path.toString
          val matchedLength = fullPath.lastIndexOf(path.toString)
          require(matchedLength >= 0)
          val pathPrefix = fullPath.substring(0, matchedLength)
          val pathString = withTrailingSlash(fileSystemPath("/", path, '/'))
          val dirs = directories flatMap { dir ⇒
            fileSystemPath(withTrailingSlash(dir), path) match {
              case "" ⇒ None
              case fileName ⇒
                val file = new File(fileName)
                if (file.isDirectory && file.canRead) Some(file) else None
            }
          }
          if (dirs.isEmpty) reject
          else complete(DirectoryListing(pathPrefix + pathString, isRoot = pathString == "/", dirs.flatMap(_.listFiles)))
        }
      }
    }

  /**
   * Same as `getFromBrowseableDirectories` with only one directory.
   */
  def getFromBrowseableDirectory(directory: String)
                                (implicit renderer: Marshaller[DirectoryListing], settings: RoutingSettings,
                                 resolver: ContentTypeResolver, refFactory: ActorRefFactory, log: LoggingContext): Route =
    getFromBrowseableDirectories(directory)

  /**
   * Serves the content of the given directories as a file system browser, i.e. files are sent and directories
   * served as browsable listings.
   */
  def getFromBrowseableDirectories(directories: String*)
                                  (implicit renderer: Marshaller[DirectoryListing], settings: RoutingSettings,
                                   resolver: ContentTypeResolver, refFactory: ActorRefFactory, log: LoggingContext): Route = {
    import RouteConcatenation._
    directories.map(getFromDirectory).reduceLeft(_ ~ _) ~ listDirectoryContents(directories: _*)
  }

  /**
   * Same as "getFromDirectory" except that the file is not fetched from the file system but rather from a
   * "resource directory".
   */
  def getFromResourceDirectory(directoryName: String)
                              (implicit resolver: ContentTypeResolver, refFactory: ActorRefFactory, log: LoggingContext): Route = {
    val base = if (directoryName.isEmpty) "" else withTrailingSlash(directoryName)
    unmatchedPath { path ⇒
      fileSystemPath(base, path, separator = '/') match {
        case ""           ⇒ reject
        case resourceName ⇒ getFromResource(resourceName)
      }
    }
  }
}

/* format: ON */

object FileAndResourceDirectives extends FileAndResourceDirectives {
  private def withTrailingSlash(path: String): String = if (path endsWith "/") path else path + '/'
  private def fileSystemPath(base: String, path: Uri.Path, separator: Char = File.separatorChar)(implicit log: LoggingContext): String = {
    import java.lang.StringBuilder
    @tailrec def rec(p: Uri.Path, result: StringBuilder = new StringBuilder(base)): String =
      p match {
        case Uri.Path.Empty       ⇒ result.toString
        case Uri.Path.Slash(tail) ⇒ rec(tail, result.append(separator))
        case Uri.Path.Segment(head, tail) ⇒
          if (head.indexOf('/') >= 0 || head == "..") {
            log.warning("File-system path for base [{}] and Uri.Path [{}] contains suspicious path segment [{}], " +
              "GET access was disallowed", base, path, head)
            ""
          } else rec(tail, result.append(head))
      }
    rec(if (path.startsWithSlash) path.tail else path)
  }
}

trait ContentTypeResolver {
  def apply(fileName: String): ContentType
}

object ContentTypeResolver {

  /**
   * The default way of resolving a filename to a ContentType is by looking up the file extension in the
   * registry of all defined media-types. By default all non-binary file content is assumed to be UTF-8 encoded.
   */
  implicit val Default = withDefaultCharset(HttpCharsets.`UTF-8`)

  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`
        mediaType match {
          case x if !x.binary ⇒ ContentType(x, charset)
          case x              ⇒ ContentType(x)
        }
      }
    }
}

case class DirectoryListing(path: String, isRoot: Boolean, files: Seq[File])

object DirectoryListing {

  private val html = new java.lang.StringBuilder()
    .append("")
    .append("Index of $")
    .append("")
    .append("

Index of $

") .append("
$

$") .append("""
""") .append("""rendered by spray on $""") .append("
$") .append("") .append("") .toString split '$' implicit def DefaultMarshaller(implicit settings: RoutingSettings): Marshaller[DirectoryListing] = Marshaller.delegate[DirectoryListing, String](MediaTypes.`text/html`) { listing ⇒ val DirectoryListing(path, isRoot, files) = listing val filesAndNames = files.map(file ⇒ file -> file.getName).sortBy(_._2) val deduped = filesAndNames.zipWithIndex.flatMap { case (fan @ (file, name), ix) ⇒ if (ix == 0 || filesAndNames(ix - 1)._2 != name) Some(fan) else None } val (directoryFilesAndNames, fileFilesAndNames) = deduped.partition(_._1.isDirectory) def maxNameLength(seq: Seq[(File, String)]) = if (seq.isEmpty) 0 else seq.map(_._2.length).max val maxNameLen = math.max(maxNameLength(directoryFilesAndNames) + 1, maxNameLength(fileFilesAndNames)) val sb = new java.lang.StringBuilder sb.append(html(0)).append(path).append(html(1)).append(path).append(html(2)) if (!isRoot) { val secondToLastSlash = path.lastIndexOf('/', path.lastIndexOf('/', path.length - 1) - 1) sb.append("../" format path.substring(0, secondToLastSlash)) } def lastModified(file: File) = DateTime(file.lastModified).toIsoLikeDateTimeString def start(name: String) = sb.append("").append(name).append("") .append(" " * (maxNameLen - name.length)) def renderDirectory(file: File, name: String) = start(name + '/').append(" ").append(lastModified(file)) def renderFile(file: File, name: String) = { val size = Utils.humanReadableByteCount(file.length, si = true) start(name).append(" ").append(lastModified(file)) sb.append(" ".substring(size.length)).append(size) } for ((file, name) ← directoryFilesAndNames) renderDirectory(file, name) for ((file, name) ← fileFilesAndNames) renderFile(file, name) if (isRoot && files.isEmpty) sb.append("(no files)") sb.append(html(3)) if (settings.renderVanityFooter) sb.append(html(4)).append(DateTime.now.toIsoLikeDateTimeString).append(html(5)) sb.append(html(6)).toString } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy