file.rb 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. require 'time'
  2. require 'rack/utils'
  3. require 'rack/mime'
  4. module Rack
  5. # Rack::File serves files below the +root+ directory given, according to the
  6. # path info of the Rack request.
  7. # e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file
  8. # as http://localhost:9292/passwd
  9. #
  10. # Handlers can detect if bodies are a Rack::File, and use mechanisms
  11. # like sendfile on the +path+.
  12. class File
  13. SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
  14. ALLOWED_VERBS = %w[GET HEAD]
  15. attr_accessor :root
  16. attr_accessor :path
  17. attr_accessor :cache_control
  18. alias :to_path :path
  19. def initialize(root, cache_control = nil)
  20. @root = root
  21. @cache_control = cache_control
  22. end
  23. def call(env)
  24. dup._call(env)
  25. end
  26. F = ::File
  27. def _call(env)
  28. unless ALLOWED_VERBS.include? env["REQUEST_METHOD"]
  29. return fail(405, "Method Not Allowed")
  30. end
  31. @path_info = Utils.unescape(env["PATH_INFO"])
  32. parts = @path_info.split SEPS
  33. parts.inject(0) do |depth, part|
  34. case part
  35. when '', '.'
  36. depth
  37. when '..'
  38. return fail(404, "Not Found") if depth - 1 < 0
  39. depth - 1
  40. else
  41. depth + 1
  42. end
  43. end
  44. @path = F.join(@root, *parts)
  45. available = begin
  46. F.file?(@path) && F.readable?(@path)
  47. rescue SystemCallError
  48. false
  49. end
  50. if available
  51. serving(env)
  52. else
  53. fail(404, "File not found: #{@path_info}")
  54. end
  55. end
  56. def serving(env)
  57. last_modified = F.mtime(@path).httpdate
  58. return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
  59. response = [
  60. 200,
  61. {
  62. "Last-Modified" => last_modified,
  63. "Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain')
  64. },
  65. env["REQUEST_METHOD"] == "HEAD" ? [] : self
  66. ]
  67. response[1].merge! 'Cache-Control' => @cache_control if @cache_control
  68. # NOTE:
  69. # We check via File::size? whether this file provides size info
  70. # via stat (e.g. /proc files often don't), otherwise we have to
  71. # figure it out by reading the whole file into memory.
  72. size = F.size?(@path) || Utils.bytesize(F.read(@path))
  73. ranges = Rack::Utils.byte_ranges(env, size)
  74. if ranges.nil? || ranges.length > 1
  75. # No ranges, or multiple ranges (which we don't support):
  76. # TODO: Support multiple byte-ranges
  77. response[0] = 200
  78. @range = 0..size-1
  79. elsif ranges.empty?
  80. # Unsatisfiable. Return error, and file size:
  81. response = fail(416, "Byte range unsatisfiable")
  82. response[1]["Content-Range"] = "bytes */#{size}"
  83. return response
  84. else
  85. # Partial content:
  86. @range = ranges[0]
  87. response[0] = 206
  88. response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
  89. size = @range.end - @range.begin + 1
  90. end
  91. response[1]["Content-Length"] = size.to_s
  92. response
  93. end
  94. def each
  95. F.open(@path, "rb") do |file|
  96. file.seek(@range.begin)
  97. remaining_len = @range.end-@range.begin+1
  98. while remaining_len > 0
  99. part = file.read([8192, remaining_len].min)
  100. break unless part
  101. remaining_len -= part.length
  102. yield part
  103. end
  104. end
  105. end
  106. private
  107. def fail(status, body)
  108. body += "\n"
  109. [
  110. status,
  111. {
  112. "Content-Type" => "text/plain",
  113. "Content-Length" => body.size.to_s,
  114. "X-Cascade" => "pass"
  115. },
  116. [body]
  117. ]
  118. end
  119. end
  120. end