lib/rack/file.rb in rack-1.2.8 vs lib/rack/file.rb in rack-1.3.0.beta

- old
+ new

@@ -10,83 +10,116 @@ # # Handlers can detect if bodies are a Rack::File, and use mechanisms # like sendfile on the +path+. class File + SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact) + attr_accessor :root attr_accessor :path + attr_accessor :cache_control alias :to_path :path - def initialize(root) + def initialize(root, cache_control = nil) @root = root + @cache_control = cache_control end def call(env) dup._call(env) end F = ::File def _call(env) @path_info = Utils.unescape(env["PATH_INFO"]) - return forbidden if @path_info.include? ".." + parts = @path_info.split SEPS - @path = F.join(@root, @path_info) + return fail(403, "Forbidden") if parts.include? ".." - begin - if F.file?(@path) && F.readable?(@path) - serving - else - raise Errno::EPERM - end + @path = F.join(@root, *parts) + + available = begin + F.file?(@path) && F.readable?(@path) rescue SystemCallError - not_found + false end - end - def forbidden - body = "Forbidden\n" - [403, {"Content-Type" => "text/plain", - "Content-Length" => body.size.to_s, - "X-Cascade" => "pass"}, - [body]] + if available + serving(env) + else + fail(404, "File not found: #{@path_info}") + end end - # NOTE: - # 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. And while - # we're at it we also use this as body then. + def serving(env) + # NOTE: + # 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. + size = F.size?(@path) || Utils.bytesize(F.read(@path)) - def serving - if size = F.size?(@path) - body = self + response = [ + 200, + { + "Last-Modified" => F.mtime(@path).httpdate, + "Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain') + }, + self + ] + response[1].merge! 'Cache-Control' => @cache_control if @cache_control + + ranges = Rack::Utils.byte_ranges(env, 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 - body = [F.read(@path)] - size = Utils.bytesize(body.first) + # 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 - [200, { - "Last-Modified" => F.mtime(@path).httpdate, - "Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain'), - "Content-Length" => size.to_s - }, body] + response[1]["Content-Length"] = size.to_s + response end - def not_found - body = "File not found: #{@path_info}\n" - [404, {"Content-Type" => "text/plain", - "Content-Length" => body.size.to_s, - "X-Cascade" => "pass"}, - [body]] - end - def each - F.open(@path, "rb") { |file| - while part = file.read(8192) + F.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 + + private + + def fail(status, body) + body += "\n" + [ + status, + { + "Content-Type" => "text/plain", + "Content-Length" => body.size.to_s, + "X-Cascade" => "pass" + }, + [body] + ] + end + end end