123456789_123456789_123456789_123456789_123456789_

Class: Rack::Files

Relationships & Source Files
Namespace Children
Classes:
Inherits: Object
Defined in: lib/rack/files.rb

Overview

Files serves files below the #root directory given, according to the path info of the ::Rack request. e.g. when Files.new(“/etc”) is used, you can access ‘passwd’ file as localhost:9292/passwd

Handlers can detect if bodies are a Files, and use mechanisms like sendfile on the path.

Be aware that just like the default behavior of most webservers, Files will follow symbolic links encountered under the root. If a symlink points to a location outside of the root, that target will still be served as part of the response.

Constant Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(root, headers = {}, default_mime = 'text/plain') ⇒ Files

[ GitHub ]

  
# File 'lib/rack/files.rb', line 32

def initialize(root, headers = {}, default_mime = 'text/plain')
  @root = (::File.expand_path(root) if root)
  @headers = headers
  @default_mime = default_mime
  @head = Rack::Head.new(lambda { |env| get env })
end

Instance Attribute Details

#root (readonly)

[ GitHub ]

  
# File 'lib/rack/files.rb', line 30

attr_reader :root

Instance Method Details

#assign_headers(headers, request)

[ GitHub ]

  
# File 'lib/rack/files.rb', line 125

def assign_headers(headers, request)
  headers.merge!(@headers) if @headers
end

#call(env)

[ GitHub ]

  
# File 'lib/rack/files.rb', line 39

def call(env)
  # HEAD requests drop the response body, including 4xx error messages.
  @head.call env
end

#fail(status, body, headers = {}) (private)

[ GitHub ]

  
# File 'lib/rack/files.rb', line 198

def fail(status, body, headers = {})
  body += "\n"

  [
    status,
    {
      CONTENT_TYPE   => "text/plain",
      CONTENT_LENGTH => body.size.to_s,
      "x-cascade" => "pass"
    }.merge!(headers),
    [body]
  ]
end

#filesize(path) (private)

[ GitHub ]

  
# File 'lib/rack/files.rb', line 217

def filesize(path)
  #   We check via File::size? whether this file provides size info
  #   via stat (e.g. /proc files often don't), otherwise we have to
  #   figure it out by reading the whole file into memory.
  ::File.size?(path) || ::File.read(path).bytesize
end

#get(env)

[ GitHub ]

  
# File 'lib/rack/files.rb', line 44

def get(env)
  request = Rack::Request.new env
  unless ALLOWED_VERBS.include? request.request_method
    return fail(405, "Method Not Allowed", { 'allow' => ALLOW_HEADER })
  end

  path_info = Utils.unescape_path request.path_info
  return fail(400, "Bad Request") unless Utils.valid_path?(path_info)

  clean_path_info = Utils.clean_path_info(path_info)
  path = ::File.join(@root, clean_path_info)

  available = begin
    ::File.file?(path) && ::File.readable?(path)
  rescue SystemCallError
    # Not sure in what conditions this exception can occur, but this
    # is a safe way to handle such an error.
    # :nocov:
    false
    # :nocov:
  end

  if available
    serving(request, path)
  else
    fail(404, "File not found: #{path_info}")
  end
end

#mime_type(path, default_mime) (private)

The MIME type for the contents of the file located at @path

[ GitHub ]

  
# File 'lib/rack/files.rb', line 213

def mime_type(path, default_mime)
  Mime.mime_type(::File.extname(path), default_mime)
end

#serving(request, path)

[ GitHub ]

  
# File 'lib/rack/files.rb', line 73

def serving(request, path)
  if request.options?
    return [200, { 'allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []]
  end
  last_modified = ::File.mtime(path).httpdate
  return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified

  headers = { "last-modified" => last_modified }
  mime_type = mime_type path, @default_mime
  headers[CONTENT_TYPE] = mime_type if mime_type

  assign_headers(headers, request)

  status = 200
  size = filesize path

  ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
  if ranges.nil?
    # No ranges:
    ranges = [0..size - 1]
  elsif ranges.empty?
    # Unsatisfiable. Return error, and file size:
    response = fail(416, "Byte range unsatisfiable")
    response[1]["content-range"] = "bytes */#{size}"
    return response
  else
    # Partial content
    partial_content = true

    if ranges.size == 1
      range = ranges[0]
      headers["content-range"] = "bytes #{range.begin}-#{range.end}/#{size}"
    else
      headers[CONTENT_TYPE] = "multipart/byteranges; boundary=#{MULTIPART_BOUNDARY}"
    end

    status = 206
    body = BaseIterator.new(path, ranges, mime_type: mime_type, size: size)
    size = body.bytesize
  end

  headers[CONTENT_LENGTH] = size.to_s

  if request.head?
    body = []
  elsif !partial_content
    body = Iterator.new(path, ranges, mime_type: mime_type, size: size)
  end

  [status, headers, body]
end