rack.git  about / heads / tags
a modular Ruby webserver interface
blob 0a257b3d54df6496d47713c6c156df00df8324aa 4907 bytes (raw)
$ git show rfc7231-sec6.3.6-205:lib/rack/file.rb	# shows this blob on the CLI

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
 
require 'time'
require 'rack/utils'
require 'rack/mime'
require 'rack/request'
require 'rack/head'

module Rack
  # Rack::File serves files below the +root+ directory given, according to the
  # path info of the Rack request.
  # e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file
  # as http://localhost:9292/passwd
  #
  # Handlers can detect if bodies are a Rack::File, and use mechanisms
  # like sendfile on the +path+.

  class File
    ALLOWED_VERBS = %w[GET HEAD OPTIONS]
    ALLOW_HEADER = ALLOWED_VERBS.join(', ')

    attr_reader :root

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

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

    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
        false
      end

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

    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

      # Set custom headers
      @headers.each { |field, content| headers[field] = content } if @headers

      response = [ 200, headers ]

      size = filesize path

      range = nil
      ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
      if ranges.nil? || ranges.length > 1
        # No ranges, or multiple ranges (which we don't support):
        # TODO: Support multiple byte-ranges
        response[0] = 200
        range = 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:
        range = ranges[0]
        response[0] = 206
        response[1]["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
        size = range.end - range.begin + 1
      end

      response[2] = [response_body] unless response_body.nil?

      response[1][CONTENT_LENGTH] = size.to_s
      response[2] = make_body request, path, range
      response
    end

    class Iterator
      attr_reader :path, :range
      alias :to_path :path

      def initialize path, range
        @path  = path
        @range = range
      end

      def each
        ::File.open(path, "rb") do |file|
          file.seek(range.begin)
          remaining_len = range.end-range.begin+1
          while remaining_len > 0
            part = file.read([8192, remaining_len].min)
            break unless part
            remaining_len -= part.length

            yield part
          end
        end
      end

      def close; end
    end

    private

    def make_body request, path, range
      if request.head?
        []
      else
        Iterator.new path, range
      end
    end

    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

    # The MIME type for the contents of the file located at @path
    def mime_type path, default_mime
      Mime.mime_type(::File.extname(path), default_mime)
    end

    def filesize path
      # If response_body is present, use its size.
      return Rack::Utils.bytesize(response_body) if response_body

      #   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

    # By default, the response body for file requests is nil.
    # In this case, the response body will be generated later
    # from the file at @path
    def response_body
      nil
    end
  end
end

git clone https://yhbt.net/rack.git