1 # frozen_string_literal: true
5 require_relative 'constants'
6 require_relative 'head'
7 require_relative 'utils'
8 require_relative 'request'
9 require_relative 'mime'
12 # Rack::Files serves files below the +root+ directory given, according to the
13 # path info of the Rack request.
14 # e.g. when Rack::Files.new("/etc") is used, you can access 'passwd' file
15 # as http://localhost:9292/passwd
17 # Handlers can detect if bodies are a Rack::Files, and use mechanisms
18 # like sendfile on the +path+.
21 ALLOWED_VERBS = %w[GET HEAD OPTIONS]
22 ALLOW_HEADER = ALLOWED_VERBS.join(', ')
23 MULTIPART_BOUNDARY = 'AaB03x'
27 def initialize(root, headers = {}, default_mime = 'text/plain')
28 @root = (::File.expand_path(root) if root)
30 @default_mime = default_mime
31 @head = Rack::Head.new(lambda { |env| get env })
35 # HEAD requests drop the response body, including 4xx error messages.
40 request = Rack::Request.new env
41 unless ALLOWED_VERBS.include? request.request_method
42 return fail(405, "Method Not Allowed", { 'allow' => ALLOW_HEADER })
45 path_info = Utils.unescape_path request.path_info
46 return fail(400, "Bad Request") unless Utils.valid_path?(path_info)
48 clean_path_info = Utils.clean_path_info(path_info)
49 path = ::File.join(@root, clean_path_info)
52 ::File.file?(path) && ::File.readable?(path)
53 rescue SystemCallError
54 # Not sure in what conditions this exception can occur, but this
55 # is a safe way to handle such an error.
62 serving(request, path)
64 fail(404, "File not found: #{path_info}")
68 def serving(request, path)
70 return [200, { 'allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []]
72 last_modified = ::File.mtime(path).httpdate
73 return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
75 headers = { "last-modified" => last_modified }
76 mime_type = mime_type path, @default_mime
77 headers[CONTENT_TYPE] = mime_type if mime_type
80 headers.merge!(@headers) if @headers
85 ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
88 ranges = [0..size - 1]
90 # Unsatisfiable. Return error, and file size:
91 response = fail(416, "Byte range unsatisfiable")
92 response[1]["content-range"] = "bytes */#{size}"
96 partial_content = true
100 headers["content-range"] = "bytes #{range.begin}-#{range.end}/#{size}"
102 headers[CONTENT_TYPE] = "multipart/byteranges; boundary=#{MULTIPART_BOUNDARY}"
106 body = BaseIterator.new(path, ranges, mime_type: mime_type, size: size)
110 headers[CONTENT_LENGTH] = size.to_s
114 elsif !partial_content
115 body = Iterator.new(path, ranges, mime_type: mime_type, size: size)
118 [status, headers, body]
122 attr_reader :path, :ranges, :options
124 def initialize(path, ranges, options)
131 ::File.open(path, "rb") do |file|
132 ranges.each do |range|
133 yield multipart_heading(range) if multipart?
135 each_range_part(file, range) do |part|
140 yield "\r\n--#{MULTIPART_BOUNDARY}--\r\n" if multipart?
145 size = ranges.inject(0) do |sum, range|
146 sum += multipart_heading(range).bytesize if multipart?
149 size += "\r\n--#{MULTIPART_BOUNDARY}--\r\n".bytesize if multipart?
161 def multipart_heading(range)
164 --#{MULTIPART_BOUNDARY}\r
165 content-type: #{options[:mime_type]}\r
166 content-range: bytes #{range.begin}-#{range.end}/#{options[:size]}\r
171 def each_range_part(file, range)
172 file.seek(range.begin)
173 remaining_len = range.end - range.begin + 1
174 while remaining_len > 0
175 part = file.read([8192, remaining_len].min)
177 remaining_len -= part.length
184 class Iterator < BaseIterator
190 def fail(status, body, headers = {})
196 CONTENT_TYPE => "text/plain",
197 CONTENT_LENGTH => body.size.to_s,
198 "x-cascade" => "pass"
204 # The MIME type for the contents of the file located at @path
205 def mime_type(path, default_mime)
206 Mime.mime_type(::File.extname(path), default_mime)
210 # We check via File::size? whether this file provides size info
211 # via stat (e.g. /proc files often don't), otherwise we have to
212 # figure it out by reading the whole file into memory.
213 ::File.size?(path) || ::File.read(path).bytesize