123456789_123456789_123456789_123456789_123456789_

Class: ActionDispatch::FileHandler

Relationships & Source Files
Inherits: Object
Defined in: actionpack/lib/action_dispatch/middleware/static.rb

Overview

This endpoint serves static files from disk using Rack::Files.

URL paths are matched with static files according to expected conventions: path, path.html, path/index.html.

Precompressed versions of these files are checked first. Brotli (.br) and gzip (.gz) files are supported. If path.br exists, this endpoint returns that file with a content-encoding: br header.

If no matching file is found, this endpoint responds 404 Not Found.

Pass the :: directory to search for matching files, an optional index: "index" to change the default path/index.html, and optional additional response headers.

Constant Summary

Class Method Summary

Instance Method Summary

Constructor Details

.new(root, index: "index", headers: {}, precompressed: %i[ br gzip ],, compressible_content_types: /\A(?:text\/|application\/javascript|image\/svg\+xml)/) ⇒ FileHandler

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 60

def initialize(root, index: "index", headers: {}, precompressed: %i[ br gzip ], compressible_content_types: /\A(?:text\/|application\/javascript|image\/svgxml)/)
  @root = root.chomp("/").b
  @index = index

  @precompressed = Array(precompressed).map(&:to_s) | %w[ identity ]
  @compressible_content_types = compressible_content_types

  @file_server = ::Rack::Files.new(@root, headers)
end

Instance Method Details

#accept_encoding(env) (private)

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 156

def accept_encoding(env)
  ::Rack::Utils.q_values(env["HTTP_ACCEPT_ENCODING"])
end

#attempt(env)

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 74

def attempt(env)
  request_method = env[::Rack::REQUEST_METHOD]

  if request_method == "GET" || request_method == "HEAD"
    if found = find_file(env[::Rack::PATH_INFO], env)
      serve Rack::Request.new(env), *found
    end
  end
end

#call(env)

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 70

def call(env)
  attempt(env) || @file_server.call(env)
end

#clean_path(path_info) (private)

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 224

def clean_path(path_info)
  path = ::Rack::Utils.unescape_path path_info.chomp("/")
  if ::Rack::Utils.valid_path? path
    ::Rack::Utils.clean_path_info path
  end
end

#compressible?(content_type) ⇒ Boolean (private)

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 170

def compressible?(content_type)
  @compressible_content_types.match?(content_type)
end

#each_candidate_filepath(path_info) {|path, content_type || "text/plain"| ... } (private)

Yields:

  • (path, content_type || "text/plain")
[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 183

def each_candidate_filepath(path_info)
  return unless path = clean_path(path_info)

  # Most requests are for dynamic routes with nothing like them under the
  # root directory. Probe the path's first segment once before building
  # and stat(2)ing each candidate: when "/users" doesn't exist, neither
  # can "/users/1", "/users/1.html", or "/users/1/index.html".
  root = path == "/"
  slash = path.index("/", 1)
  top_level = slash ? path[1...slash] : path[1..]
  top_level_exists = !root && root_entry?(top_level)

  ext = ::File.extname(path)
  content_type = ::Rack::Mime.mime_type(ext, nil)
  yield path, content_type || "text/plain" if top_level_exists

  # Tack on .html and /index.html only for paths that don't have an explicit,
  # resolvable file extension. No need to check for foo.js.html and
  # foo.js/index.html.
  unless content_type
    default_ext = ::ActionController::Base.default_static_extension
    if ext != default_ext
      default_content_type = ::Rack::Mime.mime_type(default_ext, "text/plain")

      # A top-level "/foo" may be served by "foo.html" rather than by an
      # entry named "foo", so probe for that name instead.
      if slash ? top_level_exists : root_entry?("#{top_level}#{default_ext}")
        yield "#{path}#{default_ext}", default_content_type
      end

      if root
        yield "/#{@index}#{default_ext}", default_content_type if root_entry?("#{@index}#{default_ext}")
      elsif top_level_exists
        yield "#{path}/#{@index}#{default_ext}", default_content_type
      end
    end
  end

  nil
end

#each_precompressed_filepath(filepath) (private)

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 174

def each_precompressed_filepath(filepath)
  @precompressed.each do |content_encoding|
    precompressed_ext = PRECOMPRESSED.fetch(content_encoding)
    yield content_encoding, "#{filepath}#{precompressed_ext}"
  end

  nil
end

#file_readable?(path) ⇒ Boolean (private)

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 160

def file_readable?(path)
  file_path = File.join(@root, path.b)
  File.file?(file_path) && File.readable?(file_path)
end

#find_file(path_info, env) (private)

Match a URI path to a static file to be served.

Used by the Static class to negotiate a servable file in the public/ directory (see Static#call).

Checks for path, path.html, and path/index.html files, in that order, including .br and .gzip compressed extensions.

If a matching file is found, the path and necessary response headers (Content-Type, Content-Encoding) are returned.

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 109

def find_file(path_info, env)
  each_candidate_filepath(path_info) do |filepath, content_type|
    if response = try_files(filepath, content_type, env)
      return response
    end
  end
end

#root_entry?(name) ⇒ Boolean (private)

Whether the root directory has an entry (file or directory) by this name.

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 166

def root_entry?(name)
  File.exist?("#{@root}/#{name.b}")
end

#serve(request, filepath, content_headers) (private)

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 85

def serve(request, filepath, content_headers)
  original, request.path_info =
    request.path_info, ::Rack::Utils.escape_path(filepath).b

  @file_server.call(request.env).tap do |status, headers, body|
    # Omit content-encoding/type/etc headers for 304 Not Modified
    if status != 304
      headers.update(content_headers)
    end
  end
ensure
  request.path_info = original
end

#try_files(filepath, content_type, env) (private)

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 120

def try_files(filepath, content_type, env)
  headers = { Rack::CONTENT_TYPE => content_type }

  if DEFAULT_UTF8_CONTENT_TYPES.include?(content_type)
    headers[Rack::CONTENT_TYPE] = "#{content_type}; charset=utf-8"
  end

  if compressible? content_type
    try_precompressed_files filepath, headers, env
  elsif file_readable? filepath
    [ filepath, headers ]
  end
end

#try_precompressed_files(filepath, headers, env) (private)

[ GitHub ]

  
# File 'actionpack/lib/action_dispatch/middleware/static.rb', line 134

def try_precompressed_files(filepath, headers, env)
  each_precompressed_filepath(filepath) do |content_encoding, precompressed_filepath|
    if file_readable? precompressed_filepath
      # Identity encoding is default, so we skip Accept-Encoding negotiation and
      # needn't set Content-Encoding.
      #
      # Vary header is expected when we've found other available encodings that
      # Accept-Encoding ruled out.
      if content_encoding == "identity"
        return precompressed_filepath, headers
      else
        headers[ActionDispatch::Constants::VARY] = "accept-encoding"

        if accept_encoding(env).any? { |enc, _| ACCEPTABLE[content_encoding].match?(enc) }
          headers[ActionDispatch::Constants::CONTENT_ENCODING] = content_encoding
          return precompressed_filepath, headers
        end
      end
    end
  end
end