Created
December 23, 2010 05:09
-
-
Save samleb/752590 to your computer and use it in GitHub Desktop.
Experimenting on caching `stat` calls in Rack::File
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| require 'time' | |
| require 'rack/utils' | |
| require 'rack/mime' | |
| 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 | |
| attr_accessor :root | |
| attr_accessor :path | |
| alias :to_path :path | |
| def initialize(root, options = {}) | |
| @root = root | |
| unless options[:cache] == false | |
| @cache = {} | |
| @cache_size = options[:cache_size] || 1000 | |
| end | |
| end | |
| def call(env) | |
| dup._call(env) | |
| end | |
| F = ::File | |
| def _call(env) | |
| @path_info = Utils.unescape(env["PATH_INFO"]) | |
| return fail(403, "Forbidden") if @path_info.include? ".." | |
| @path = F.join(@root, @path_info) | |
| if attrs = size_and_mtime | |
| serving(env, *attrs) | |
| else | |
| fail(404, "File not found: #{@path_info}") | |
| end | |
| end | |
| def serving(env, size, mtime) | |
| response = [ | |
| 200, | |
| { | |
| "Last-Modified" => mtime, | |
| "Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain') | |
| }, | |
| self | |
| ] | |
| 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 | |
| # 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[1]["Content-Length"] = size.to_s | |
| response | |
| end | |
| def each | |
| 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 size_and_mtime | |
| return get_size_and_mtime unless @cache | |
| unless entry = @cache[@path] | |
| entry = @cache[@path] = get_size_and_mtime || :unavailable | |
| end | |
| prune_cache if @cache.size > @cache_size | |
| entry if entry != :unavailable | |
| end | |
| def get_size_and_mtime | |
| return unless F.file?(@path) && F.readable?(@path) | |
| # 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)) | |
| mtime = F.mtime(@path).httpdate | |
| [size, mtime] | |
| rescue SystemCallError | |
| end | |
| def prune_cache | |
| target_size = (@cache_size * 0.75).to_i | |
| excess = @cache.size - target_size | |
| excess = prune_cache_entries(excess, true) | |
| prune_cache_entries(excess) if excess > 0 | |
| end | |
| def prune_cache_entries(excess, only_unavailable = false) | |
| @cache.each do |path, attrs| | |
| @cache.delete(path) if !only_unavailable || attrs == :unavailable | |
| return 0 if (excess -= 1).zero? | |
| end | |
| excess | |
| end | |
| def fail(status, body) | |
| body += "\n" | |
| [ | |
| status, | |
| { | |
| "Content-Type" => "text/plain", | |
| "Content-Length" => body.size.to_s, | |
| "X-Cascade" => "pass" | |
| }, | |
| [body] | |
| ] | |
| end | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment