class Racknga::Middleware::Range

This is a middleware that provides HTTP range request (partial request) support. For example, HTTP range request is used for playing a video on the way.

Usage:

require "racknga"
use Racknga::Middleware::Range
run YourApplication

Public Class Methods

new(application) click to toggle source
# File lib/racknga/middleware/range.rb, line 31
def initialize(application)
  @application = application
end

Public Instance Methods

call(environment) click to toggle source

For Rack.

# File lib/racknga/middleware/range.rb, line 36
def call(environment)
  status, headers, body = @application.call(environment)
  return [status, headers, body] if status != 200

  headers = Rack::Utils::HeaderHash.new(headers)
  headers["Accept-Ranges"] = "bytes"
  request = Rack::Request.new(environment)
  range = request.env["HTTP_RANGE"]
  if range and /\Abytes=(\d*)-(\d*)\z/ =~ range
    first_byte, last_byte = $1, $2
    status, headers, body = apply_range(status, headers, body, request,
                                        first_byte, last_byte)
  end
  [status, headers.to_hash, body]
end

Private Instance Methods

apply_range(status, headers, body, request, first_byte, last_byte) click to toggle source
# File lib/racknga/middleware/range.rb, line 53
def apply_range(status, headers, body, request, first_byte, last_byte)
  unless use_range?(request, headers)
    return [status, headers, body]
  end
  length = guess_length(headers, body)
  return [status, headers.to_hash, body] if length.nil?

  if first_byte.empty? and last_byte.empty?
    headers["Content-Length"] = "0"
    return [Rack::Utils.status_code(:requested_range_not_satisfiable),
            headers,
            []]
  end

  if last_byte.empty?
    last_byte = length - 1
  else
    last_byte = last_byte.to_i
  end
  if first_byte.empty?
    first_byte = length - last_byte
    last_byte = length - 1
  else
    first_byte = first_byte.to_i
  end

  byte_range_spec = "#{first_byte}-#{last_byte}/#{length}"
  range_length = last_byte - first_byte + 1
  headers["Content-Range"] = "bytes #{byte_range_spec}"
  headers["Content-Length"] = range_length.to_s
  stream = RangeStream.new(body, first_byte, range_length)
  if body.respond_to?(:to_path)
    def stream.to_path
      @body.to_path
    end
  end
  [Rack::Utils.status_code(:partial_content),
   headers,
   stream]
end
guess_length(headers, body) click to toggle source
# File lib/racknga/middleware/range.rb, line 113
def guess_length(headers, body)
  length = headers["Content-Length"]
  return length.to_i unless length.nil?
  return body.stat.size if body.respond_to?(:stat)
  nil
end
use_range?(request, headers) click to toggle source
# File lib/racknga/middleware/range.rb, line 94
def use_range?(request, headers)
  if_range = request.env["HTTP_IF_RANGE"]
  return true if if_range.nil?

  if /\A(?:Mo|Tu|We|Th|Fr|Sa|Su)/ =~ if_range
    last_modified = headers["Last-Modified"]
    return false if last_modified.nil?
    begin
      if_range = Time.httpdate(if_range)
      last_modified = Time.httpdate(last_modified)
    rescue ArgumentError
      return true
    end
    if_range == last_modified
  else
    if_range == headers["ETag"]
  end
end