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

spray.routing.directives.RangeDirectives.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 shapeless.HNil
import spray.httpx.marshalling.Marshaller
import spray.http._
import StatusCodes._
import HttpHeaders._

trait RangeDirectives {
  import BasicDirectives._
  import RouteDirectives._

  /**
   * Answers GET requests with an `Accept-Ranges: bytes` header and converts HttpResponses coming back from its inner
   * route into partial responses if the initial request contained a valid `Range` request header. The requested
   * byte-ranges may be coalesced.
   * This directive is transparent to non-GET requests
   * Rejects requests with unsatisfiable ranges `UnsatisfiableRangeRejection`.
   * Rejects requests with too many expected ranges.
   *
   * Note: if you want to combine this directive with `conditional(...)` you need to put
   * it on the *inside* of the `conditional(...)` directive, i.e. `conditional(...)` must be
   * on a higher level in your route structure in order to function correctly.
   *
   * @see https://tools.ietf.org/html/draft-ietf-httpbis-p5-range/
   */
  def withRangeSupport(m: RangeDirectives.WithRangeSupportMagnet): Directive0 = {
    import m._

    class IndexRange(val start: Long, val end: Long) {
      def length = end - start
      def apply(entity: HttpEntity.NonEmpty) = HttpEntity(entity.contentType, entity.data.slice(start, length))
      def distance(other: IndexRange) = mergedEnd(other) - mergedStart(other) - (length + other.length)
      def mergeWith(other: IndexRange) = new IndexRange(mergedStart(other), mergedEnd(other))
      def contentRangeHeader(entity: HttpEntity.NonEmpty) = `Content-Range`(ContentRange(start, end - 1, entity.data.length))
      private def mergedStart(other: IndexRange) = math.min(start, other.start)
      private def mergedEnd(other: IndexRange) = math.max(end, other.end)
    }

    def indexRange(entityLength: Long)(range: ByteRange): IndexRange =
      range match {
        case ByteRange.Slice(start, end)    ⇒ new IndexRange(start, math.min(end + 1, entityLength))
        case ByteRange.FromOffset(first)    ⇒ new IndexRange(first, entityLength)
        case ByteRange.Suffix(suffixLength) ⇒ new IndexRange(math.max(0, entityLength - suffixLength), entityLength)
      }

    /**
     * When multiple ranges are requested, a server may coalesce any of the ranges that overlap or that are separated
     * by a gap that is smaller than the overhead of sending multiple parts, regardless of the order in which the
     * corresponding byte-range-spec appeared in the received Range header field. Since the typical overhead between
     * parts of a multipart/byteranges payload is around 80 bytes, depending on the selected representation's
     * media type and the chosen boundary parameter length, it can be less efficient to transfer many small
     * disjoint parts than it is to transfer the entire selected representation.
     */
    def coalesceRanges(iRanges: Seq[IndexRange]): Seq[IndexRange] =
      iRanges.foldLeft(Seq.empty[IndexRange]) { (acc, iRange) ⇒
        val (mergeCandidates, otherCandidates) = acc.partition(_.distance(iRange) <= rangeCoalescingThreshold)
        val merged = mergeCandidates.foldLeft(iRange)(_ mergeWith _)
        otherCandidates :+ merged
      }

    def multipartRanges(ranges: Seq[ByteRange], entity: HttpEntity.NonEmpty) = {
      val iRanges = ranges.map(indexRange(entity.data.length))
      val bodyParts = coalesceRanges(iRanges).map(ir ⇒ BodyPart(ir(entity), ir.contentRangeHeader(entity) :: Nil))
      MultipartByteRanges(bodyParts)
    }

    def rangeResponse(range: ByteRange, entity: HttpEntity.NonEmpty, headers: List[HttpHeader]) = {
      val aiRange = indexRange(entity.data.length)(range)
      HttpResponse(PartialContent, aiRange(entity), aiRange.contentRangeHeader(entity) :: headers)
    }

    def satisfiable(entityLength: Long)(range: ByteRange): Boolean =
      range match {
        case ByteRange.Slice(firstPos, _)   ⇒ firstPos < entityLength
        case ByteRange.FromOffset(firstPos) ⇒ firstPos < entityLength
        case ByteRange.Suffix(length)       ⇒ length > 0
      }

    def applyRanges(ranges: Seq[ByteRange]): Directive0 =
      mapRequestContext { ctx ⇒
        ctx.withRouteResponseHandling {
          case HttpResponse(OK, entity: HttpEntity.NonEmpty, headers, protocol) ⇒
            ranges.filter(satisfiable(entity.data.length)) match {
              case Nil                   ⇒ ctx.reject(UnsatisfiableRangeRejection(ranges, entity.data.length))
              case Seq(satisfiableRange) ⇒ ctx.complete(rangeResponse(satisfiableRange, entity, headers))
              case satisfiableRanges     ⇒ ctx.complete(PartialContent, headers, multipartRanges(satisfiableRanges, entity))
            }
        }
      }

    def rangeHeaderOfGetRequests(ctx: RequestContext): Option[Range] =
      if (ctx.request.method == HttpMethods.GET) ctx.request.header[Range] else None

    extract(rangeHeaderOfGetRequests).flatMap[HNil] {
      case Some(Range(RangeUnit.Bytes, ranges)) ⇒
        if (ranges.size <= rangeCountLimit) applyRanges(ranges) & RangeDirectives.respondWithAcceptByteRangesHeader
        else reject(TooManyRangesRejection(rangeCountLimit))
      case _ ⇒ MethodDirectives.get & RangeDirectives.respondWithAcceptByteRangesHeader | pass
    }
  }
}

object RangeDirectives extends RangeDirectives {
  private val respondWithAcceptByteRangesHeader: Directive0 =
    RespondWithDirectives.respondWithHeader(`Accept-Ranges`(RangeUnit.Bytes))

  class WithRangeSupportMagnet(val rangeCountLimit: Int, val rangeCoalescingThreshold: Long)(implicit m: Marshaller[MultipartByteRanges])
  object WithRangeSupportMagnet {
    implicit def fromSettings(u: Unit)(implicit settings: RoutingSettings, m: Marshaller[MultipartByteRanges]) =
      new WithRangeSupportMagnet(settings.rangeCountLimit, settings.rangeCoalescingThreshold)
    implicit def fromCountLimitAndCoalescingThreshold(t: (Int, Long))(implicit m: Marshaller[MultipartByteRanges]) =
      new WithRangeSupportMagnet(t._1, t._2)
  }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy