diff options
author | Postmodern <postmodern.mod3@gmail.com> | 2012-01-04 12:08:44 -0800 |
---|---|---|
committer | Samuel Williams <samuel.williams@oriontransfer.co.nz> | 2019-11-15 11:06:20 +0900 |
commit | 626272b2bc6b270bbd3d2aa6aaa64722350f42be (patch) | |
tree | a10e2d103ef2bc6f900525cc4ae313520b699ffb | |
parent | 3fc03d35d8191d584ce3b2c9575dd243ed1a6f1a (diff) | |
download | rack-626272b2bc6b270bbd3d2aa6aaa64722350f42be.tar.gz |
Renamed Rack::File to Rack::Files, since it can serve multiple files from a root directory.
* Left a Rack::File constant, for backwards compatibility.
-rw-r--r-- | CHANGELOG.md | 3 | ||||
-rw-r--r-- | README.rdoc | 2 | ||||
-rw-r--r-- | lib/rack/directory.rb | 4 | ||||
-rw-r--r-- | lib/rack/file.rb | 176 | ||||
-rw-r--r-- | lib/rack/files.rb | 178 | ||||
-rw-r--r-- | lib/rack/sendfile.rb | 4 | ||||
-rw-r--r-- | lib/rack/static.rb | 6 | ||||
-rw-r--r-- | test/spec_cascade.rb | 4 | ||||
-rw-r--r-- | test/spec_files.rb (renamed from test/spec_file.rb) | 60 |
9 files changed, 223 insertions, 214 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c661404..1825ddc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ _Note: There are many unreleased changes in Rack (`master` is around 300 commits - Add Falcon to the default handler fallbacks. ([@ioquatix](https://github.com/ioquatix)) - Update codebase to avoid string mutations in preparation for `frozen_string_literals`. ([@pat](https://github.com/pat)) - Change `MockRequest#env_for` to rely on the input optionally responding to `#size` instead of `#length`. ([@janko](https://github.com/janko)) +- Rename `Rack::File` -> `Rack::Files` and add deprecation notice. ([@postmodern](https://github.com/postmodern)). ### Removed @@ -216,7 +217,7 @@ Items below this line are from the previously maintained HISTORY.md and NEWS.md - Rack::Auth::AbstractRequest#scheme now yields strings, not symbols - Rack::Utils cookie functions now format expires in RFC 2822 format - Rack::File now has a default mime type - - rackup -b 'run Rack::File.new(".")', option provides command line configs + - rackup -b 'run Rack::Files.new(".")', option provides command line configs - Rack::Deflater will no longer double encode bodies - Rack::Mime#match? provides convenience for Accept header matching - Rack::Utils#q_values provides splitting for Accept headers diff --git a/README.rdoc b/README.rdoc index 53a36283..d82536ba 100644 --- a/README.rdoc +++ b/README.rdoc @@ -76,7 +76,7 @@ applications needs using middleware, for example: * Rack::CommonLogger, for creating Apache-style logfiles. * Rack::ShowException, for catching unhandled exceptions and presenting them in a nice and helpful way with clickable backtrace. -* Rack::File, for serving static files. +* Rack::Files, for serving static files. * ...many others! All these components use the same interface, which is described in diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb index f0acc40d..88a283c8 100644 --- a/lib/rack/directory.rb +++ b/lib/rack/directory.rb @@ -10,7 +10,7 @@ module Rack # will be presented in an html based index. If a file is found, the env will # be passed to the specified +app+. # - # If +app+ is not specified, a Rack::File of the same +root+ will be used. + # If +app+ is not specified, a Rack::Files of the same +root+ will be used. class Directory DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>" @@ -60,7 +60,7 @@ table { width:100%%; } def initialize(root, app = nil) @root = ::File.expand_path(root) - @app = app || Rack::File.new(@root) + @app = app || Rack::Files.new(@root) @head = Rack::Head.new(lambda { |env| get env }) end diff --git a/lib/rack/file.rb b/lib/rack/file.rb index 425c1d38..85d5be72 100644 --- a/lib/rack/file.rb +++ b/lib/rack/file.rb @@ -1,178 +1,8 @@ # frozen_string_literal: true -require 'time' -require 'rack/utils' -require 'rack/mime' -require 'rack/request' -require 'rack/head' +require 'rack/files' 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 = ::File.expand_path 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 response_body.bytesize 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 + warn "Rack::File is deprecated, please use Rack::Files instead." + File = Files end diff --git a/lib/rack/files.rb b/lib/rack/files.rb new file mode 100644 index 00000000..61658e5c --- /dev/null +++ b/lib/rack/files.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +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::Files.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 Files + ALLOWED_VERBS = %w[GET HEAD OPTIONS] + ALLOW_HEADER = ALLOWED_VERBS.join(', ') + + attr_reader :root + + def initialize(root, headers = {}, default_mime = 'text/plain') + @root = ::File.expand_path 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 response_body.bytesize 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 diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb index 51ba4db5..3774b260 100644 --- a/lib/rack/sendfile.rb +++ b/lib/rack/sendfile.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rack/file' +require 'rack/files' require 'rack/body_proxy' module Rack @@ -16,7 +16,7 @@ module Rack # # In order to take advantage of this middleware, the response body must # respond to +to_path+ and the request must include an X-Sendfile-Type - # header. Rack::File and other components implement +to_path+ so there's + # header. Rack::Files and other components implement +to_path+ so there's # rarely anything you need to do in your application. The X-Sendfile-Type # header is typically set in your web servers configuration. The following # sections attempt to document diff --git a/lib/rack/static.rb b/lib/rack/static.rb index 512e4da9..9a0017db 100644 --- a/lib/rack/static.rb +++ b/lib/rack/static.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "rack/file" +require "rack/files" require "rack/utils" require_relative 'core_ext/regexp' @@ -9,7 +9,7 @@ module Rack # The Rack::Static middleware intercepts requests for static files # (javascript files, images, stylesheets, etc) based on the url prefixes or - # route mappings passed in the options, and serves them using a Rack::File + # route mappings passed in the options, and serves them using a Rack::Files # object. This allows a Rack stack to serve both static and dynamic content. # # Examples: @@ -100,7 +100,7 @@ module Rack # Allow for legacy :cache_control option while prioritizing global header_rules setting @header_rules.unshift([:all, { CACHE_CONTROL => options[:cache_control] }]) if options[:cache_control] - @file_server = Rack::File.new(root) + @file_server = Rack::Files.new(root) end def add_index_root?(path) diff --git a/test/spec_cascade.rb b/test/spec_cascade.rb index a06aefeb..abb7b57f 100644 --- a/test/spec_cascade.rb +++ b/test/spec_cascade.rb @@ -3,7 +3,7 @@ require 'minitest/global_expectations/autorun' require 'rack' require 'rack/cascade' -require 'rack/file' +require 'rack/files' require 'rack/lint' require 'rack/urlmap' require 'rack/mock' @@ -14,7 +14,7 @@ describe Rack::Cascade do end docroot = File.expand_path(File.dirname(__FILE__)) - app1 = Rack::File.new(docroot) + app1 = Rack::Files.new(docroot) app2 = Rack::URLMap.new("/crash" => lambda { |env| raise "boom" }) diff --git a/test/spec_file.rb b/test/spec_files.rb index ba713c61..ad57972d 100644 --- a/test/spec_file.rb +++ b/test/spec_files.rb @@ -1,21 +1,21 @@ # frozen_string_literal: true require 'minitest/global_expectations/autorun' -require 'rack/file' +require 'rack/files' require 'rack/lint' require 'rack/mock' -describe Rack::File do +describe Rack::Files do DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT - def file(*args) - Rack::Lint.new Rack::File.new(*args) + def files(*args) + Rack::Lint.new Rack::Files.new(*args) end it 'serves files with + in the file name' do Dir.mktmpdir do |dir| File.write File.join(dir, "you+me.txt"), "hello world" - app = file(dir) + app = files(dir) env = Rack::MockRequest.env_for("/you+me.txt") status, _, body = app.call env @@ -28,14 +28,14 @@ describe Rack::File do end it "serve files" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test") res.must_be :ok? assert_match(res, /ruby/) end it "set Last-Modified header" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test") path = File.join(DOCROOT, "/cgi/test") @@ -45,7 +45,7 @@ describe Rack::File do it "return 304 if file isn't modified since last serve" do path = File.join(DOCROOT, "/cgi/test") - res = Rack::MockRequest.new(file(DOCROOT)). + res = Rack::MockRequest.new(files(DOCROOT)). get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => File.mtime(path).httpdate) res.status.must_equal 304 @@ -54,14 +54,14 @@ describe Rack::File do it "return the file if it's modified since last serve" do path = File.join(DOCROOT, "/cgi/test") - res = Rack::MockRequest.new(file(DOCROOT)). + res = Rack::MockRequest.new(files(DOCROOT)). get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => (File.mtime(path) - 100).httpdate) res.must_be :ok? end it "serve files with URL encoded filenames" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/%74%65%73%74") # "/cgi/test" + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/%74%65%73%74") # "/cgi/test" res.must_be :ok? # res.must_match(/ruby/) # nope @@ -71,12 +71,12 @@ describe Rack::File do end it "serve uri with URL encoded null byte (%00) in filenames" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test%00") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test%00") res.must_be :bad_request? end it "allow safe directory traversal" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) res = req.get('/cgi/../cgi/test') res.must_be :successful? @@ -89,7 +89,7 @@ describe Rack::File do end it "not allow unsafe directory traversal" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) res = req.get("/../README.rdoc") res.must_be :client_error? @@ -104,7 +104,7 @@ describe Rack::File do end it "allow files with .. in their name" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) res = req.get("/cgi/..test") res.must_be :not_found? @@ -116,33 +116,33 @@ describe Rack::File do end it "not allow unsafe directory traversal with encoded periods" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/%2E%2E/README") + res = Rack::MockRequest.new(files(DOCROOT)).get("/%2E%2E/README") res.must_be :client_error? res.must_be :not_found? end it "allow safe directory traversal with encoded periods" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/%2E%2E/cgi/test") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/%2E%2E/cgi/test") res.must_be :successful? end it "404 if it can't find the file" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/blubb") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/blubb") res.must_be :not_found? end it "detect SystemCallErrors" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi") res.must_be :not_found? end it "return bodies that respond to #to_path" do env = Rack::MockRequest.env_for("/cgi/test") - status, _, body = Rack::File.new(DOCROOT).call(env) + status, _, body = Rack::Files.new(DOCROOT).call(env) path = File.join(DOCROOT, "/cgi/test") @@ -154,7 +154,7 @@ describe Rack::File do it "return correct byte range in body" do env = Rack::MockRequest.env_for("/cgi/test") env["HTTP_RANGE"] = "bytes=22-33" - res = Rack::MockResponse.new(*file(DOCROOT).call(env)) + res = Rack::MockResponse.new(*files(DOCROOT).call(env)) res.status.must_equal 206 res["Content-Length"].must_equal "12" @@ -165,7 +165,7 @@ describe Rack::File do it "return error for unsatisfiable byte range" do env = Rack::MockRequest.env_for("/cgi/test") env["HTTP_RANGE"] = "bytes=1234-5678" - res = Rack::MockResponse.new(*file(DOCROOT).call(env)) + res = Rack::MockResponse.new(*files(DOCROOT).call(env)) res.status.must_equal 416 res["Content-Range"].must_equal "bytes */208" @@ -173,7 +173,7 @@ describe Rack::File do it "support custom http headers" do env = Rack::MockRequest.env_for("/cgi/test") - status, heads, _ = file(DOCROOT, 'Cache-Control' => 'public, max-age=38', + status, heads, _ = files(DOCROOT, 'Cache-Control' => 'public, max-age=38', 'Access-Control-Allow-Origin' => '*').call(env) status.must_equal 200 @@ -183,7 +183,7 @@ describe Rack::File do it "support not add custom http headers if none are supplied" do env = Rack::MockRequest.env_for("/cgi/test") - status, heads, _ = file(DOCROOT).call(env) + status, heads, _ = files(DOCROOT).call(env) status.must_equal 200 heads['Cache-Control'].must_be_nil @@ -191,7 +191,7 @@ describe Rack::File do end it "only support GET, HEAD, and OPTIONS requests" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) forbidden = %w[post put patch delete] forbidden.each do |method| @@ -209,7 +209,7 @@ describe Rack::File do end it "set Allow correctly for OPTIONS requests" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) res = req.options('/cgi/test') res.must_be :successful? res.headers['Allow'].wont_equal nil @@ -217,35 +217,35 @@ describe Rack::File do end it "set Content-Length correctly for HEAD requests" do - req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))) + req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT))) res = req.head "/cgi/test" res.must_be :successful? res['Content-Length'].must_equal "208" end it "default to a mime type of text/plain" do - req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))) + req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT))) res = req.get "/cgi/test" res.must_be :successful? res['Content-Type'].must_equal "text/plain" end it "allow the default mime type to be set" do - req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, 'application/octet-stream'))) + req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT, nil, 'application/octet-stream'))) res = req.get "/cgi/test" res.must_be :successful? res['Content-Type'].must_equal "application/octet-stream" end it "not set Content-Type if the mime type is not set" do - req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, nil))) + req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT, nil, nil))) res = req.get "/cgi/test" res.must_be :successful? res['Content-Type'].must_be_nil end it "return error when file not found for head request" do - res = Rack::MockRequest.new(file(DOCROOT)).head("/cgi/missing") + res = Rack::MockRequest.new(files(DOCROOT)).head("/cgi/missing") res.must_be :not_found? res.body.must_be :empty? end |